I've made a bunch of progress to DeathRun this update.  Here's what is now done.

  • Added in-game changing of Death's acceleration and not just velocity
  • Added debug code to help me see what is going on for animations
  • Added animations to Death and Guy
  • Changed map
  • Camera follows player instead of static placement
  • Added support for multiple platform types
  • Drop through platforms added
  • Death kills targets when he hits them
  • Enemies hurt player
  • Dead player and enemies respawn on from the top of the screen.

This is quite a lot, but really most of the code is very simple.  Click Read More for more details or to play the game in its current state.

Change Death's acceleration

I made a realization while messing around with Death's variables last night.  The difficulty in evading Death comes from two things.  First, Death's maxVelocity determines how quickly Death can chase you.  Obviously the faster Death moves the less time the player has to react to him, so the game becomes harder as I add more elements.  However, I failed to account for the amount that acceleration plays into Death's difficulty.  Right now, Death's acceleration is set using a variable I called currentSpeedRatio which is poorly named.  Death's acceleration is set every update to maxVelocity * currentSpeedRatio.  CurrentSpeedRatio was set to 10, so Death could corner sharply and chase the player effectively.  By setting this variable much lower Death reacts to changes in the player's location much more slowly, which is something that I like and more in line with how I originally envisioned it. 

So I decided to put in the ability to change the currentSpeedRatio in the game. First I added two new keys to InputHelper in Main.hx

InputHelper.addButton("accelup");
InputHelper.addButton("acceldown");
InputHelper.assignKeyToButton("C", "accelup");
InputHelper.assignKeyToButton("Z", "acceldown");

Then it is a matter of just listening for these key presses in the LevelState.update() function and changing Death's variables when necessary. 

if (InputHelper.isButtonJustPressed("accelup")) {
	_death.currentSpeedRatio += .5;
	setDebugText();
}
	if (InputHelper.isButtonJustPressed("acceldown")) {
	_death.currentSpeedRatio -= .5;
	setDebugText();
}

You might notice that I added a new function called setDebugText().  This just changes the text at the top of the screen to display more stuff.  Here's the code for it:

public function setDebugText() {
	debugText.text = "Player health: " + _p.health;
	debugText.text += "\nDeath speed: " + _death.maxSpeed;
	debugText.text += "\nDeath acceleration: " + _death.currentSpeedRatio;
}

I also added the player's health to the list of things displayed because I know I'm going to need it later.  Now when I run the game I can change Death's acceleration values in real time.  I'm noticing that I like it more with lower acceleration and faster movement speeds.  Death loops around the screen and reacts more slowly to player movement, but I'm also able to set the speed a lot higher and still have the game be possible.  I'll see what values I end up on.

Animations and debugging

Now that I'm in a somewhat playable form I think I'm ready to start with adding graphics to the game.  Before I do that though, I'm going to need to turn on some debugging code so I can see the hitboxes.  HaxeFlixel allows the graphics and hitboxes to be different sizes which is a nice feature, so I want to see the hitboxes all the time.  Debugging is easy to turn on. 

FlxG.debugger.drawDebug = true;

Now I need a plan for how I'm going to make and use my graphics.  HaxeFlixel has a lot of options when it comes to importing and displaying images, but there are some basic things to keep in mind.  First, when you want to draw graphics to a screen the computer first has to bind it, which makes it available to be drawn on the screen.  Each time a new graphic should be drawn it must also be bound.  The binding process is computationally expensive, so a way to make your game faster is to reduce the number of texture binds you call.  I could just make a sprite sheet for each of my game entities and then use those to draw on the screen.  But instead, I'm going to use a TextureAtlas, which is a fancy term for one really large image that contains a bunch of smaller ones.  If I combine all my spritesheets into a single textureAtlas I have to bind one image instead of one for each sprite in my game.  Right now I have Death, the player, some slimes, and the map for textures, so I need to bind four different images.  If I'm targetting 60 frames a second that is 240 texture binds a second.  If I put them all in an atlas I can only bind one image per update which can increase the speed my game runs at.

Also, I can use some helpful functions that come with TextureAtlases which I like.

So now I just need to generate my graphics.  I've recently been using a program called Spriter to make my sprites, which I am very much enjoying.  After messing around with Guy for awhile, this is the running animation I settled on.

which, when animated, will look like this:

When it comes time to actually export my sprites I will export them all as individual numbered images for packing into a TextureAtlas.  I also created some standing and jumping images.  Next up was Death:

His movement is subtle but will hopefully look good when played at a normal resolution.  I'm not much of an artist, so these all might change later as I keep messing around with them, but they will do for now. 

To pack them all up I use a tool called Texture Packer, available in lite (free) and full versions.  The free one is sufficient for my needs now.  Using the tool is beyond the scope of this, but I just need to make sure that I'm exporting the data as a generic JSON file so HaxeFlixel can read it no problems.  After packing the textures, I get a SpriteSheet that looks like this...

And a JSON file that looks like this

{"frames": [

{
	"filename": "DeathFlying__000.png",
	"frame": {"x":2,"y":2,"w":122,"h":71},
	"rotated": false,
	"trimmed": false,
	"spriteSourceSize": {"x":0,"y":0,"w":122,"h":71},
	"sourceSize": {"w":122,"h":71},
	"pivot": {"x":0.5,"y":0.5}
},
{
	"filename": "DeathFlying__001.png",
	"frame": {"x":126,"y":2,"w":122,"h":71},
	"rotated": false,
	"trimmed": false,
	"spriteSourceSize": {"x":0,"y":0,"w":122,"h":71},
	"sourceSize": {"w":122,"h":71},
	"pivot": {"x":0.5,"y":0.5}
},
{
	"filename": "DeathFlying__002.png",
	"frame": {"x":250,"y":2,"w":122,"h":71},
	"rotated": false,
	"trimmed": false,
	"spriteSourceSize": {"x":0,"y":0,"w":122,"h":71},
	"sourceSize": {"w":122,"h":71},
	"pivot": {"x":0.5,"y":0.5}
}, ... etc... etc...

Each statement has a reference to the image and describes where the image starts, how large it is, if it is trimmed and rotated, etc. 

Adding the animation to the sprite

Getting this into one of my sprites only takes a couple lines of code.  Each FlxSprite object has an Animation object built into it cleverly named animation which we will use to generate and play the animations.  I'll go into my Player class and add the code needed for it to it work..

		var tex = new TexturePackerData("assets/images/atlas.json", "assets/images/images.png");
		loadGraphicFromTexture(tex);
		animation.addByPrefix("run", "GuyRunning", 60, true);
		animation.addByPrefix("stand", "GuyStanding", 60, true);
		animation.addByPrefix("jump", "GuyJumping", 60, true);
		animation.play("run");
		width = 40;
		height = 40;
		centerOffsets();
		centerOrigin();
		this.setFacingFlip(FlxObject.RIGHT, true, false);
		this.setFacingFlip(FlxObject.LEFT, false, false);

Ok, so a lot of code.  Lets walk through it.

var tex = new TexturePackerData("assets/images/atlas.json", "assets/images/images.png");

The first line creates a new TexturePackerData object that I call tex.  A TexturePackerData object takes two parameters.  The first is the .json file that contains all the locations for our images on our spritesheet, and the second parameter is the path to the spritesheet itself.  The TexturePackerData object by itself doesn't do much, but I can load a sprite with this TexturePackerData so I can use the images in it.

loadGraphicFromTexture(tex);

Now I've got my Sprite loaded with images and I'm ready to set up animations.  If I look at my .json file I can see that I have 30 images in my TextureFileData that I want to use to build the running animation for the player.  These images are called GuyRunning__000.png through GuyRunning__030.png.  Since all of the running animations have the same prefix I can create the animation with the addByPrefix command of the Player.animation object.

animation.addByPrefix("run", "GuyRunning", 60, true);
animation.addByPrefix("stand", "GuyStanding", 60, true);
animation.addByPrefix("jump", "GuyJumping", 60, true);
animation.play("run");

The animation.addByPrefix() function takes three arguments.  The first is a label for this animation.  This is the name that I am going to call it throughout my code.  I use simple, all lower case descriptions like running and jumping across all my entities to keep things consistent.  The second parameter is the prefix of the images that should be turned into this animation.  In the GuyRunning case, it grabs GuyRunning__000.png through GuyRunning__030.png and will loop through them to play the animation.  The third parameter is the frames per second this animation should play at.  I'm targetting 60 frames per second and I have 30 frames in my animation, so my animation will loop through twice per second.  This will mean that the animation will play at an appropriate speed compared to how fast my player is moving.  I can raise this number to make the animation play faster or lower it to play slower.  Last is the looping parameter which determines if the animation will loop back through after finishing.  If this was false, it would stay on the last frame when the animation was over.

The animation.play("run") just tells the sprite to start playing the running animation. 

The next lines of code changes around out hitbox.  When you load an image into a sprite it automatically creates a hitbox the size of the image.  Now that I loaded multiple animations my hitbox becomes the size of the largest one.  The problem with this is that I actually want my hitbox to be smaller than the images.  It isn't any fun playing a game with bad hit detection so if my hitbox extends beyond the front and back of my sprite it can lead to instances where a player swears that he dodged Death but got killed anyways.  Better to set all the hitboxes a little small and have near misses go in favor of the player.

width = 40;
height = 40;
centerOffsets();
centerOrigin();

I'm going to set my width and height to the size of a single tile.  My animations were made at about 50x50 pixles on purpose so they would fit nicely around a 40x40 hitbox.  I also made sure to make the center of the images in roughly the same place, so my centerOffsets() call will put the images in the center of the hitbox.  Last, I call centerOrigins(), which moves the origin of the sprite to the center if the images also.  This will let me let the images scale and rotate properly if I ever decide to do that.

You might notice that I only have images that face left.  This is a problem because I'm sure that players will eventually get tired of only moving left and want to run right, mostly because I start them on the far left of the map.  Luckily, I don't have to make images facing right also because HaxeFlixel can generate a bunch of flipped images for me. 

this.setFacingFlip(FlxObject.RIGHT, true, false);
this.setFacingFlip(FlxObject.LEFT, false, false);

I use the setFacingFlip() function to create these flipped images.  The setFacingFlip() function takes three parameters.  The first is the facing of the sprite that we need to modify.  The second and third are booleans that will flip the image along the X and Y axis if set to true.  So my code says if the sprite is facing right, flip the images on the X axis.  If I'm facing right, don't.  Now I just need to remember to set the facing later in my code.

  Now I need to change the update() function so it plays the appropriate animations at the correct time.  To keep my code easier to read, I'll create a function called graphicsCode() and call it from my update() right after my movementCode() call.

movementCode();
graphicsCode();

Here's graphicsCode():

public function graphicsCode() {

	//Are we facing left or right?
	if (velocity.x > 0)
	facing = FlxObject.RIGHT;
	if (velocity.x < 0)
	facing = FlxObject.LEFT;
		
	if(isTouching(FlxObject.FLOOR)) {
        	//If we have a positive velocity we are moving right.
            if (velocity.x != 0)
            animation.play("run");
            else
            animation.play("stand");

	}
	else
	animation.play("jump");
}

The first two if(velocity.x) statements just set the facing of my sprite.  If My velocity.x is positive, I'm facing right.  If not, I'm facing left.  You'll notice that If the velocity is 0 then the facing just doesn't change, so the player will remain facing the same direction he was last moving in.  Then we have a simple if(isTouching(FlxObject.FLOOR)) call which determines if I'm on the floor or not.  If not, play the jumping animation.  If so, I have a little more work to do.

Then I just need to determine if the player is moving or not.  If the player's velocity is zero we are standing still.  Otherwise we are moving.  Simple.

Death's animation code is almost identical, just with different names and a lack of isTouching(FlxObject.FLOOR), since he in fact is flying.  Now the basic animation is in place.

Changes to the map

I also made some changes to how I load the maps to give me some more options in game.  I want to have certain platforms that are solid and others that only function in one direction.  So the platform can be jumped through from the bottom and landed on.  The player will also be able to drop through these platforms giving him some extra options when avoiding Death and killing monsters.  

 

The first thing that I did was make another image that will be my temporary test map.  I used the same black and white scheme that I did last time, but this time I also added another color.  I chose Red to be the color that represents the one way platforms that the player can jump through.  I also made the level larger than the screen so I can get the camera to follow the player.  Here's my level image.

One thing that is very important is that the red be pure red.  Pure red will have a specific hex value that we will be searching for.  So the RGB value must be 255,0,0.  I also needed a new tileset so I can tell which tiles should be which.  I made a bunch of 40x40 tiles and numbered them.  

I know it looks ugly.  It is actually another tileset that was only 16x16 that I blew up, but then I needed to add a 0 tile so the numbers would line up.  

map.loadMap(FlxStringUtil.bitmapToCSV(Assets.getBitmapData("assets/images/testlevel2.png"), false, 1, [0x00FFFFFF, 0x00000000, 0x00FF0000]), "assets/images/numbers40px.png");

This code is a little more complicated than it used to be, but it is doing essentually the same things with one additional feature.  I am again using the FlxStringUtil.bitmapToCSV() function to turn the image into a string that the map.loadMap() function can use, but this time I am specifying some additional options.  The next parameter asks if I want to inverse the colors, so white would be solid and black would be empty.  I don't want that so I set it to false, which is the default value anyway.  Next is the scale.  I leave it as the default value of 1, but if I picked two then each pixel in my image would be transformed into a 2x2 block of tiles in my map.  The fourth parameter is where I am going to tell it to look for Red.  

The fourth parameter wants an array of Integers that describes what colors should equate to what tile numbers.  I am supplying these integers in hexidecimal so I can read it.  the 0x lets the compiler know that I'm specifying a hexidecimal number and the numbers after the x contain my values.

So the first two numbers represent the Alpha property (how clear the color is.  In this case 00 means fully opaque.    The second set of numbers is the Red value, followed by Green, and lastly Blue.  In this case FF is hexidecimal for 255, which means this is a folly opaque white.  So [0x00FFFFFF, 0x00000000, 0x00FF0000] says that 0 should be White, 1s are Black, and 2s are Red.  You can convert decimal to hex with online tools like this one.

Now when I test my map I can run off the edge and disappear.  Looks like I need to fix my camera.

Camera changes

I want the camera to follow around the player keeping him in the center of the screen at all times.  I don't want it to move outside the borders of the map however.  HaxeFlixel actually does all this heavy lifting for you and I can add this with a couple lines of code.  I added this to my LevelState.create() function at the bottom.//Camera settings.

FlxG.camera.follow(_p);
FlxG.camera.setBounds(0, 0, map.width, map.height, true);

The first line tells the camera to keep the player in the center of the screen.  The second however, limits the camera to only displaying to the edges of the map.  Done with the camera code!  It's nice to have something be straightforward.

Dropping through platforms

I also wanted the player to be able to drop through the one way platforms by pessing the down button.  HaxeFlixel doesn't provide this possibility out of the box but it is easy enough to code.  There are some pitfalls that I had to deal with though so the drop through platforms work in all cases.  As a result, this code is hacked together and needs to be revisited at a future date.  But it works for now, so I'm moving on.  I added this to the Player.hx file in the update function.

//If we are on the ground and down is pressed, we need to check if we are on a droppable platform.
if (isTouching(FlxObject.DOWN) && InputHelper.isButtonPressed("down")) {
	var dropl:Bool = false;
	var dropr:Bool = false;
	//Check the tile below the left side.
	var left = l.map.getTile(Reg.mapPoint(x), Reg.mapPoint(y + height));
	var right = l.map.getTile(Reg.mapPoint(x + width), Reg.mapPoint(y + height));
	if (left == 0 ||left == 2)
	dropl = true;
	//Check the tile below the left side.
	if (right == 0||right == 2)
	dropr = true;
	if (dropl && dropr)
	y+= 5;
}

I originally thought this would be simple.  If the player is on the ground and pressing down, look at the tile underneath him.  If it is a 2 (indicating a one way platform) just move the y value down a couple pixels so the collision code doesn't trip and then let gravity take over.  This works for a majority of cases, but there are a couple that it doesn't.  The problem is that the player might be standing on top of two different tiles at the same time.  So instead of checking for just one tile I have to check for the tile oon the left and the tile on the right.  If they are both empty (0) or a one way platform (2) then move the player's Y position down 5 pixels.  If either is solid (1) they should not drop. 

Death can actually kill now

I also needed Death to actually kill things when he hits them.  I've had enough of him floating around harmlessly.  So let's add it.

FlxG.overlap(_death, _entities, deathHits);

This is added to my Levelstate.update() function.  It is simple enough.  When Death hits something in the Entity group (which is made up of the player and all the enemies), we want to run the deathHits function that I will create in a minute.  

/**
 * Function that fires when Death hits a target
 * @param	d Death
 * @param	target The target he hit.
 */
public function deathHits(d:Death, target:Entity) {
	target.kill();
	if (Std.is(target, Player)) {
        	_p.health = 3;
		setDebugText();
	}
	//If we killed an enemy, respawn it randomly off the top of the screen.
	if (Std.is(target, Enemy))
		target.reset(FlxRandom.floatRanged(100, map.width - Reg.TIESIZE), 40);
}

This function is just for testing purposes and when the game is released will be completely replaced, so I don't want to spend a lot of time working on it.  I just want to get the basics in place so I get a feel for how the game plays. 

The first thing I do when Death hits an entity is to kill it.  Calling the kill() function on a FlxSprite will set the alive flag to false and remove it from the collision lists, which is a good thing. 

The next thing to do is check to see if Death hit the player.  If we did the player will still be killed, but I'm going to reset the player's health value to 3, which is the maximum.  Health is another property built into FlxSprites which I'm going to use.  Right now the health code doesn't do anything because nothing reduces it, but we will get there in a minute so I might as well do it now.  Then I reset the debugText to display the new health value.. 

The final check is to see if I hit an enemy.  If I did, I want to reset it to a random location on the top of the screen.

Monsters can hurt

This update wasn't good for the player.  Death can kill him and now the monsters are going to start to hurt him when they touch him.  I guess it is nice that Death can also kill the monsters, but it's a poor tradeoff. 

FlxG.overlap(_p, _enemies, playerHitsMonster);
	

I added another collision detection to the LevelState.update() function.  This one looks for collisions between the player and enemies and runs the playerHitsMonsters() function when they happen.  I don't have that function yet, but that's the next step.

public function playerHitsMonster(p:Player, e:Enemy) {
	p.hitByEnemy(e);
	setDebugText();
}

Not complicated.  I am going to let the player object handle the collision so no real code here.  Let's look at the player.hitByEnemy() function.

public function hitByEnemy(e:Enemy) {
	if(invincibleCounter <0) {
		health--;
		if(health > 0) {
			FlxSpriteUtil.flicker(this, invincibleTime);
			invincibleCounter = invincibleTime;
		} else {
			health = 3;
			reset(FlxRandom.floatRanged(100, l.map.width - Reg.TIESIZE), 40);
		}
	}
}

I need The basic plan is that whenever the player hits an enemy we lower the player's health by one.  When it hits zero the player dies.  Seems simple enough, so at first I was just saying health-- and calling it a day.  However, when I tested the code I would hit a monster and instantly die.  It took a minute to figure out what was going on. 

The LevelState.update() function runs ideally at 60 frames a second and detects collisions each time.  If the player runs into an enemy it is going to detect that collision once an update.  Three collisions will be detected in 1/20th of a second, which is close enough to instantly that it doesn't matter.  I need some sort of invincibility counter though to avoid that problem. 

I'm going to handle it in my normal way. 

//Player variables
var invincibleTime:Float = 1;
var invincibleCounter:Float = 0;

//In the update function
invincibleCounter -= FlxG.elapsed;

I make two variables.  The invincibleTime is how long in seconds we should wait between detecting collisions between the player and enemies.  I set it to 1 second because it was a round number.  The invincibleCounter I decrease by the elapsed time each time the update function fires.  That way if the invincibleCounter is below 0 I should detect hits and if it is greater than 0 I should not.  Back to the hitByEnemy function.

if(invincibleCounter <0) {
	health--;
	if(health > 0) {
		FlxSpriteUtil.flicker(this, invincibleTime);
		invincibleCounter = invincibleTime;
	} else {
		health = 3;
		reset(FlxRandom.floatRanged(100, l.map.width - Reg.TIESIZE), 40);
	}
}

Now I only want to reduce my health if I'm not invincible.  Great!FlxSpriteUtil.flicker(this, invincibleTime);FlxSpriteUtil.flicker(this, invincibleTime);

I wanted some sort of visible feedback for the player to tell that they are temporarily safe from enemies (but not Death.  You're never safe from him).  So I decided to use the FlxSpriteUtil.flicker().  It takes two parameters.  The first is the sprite to flicker and the second is how long it should flicker for.  Next, I need to set my invincibleCounter so I don't immediately register another hit.  

The else statement handles what happens if the player runs out of health.  For now I'm just resetting the player to the top of the screen randomly like the enemies that die, but this function will eventually notify the LevelState that the player has died and let the LevelState deal with the fallout.

You might be wondering why I handled the hit here in the player function instead of the LevelState and why I passed the enemy object as a parameter if I wasn't going to use it.  This is just future planning.  I might later want different enemies to do different amounts of damage or something, so I just put those pieces in place now. 

Done!

With that, all the pieces are in place for the game.  Now the player can run around, kill, and be killed.  I'm starting to see for the first time how the final product will play and I'm actually rather happy with it. 

 

 

 

Add comment


Security code
Refresh