Tile based game tutorial using Haxe and OpenFL: Part 4

posted on Sep 22, 2014 in Haxe, OpenFL, Game design
Tile based animation Haxe OpenFL

So far we have a controllable character with basic walking animation implementation.

By basic I mean that we can animate through a preset sequence of frames, and stop the animation when needed. Our animation system right now plays one animation frame per one rendering frame (i.e. draw call). No matter how fast you gotta go, this is just too fast.

This can be fixed by adding 1 more feature to the animation logic - delays between animation frames. The delays can be specified in two units - actual time (seconds) or rendering frames (i.e. draw calls). Today we'll implement the latter.

Before we move on, however, we need to address the code re-usability issue.

Until now our animation logic was small enough to be considered the inner logic of the PlayerCharacter object. It's obvious now that we're going to extend the animation system.

In a tile-based game, if our character is animated, how likely is it that other future entities will have to be animated in a similar way?

Very likely.

How annoying would it be to rewrite the same animation logic every time for each entity, even though they only differ slightly?

Pretty annoying. Not to mention all the new opportunities for errors.

This is why I'll be moving the whole animation code into a new class AnimationStepper, which can later be accessed by a common interface and be used in different entities for different purposes.

The AnimationStepper class will be fed an array of frame values, as well as the delay between animated frames in rendering frames (draw calls). The instance will then handle all the stepping and stopping logic, circulating through the provided frame values.

Whenever we need to find out what animation frame should be displayed right now, we'll call this object's getFrame() method.

Here's the full AnimationStepper.hx code, which I will explain in a bit:

package ;

/**
 * Handling of frame animation.
 * @author Kirill Poletaev
 */
class AnimationStepper
{
	public var frames:Array<Int>;
	public var frameDelay:Int;
	
	private var step:Int;
	private var frameCounter:Int;

	public function new(frames:Array<Int>, frameDelay:Int) 
	{
		this.frames = frames;
		this.frameDelay = frameDelay;
		step = 0;
		frameCounter = 0;
	}
	
	public function reset():Void {
		step = 0;
		frameCounter = 0;
	}
	
	public function animate():Void {
		if (frameCounter >= 0) {
			frameCounter = -frameDelay;
			step++;
			if (step == frames.length) {
				step = 0;
			}
		}else {
			frameCounter++;
		}
	}
	
	public function getFrame():Int {
		return frames[step];
	}
	
}

As you can see, we pass 2 values to the constructor, as planned - the array of frame values and the delay between frames. The rest code should be familiar if you followed my previous tutorial, because this logic was taken from the PlayerCharacter class. What's new is the frameCounter variable, along with its associated wrapper code in animate().

Whenever the animate() method is called, we increase the frameCounter value by 1. As soon as the counter hits 0 (the starting value is a negative number), we increase the step value by 1 and move to the next animation frame from the array.

This way, if our delay is set to 5 frames, for example, then the animate() method will only increase the "step" value every 5 rendering frames (i.e. every 5 draw calls).

If we go back to our PlayerCharacter class, we can simplify the code a lot by removing all the existing animation logic and replacing it with the usage of the new AnimationStepper class.

Instead of initializing all the animation related variables, simply create an instance of the new class:

walkAnimStepper = new AnimationStepper([0, 1, 0, 2], 5);

The resetAnim() and animate() methods become much simpler too:

public function resetAnim():Void {
	walkAnimStepper.reset();
}

public function animate():Void {
	walkAnimStepper.animate();
}

Finally, the draw() method now uses the getFrame() method of the AnimationStepper to find out which frame to pass to the draw call:

override public function draw():Array<Float> {
	var tile:Int = direction[walkAnimStepper.getFrame()];
	return [position.x, position.y, tile];
}

The full PlayerCharacter.hx code ends up looking like this:

package ;
import openfl.display.Tilesheet;
import openfl.geom.Point;
import openfl.geom.Rectangle;

/**
 * Player character's entity.
 * @author Kirill Poletaev
 */
enum Direction {
	Left;
	Right;
	Up;
	Down;
}
 
class PlayerCharacter extends TileEntity
{
	private var faceDown:Array<Int>;
	private var faceUp:Array<Int>;
	private var faceRight:Array<Int>;
	private var faceLeft:Array<Int>;
	
	public var position:Point;
	private var direction:Array<Int>;
	
	public var movementSpeed:Int;
	private var walkAnimStepper:AnimationStepper;

	public function new(tilesheet:Tilesheet) 
	{
		faceDown = new Array<Int>();
		faceUp = new Array<Int>();
		faceRight = new Array<Int>();
		faceLeft = new Array<Int>();
		
		faceDown.push(tilesheet.addTileRect(new Rectangle(0, 32, 32, 32)));
		faceDown.push(tilesheet.addTileRect(new Rectangle(32, 32, 32, 32)));
		faceDown.push(tilesheet.addTileRect(new Rectangle(64, 32, 32, 32)));
		
		faceUp.push(tilesheet.addTileRect(new Rectangle(0, 64, 32, 32)));
		faceUp.push(tilesheet.addTileRect(new Rectangle(32, 64, 32, 32)));
		faceUp.push(tilesheet.addTileRect(new Rectangle(64, 64, 32, 32)));
		
		faceLeft.push(tilesheet.addTileRect(new Rectangle(0, 96, 32, 32)));
		faceLeft.push(tilesheet.addTileRect(new Rectangle(32, 96, 32, 32)));
		faceLeft.push(tilesheet.addTileRect(new Rectangle(64, 96, 32, 32)));
		
		faceRight.push(tilesheet.addTileRect(new Rectangle(0, 128, 32, 32)));
		faceRight.push(tilesheet.addTileRect(new Rectangle(32, 128, 32, 32)));
		faceRight.push(tilesheet.addTileRect(new Rectangle(64, 128, 32, 32)));
		
		position = new Point(128, 128);
		direction = faceDown;
		movementSpeed = 3;
		
		walkAnimStepper = new AnimationStepper([0, 1, 0, 2], 5);
	}
	
	override public function draw():Array<Float> {
		var tile:Int = direction[walkAnimStepper.getFrame()];
		return [position.x, position.y, tile];
	}
	
	public function face(dir:Direction):Void {
		switch(dir) {
			case Up: direction = faceUp;
			case Down: direction = faceDown;
			case Right: direction = faceRight;
			case Left: direction = faceLeft;
		}
	}
	
	public function resetAnim():Void {
		walkAnimStepper.reset();
	}
	
	public function animate():Void {
		walkAnimStepper.animate();
	}
	
}

The best part, of course, is that we can use the AnimationStepper class in any entity we want. Simple things are nice.

In the next part we'll work on tile collision.

9832