Now that I've got Death moving around the stage and chasing the player I've got a couple more mechanics to work on before the game becomes technically playable.  I know I've got to add some enemies to the game, and I know that I want the player to be able to taunt Death to make him charge.  I also realized that the last time I was testing the game my laptop was doing updates in the background so the framerate was lower than normal.  As a result, when I played the game later at the normal rate it moved much faster than I thought it would.  So I need to balance the speed of the player and Death.

I also need to have some idea how the game plays at different speeds.  My goal is to have Death speed up over time to increase the difficulty, but I'm going to need some playtesting to discover what the upper and lower limits should be.  So I also want a way to change Death's speed in real time so I can test.

Charge of Death

Making Death charge is actually pretty straightforward.  I need a new variable, a new function, and some changes to Death's update function.  The basic plan is when the player taunts, Death will move straight in the direction of the player at a speed that is greater than normal for a set amount of time. 

//Death dash variables
var DASHLEN:Float = .75;
var dashRatio:Float = 2;
var dashTime:Float = 0;


/**
 * Makes Death dash at a target.
 */
public function dashAtTarget() {
	maxVelocity.set(maxSpeed * dashRatio, maxSpeed * dashRatio);
	dashTime = DASHLEN;
	acceleration.set();
	velocity = FlxAngle.rotatePoint(maxSpeed * dashRatio, 0, 0, 0, FlxAngle.angleBetween(this, target, true));
}

I declare three variables at the top of Death.hx.  DASHLEN is how long I want Death to charge for before reverting back to his standard accelerate-at-my-target routine.  DashRatio is how much faster Death should speed up when charging.  Right now it is set to 2, so Death's charge speed is twice his maximum movement speed.  DashTime is a variable that will start at DASHLEN and then count down.  When I hit 0 or below I know that Death is done charging. 

The function dashAtTarget only has a couple lines and sets up for the work to be done in the update function.  The first thing I need to do is change the maxVelocity.  I previously capped this so Death doesn't accelerate to huge speeds, but I need to increase this limit so Death can travel at charge speeds.  Simply setting the limits to maxSpeed*dashRatio takes care of that. 

Next I set my charge countdown timer dashTime to DASHLEN.  This countdown timer will be changed in the update() function.

I also set Death's acceleration to 0,0, because the charge is going to change Death's velocity.  This means that no matter what direction Death was going in, when taunted he will charge in a straight line.  If I left the acceleration on he would charge in an arc, which isn't a problem, but not what I'm going for.

Finally, I am going to change Death's velocity directly.  I'm doing the same thing that I do with Death's acceleration in the update function that I talked about last time.  Just create a vector of Speed,0 and rotate it around 0,0 the number of degrees to his target.

Next up is the update function:

//If we have a target...
if (target != null) {
	//If the dash time is greater than 0 we are dashing.
	if (dashTime > 0) {
		dashTime -= FlxG.elapsed;
		if (dashTime <= 0) {
			maxVelocity.set(maxSpeed, maxSpeed);
		}
	} else{
	acceleration = FlxAngle.rotatePoint(maxSpeed * currentSpeedRatio, 0, 0, 0, FlxAngle.angleBetween(this, target, true));
	}
}
		

This code replaces everything under the super.update() call. 

The update function changes are minor.  I already set the velocity and maxVelocity in the dashAtTarget function, so I pretty much just let Death go.  The if(dashTime >0) statement just checks to see if there is time on the countdown timer.  If there is, reduce the counter.  If we just reduced the counter to 0 or below it mans that Death will resume his normal course the next update.  So I need to reset his maxVelocity back to what it was before so Death isn't allowed to go at his dashing speed during a normal move.  The dash is done!

Now to get Death to charge.  I need a new button for InputHelper which I will set to the spacebar for now (remember, this goes in Main.hx)

InputHelper.addButton("taunt");
InputHelper.assignKeyToButton("SPACE", "taunt");

and change my LevelState.update() function to run Death's dashAtTarget() function when we press it.

if (InputHelper.isButtonJustPressed("taunt")) 
_death.dashAtTarget();

Using maxVelocity on a floating entity (warning: theory and musings)

A quick aside, I was thinking about my implementation now which limits the maxVelocity on the X and Y axis and what that means for the game.  The other way that I could do this is to limit the magnitude of the vector of the velocity instead which might actually lead to a better experience.  By limiting the velocity on the X and Y axis I actually allow Death to move much faster diagonally than I do vertically or horizontally.  In other words, Death's potential velocity is a square, so the velocity at the corners is higher than at the sides.  If I limit the velocity vector instead then the velocity limit is the same no matter what direction Death is traveling.

Blue is the velocity.  Notice on the left that the diagonals are much longer than the horizontal and vertical.

The difference is subtle, but it is there.  If I limit Death's velocity on the X and Y to 300, then when Death is moving on the diagonal he can actually reach speeds of 425 (we can calculate this using Pythagorean's Thorium).  If I limit on the magnitude the max speed is the same no matter what direction Death is traveling.

How would this affect the game if I changed it?  Well, if the player is running horizontally and Death is approaching from above or below, I'm thinking that Death would fall farther behind the player player because Death's vertical movement would come at a cost of some of his horizontal velocity.  He might bob up and down less because as he pulled in behind the player there would be less vertical velocity to compensate for. 

On the other hand, Death bobbing up and down leads to a floating look that I like, so I might end up keeping it.  Maybe I'll come back and change this later.

Tweaking settings

I ran into a problem when I coded my player movement earlier.  My laptop that I was working on was applying updates so it was running slowly but I didn't notice.  The result was that when I ran the game I was moving at a lower frames per second than I would be normally, so everything was moving slower than it should.  So I balanced the movement to this extra slow rate and when I played it later after the updates finished everything ran really fast.  So I've balanced the movement variables of the Player again. 

//Movement variables
var GRAV:Int = 2000;
var JMPLEN:Float = .14;
var JMPSTR:Int = 800;
var RUNSPD:Int = 400;

I also need to have an idea of how fast Death should move to give a good game experience.  I need to find values that are easy (low speeds) and difficult (high speeds) and find if there is a sweet spot somewhere in the middle that will provide a good game.  To do this I'm going to write a way for the player to speed up and slow down Death during the game so I can test it. 

First I add two new virtual buttons to InputHelper and assign them to Q and E.

InputHelper.addButton("speedup");
InputHelper.addButton("speeddown");
InputHelper.assignKeyToButton("E", "speedup");
InputHelper.assignKeyToButton("Q", "speeddown");

When I change Death's speed I really am just changing his maxVelocity setting, so I can't just change my Death.maxSpeed variable.  So to make sure that I don't forget to change the maxVelocity when I change maxSpeed I'm going to make a function in Death.hx that will do it for me.  I also want to just supply the change in speed rather than just the new speed because I don't want to have to look up the current speed when I want to change it to something.  So in this case I want every press of the key to move Death's speed up or down 50. 

/**
 * Sets Death's speed.
 * @param	speed Change in Death's speed.
 */
public function setDeathSpeed(deltaSpeed:Int) {
	maxSpeed += deltaSpeed;
	maxVelocity.set(maxSpeed, maxSpeed);
}

I also want something to display the current speed on the screen.  I could use trace() but that would draw it on the screen in Flash and would quickly cover the entire screen with writing, so instead I'm going to display it in game instead.  To do that I'm going to use an FlxText object that I'll make in the LevelState class.

//Debug variables
var debugText:FlxText;

//Inside create function

//Debug stuff
debugText = new FlxText(0,0,0,"",15);
//Set the text color to red so it is easier to see
debugText.color = FlxColor.RED;

//After adding all the other variables to the stage
add(debugText);

Now I just need to add some functionality to the LevelState.update() function.  This goes after the InputHelper.updateKeys() call.

if (InputHelper.isButtonJustPressed("speedup")) {
_death.setDeathSpeed(50);
debugText.text = "Death speed: " + _death.maxSpeed;
}
if (InputHelper.isButtonJustPressed("speeddown")) {
_death.setDeathSpeed( -50);
debugText.text = "Death speed: " + _death.maxSpeed;
}

This code just calls the setDeathSpeed() function with eitehr 50 or -50 (depending if we want the speed to go up or down) and then changes the debugText.text to display a message about Death's new speed.  Simple.

Adding Enemies

This is all well and good, but we need some enemies to threaten our player and for Death to kill. I previously created an Entity class that I want all my monsters to be a part of, so now I am going to create a new Enemy class that has a baseclass of Entity.  The Enemy class will have any code in it that I want to effect every monster.  Then, when I want a specific monster with specific characteristics and code I will extend my Enemy class with custom code.  So here I created a new Enemy.hx class.

class Enemy extends Entity
{

	//This is a pointer to the LevelState for AI logic.
	var l:LevelState;
	
	public function new(X:Float=0, Y:Float=0) 
	{
		super(X, Y);
		acceleration.y = 800;
                create();
	}
	
 	/**
	 * This updates the enemy's logic.  It is called by the update fcuntion automatically and should be overridden on each monster.
	 */
	public function step() {
		
	}
	
	/**
	 * Sets the level state.  This is needed for the enemy logic.
	 * @param	l
	 */
	public function setLevelState(l:LevelState) {
		this.l = l;
	}
	
	override public function update():Void 
	{
		step();
		super.update();
	}
	
}

There is not a whole lot going on in this class just yet, but it is giving me a framework that I can build on.  I want to make future Enemy creation as easy as possible and I don't want to rewrite any code if practical, so this class will let me do that.  Here are a couple things to note:

public function new(X:Float=0, Y:Float=0) 
{
        super(X, Y);
	acceleration.y = 800;
}

My constructor only does one thing right now.  It sets an acceleration.y value so every monster I create is affected by gravity.  This means that if I don't give a monster gravity (or if I forget to, which is much more likely) it automatically has some.  

/**
 * Sets the level state.  This is needed for the enemy logic.
 * @param	l
 */
public function setLevelState(l:LevelState) {
	this.l = l;
}

My Enemy class is going to have some artificial intelligence, so it is going to have to know about the level that it is in.  The simplest way for me to do that is to pass it the LevelState object for it to reference.  Now it knows everything publicly available to the LevelState, which is actually not a lot because I didn't declare anything public so it defaulted to private.  I'll have to go back and change that later.

override public function update():Void 
{
	step();
	super.update();
}

/**
* This updates the enemy's logic.  It is called by the update function automatically and should be overridden on each monster.
*/
public function step() {
       
}

This is the update function.  It just calls step(), which is an empty function.  I am going to override this step function in each monster and it is going to contain all the game logic that determines the monster's behavior.

Now that my enemy is ready to go, I need a way to add it to the LevelState.  Every time I add an enemy to the level I've actually got to do quite a bit.  I've got to set its location on the map somewhere.  I've got to pass the LevelState object to the enemy so it can use its AI.  I've also got to add it to my _enemies and _entities FlxTypedGroups (remember them?) so they collide with the map properly.  I don't want to type all that every time I want to add an enemy (and I don't want to forget to do it!) so I'll make a function that sets up an enemy that I pass it.

public function addEnemy(e:Enemy, x:Float, y:Float) {
	e.x = x;
	e.y = y;
	e.setLevelState(this);
	_enemies.add(e);
        _entities.add(e);
}

There we go.  This function isn't doing anything we haven't done already before.  So when I want to use it, I'll say something like addEnemy(new Enemy(), 100,150); and the function will add the new Enemy to the board at the location 100,150 and put it in all the correct groups.  Now we are ready to actually make an enemy.

Make a slime

I'm going to make a simple slime enemy that is going to walk along a platform until it hits the edge or a wall, at which point it will turn around and go in the opposite direction.  A basic enemy type in every platformer ever.  So I created a new folder under source called enemies (make it lower case or Haxe will complain later) and created a new Class in it called Slime.hx.  Slime has a BaseClass of Enemy (which has a baseClass of Entity, which has a base class if FlxSprite, which has a baseclass of FlxObject...).  I set up my Enemy class so to make an enemy I just need to initialize any variables in the constructor and then override the step() function, so lets start with the variables and constructor.

var speed:Int = 200;
	
public function new(X:Float=0, Y:Float=0) 
{
	super(X, Y);
	velocity.x = speed;
	makeGraphic(40, 40, FlxColor.GREEN);

}

I've decided that I am going to change the slime's velocity directly instead of messing with acceleration to get it to move.  It means that the slime is going to move at full speed all the time and change directions suddenly which I don't think looks as nice as a smooth velocity change, but it also means I don't have to worry about the slime approaching the edge of a platform and not having enough time to accelerate in the opposite direction before falling off a cliff, so it is a tradeoff.  I created one variable called speed that will determine the slime's maximum (and only) velocity.  I set the velocity.x to the speed variable so the slime starts off moving to the right.  Then I make a temporary green rectangle to represent the slime.  Nothing new or special here.

The step() function is where the Slime's simple AI comes into play. 

var right:Bool = true;

override public function step() 
{
	if(isTouching(FlxObject.FLOOR)) {
		//If we are on the floor and moving right, look to see if we are on the edge of a cliff.  If so, turn around.
		if (right) {
			//Check to see if we are on the edge of a platform.
			if (isTouching(FlxObject.RIGHT) || l.map.getTile( Math.floor((x + width) / Reg.TIESIZE ), Math.floor((y + height) / Reg.TIESIZE )) == 0 ) {
			//If so, turn around.
			velocity.x = -speed;
			right = false;
			}
		} else if (!right) {
			if (isTouching(FlxObject.LEFT) || l.map.getTile( Math.floor(x / Reg.TIESIZE ), Math.floor((y + height) / Reg.TIESIZE )) == 0) {
			//If so, turn around.
			velocity.x = speed;
			right = true;
			}
		}
	}
}

This is simple code that makes an enemy walk back and forth along a platform.  I need a new variable to keep track of the direction that the slime is moving and what direction I should check for cliffs or walls.  I originally was just using the velocity.x varable to detect what direction I was traveling in, but that causes problems later on when the enemy walks into a wall and its velocity drops to 0.  

if(isTouching(FlxObject.FLOOR)) {

This line just assures that we are only making checks when the enemy is on the ground.  Otherwise I'll get this strange behavior where the enemy virbates back and forth because of the rules below it.

if (right) {
			//Check to see if we are on the edge of a platform.
			if (isTouching(FlxObject.RIGHT) || l.map.getTile( Math.floor((x + width) / Reg.TIESIZE ), Math.floor((y + height) / Reg.TIESIZE )) == 0 ) {
			//If so, turn around.
			velocity.x = -speed;
			right = false;
			}
}

This code turns the slime around if it runs into a wall or approaches a cliff.  The isTouching(FlxObject.RIGHT) handles the wall check, but the real challenge comes in detecting what is ahead of us.  The difficulty is that our map stores data in tiles, while our entities are stored in pixels.  I passed the LevelState into the slime with the addEnemy() function so I can access it with the variable name l.  So what I want my slime to do is look at the tile below its right side to see if it is empty.  If it is, we want to change directions.

l.map.getTile( Math.floor((x + width) / Reg.TILESIZE ), Math.floor((y + height) / Reg.TILESIZE )) == 0

l.map.getTile() is the function that will get me the value of a tile on the map (0 for passable, 1 for solid).  The trick is that it is looking for integers for tile numbers and in HaxeFlixel all the FlxSprites store their location in world units, or their position in the world.  To get their location in map tiles I have to take the location in world units and divide it by the size of each tile.  Each of my tiles are 40 x 40.  I don't want to hard code this number all over the place, so I'm going to use the Reg object that is automatically set up for me with every HaxeFlixel project.  The Reg object is just a collection off static variables, so I can open the Reg.hx file and add the following line:

public static var TILESIZE = 40;

The static designation means that this variable is available without creating an object out of the Reg class, so I can get at it by using Reg.TILESIZE instead of something like var r = new Reg(); r.TILESIZE. 

So I can get the slime's location on the map's X axis by dividing the x value by Reg.TILESIZE.  The problem is that to access the map.getTile() function I need Integers and x/Reg.TILESIZE returns a Float.  So I need to call a handle Math function.  Math has some static functions just like Reg, and the Math.floor() function takes a Float and converts it to an Int.  So this gives me my X location in tile units:

 Math.floor((x + width) / Reg.TILESIZE )

I can do the same thing for the Y axis. The problem is that this is the map location for the upper left corner of my slime and I really need to check the lower right.  So I need to add the width to X and the height to Y before running them through the Math.floor() function to get the position of the lower right corner.  Then I check to see if that space is empty.  If it is, that means the right hand side is over an edge and I need to turn around.

//If so, turn around.
velocity.x = -speed;
right = false;

Nothing special here.  Just changing some variables.

Now lets add a couple new slimes to our game and see then run around.   Right now there is no interaction between the slime, player, or Death, but that will be for next time!

//Add enemies to the LevelState.hx file.  I added it after Death, but it doesn't matter as long as it is in the create function.

addEnemy(new Slime(), 400,600);
addEnemy(new Slime(), 100,200);
addEnemy(new Slime(), 500,100);

WASD or Arrows to move.  Q and E to speed up and slow down Death.  Space to taunt.

Code available here.

Add comment


Security code
Refresh