Pushbutton Engine – Animating Bitmap Sprites pt.2

In the previous article you learned how to animate the ‘Hero’ entity using sprite sheets and the AnimationController() class. In this article we’re going to build upon that and add in some animations based on user input. As it currently stands we currently have an ‘idle’ animation that just swoops the UFO sprite used for the player controlled ‘Hero’ object back and forth (you can see this below).

[kml_flashembed publishmethod=”dynamic” fversion=”10.0.0″ replaceId=”simpleUFODemo” movie=”http://blog.flashgen.com/swfs/simpleUfoDemo.swf” width=”100″ height=”100″ targetclass=”flashmovie”]

Get Adobe Flash player

[/kml_flashembed]

What we are going to do next is add in extended animations that are triggered when the user moves their craft either left or right. To do this I’d suggest creating separate sprite sheets (just because it is easier to debug and when it comes to profiling / optimizing memory usage in your game, it will be easier to spot potential graphical bottlenecks. The actual process of adding in additional animations is exactly the same as the one you created for the ‘idle’ animation in the previous article. In fact to push this forward I’m not going to dig deep in to the structure of this part of the code – you can read the first part if you’re still not 100% clear with the code ‘building blocks’. Just as a quick refresher here is the code as it currently stands within the initializeHero() method.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

private function initializeHero():void
{
	var _hero	:IEntity = allocateEntity();
 
	// Add dimensions to our hero
	var _spatial	:SimpleSpatialComponent = new SimpleSpatialComponent();
	_spatial.size = new Point(50, 50);
	_spatial.position = new Point(0, 0);
 
	_hero.addComponent(_spatial, "Spatial");
 
	// Render our hero
	var _renderer		:SpriteSheetRenderer = new SpriteSheetRenderer();
	_renderer.positionProperty = new PropertyReference("@Spatial.position");
	_renderer.spriteSheet = _idleSpriteSheet;
	_renderer.spriteIndex = 0;
	_renderer.layerIndex = 10;
	_renderer.scene = PBE.scene;
 
	_hero.addComponent(_renderer, "Renderer");
 
	// Add animation controller and animations
	var _animationController	:AnimationController = new AnimationController();
	_animationController.spriteSheetReference = new PropertyReference("@Renderer.spriteSheet"); 
	_animationController.currentFrameReference = new PropertyReference("@Renderer.spriteIndex");
	_animationController.defaultAnimation = "idle";
 
	// Create Idle Animation
	var _idleSpriteSheet	:SpriteSheetComponent= new SpriteSheetComponent();
	_idleSpriteSheet.imageFilename = "../../../../assets/spritesheets/ufoIdle.png";
 
	var _idleDivider	:CellCountDivider = new CellCountDivider();
	_idleDivider.xCount = 5;
	_idleDivider.yCount = 5;
	_idleSpriteSheet.divider = _idleDivider;
 
	//	Create Idle Animation Controller
	var _idleAnimation	:AnimationControllerInfo = new AnimationControllerInfo();
	_idleAnimation.frameRate = 5;
	_idleAnimation.loop = true;
	_idleAnimation.spriteSheet = _idleSpriteSheet;
	_animationController.animations["idle"] = _idleAnimation;
 
	// Add Animation Controller to Hero
	_hero.addComponent(_animationController, "Animator");
	_hero.initialize();
}

Adding Additional Animations
To add in support for additional animations you just need to specify a SpriteSheetComponent(), CellCountDivider() and AnimationControllerInfo() instance for each one. As I mentioned earlier, these are structurally the same the only difference being the actual name assigned to each one when passed in to the AnimationController().

As before, you declare an instance of SpriteSheetComponent() and pass in the relevant sprite sheet, then you need to assign a CellCountDivider() instance to it so you can define the amount of rows and columns it contains. The last step is to instantiate an instance of AnimationControllerInfo(), set the frame rate you desire, indicate if it needs to loop (in this case neither do) and pass in a reference to the actual SpriteSheetComponent() instance. The final part is to add the AnimationControllerInfo() object to the animations array of the AnimationController(), (note this array is String indexed and not zero indexed), and you’re there.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

// Create Move Left Animation
var _moveLeftSpriteSheet	:SpriteSheetComponent = new SpriteSheetComponent();
_moveLeftSpriteSheet.imageFilename = "../../../../assets/spritesheets/ufoMoveLeft.png";
 
var _moveLeftDivider	:CellCountDivider = new CellCountDivider();
_moveLeftDivider.xCount = 6;
_moveLeftSpriteSheet.divider = _moveLeftDivider;
 
var _moveLeftAnimation	:AnimationControllerInfo = new AnimationControllerInfo();
_moveLeftAnimation.frameRate = 6;
_moveLeftAnimation.loop = false;
_moveLeftAnimation.spriteSheet = _moveLeftSpriteSheet;
_animationController.animations["moveLeft"] = _moveLeftAnimation;
 
// Create Move Right Animation
var _moveRightSpriteSheet	:SpriteSheetComponent = new SpriteSheetComponent();
_moveRightSpriteSheet.imageFilename = "../../../../assets/spritesheets/ufoMoveRight.png";
 
var _moveRightDivider	:CellCountDivider = new CellCountDivider();
_moveRightDivider.xCount = 6;
_moveRightSpriteSheet.divider = _moveRightDivider;
 
var _moveRightAnimation	:AnimationControllerInfo = new AnimationControllerInfo();
_moveRightAnimation.frameRate = 6;
_moveRightAnimation.loop = false;
_moveRightAnimation.spriteSheet = _moveRightSpriteSheet;
_animationController.animations["moveRight"] = _moveRightAnimation;

Switching Animations
While you have added the relevant animations, you still need to provide a way of changing the animation based on your criteria (in this case the desired arrow key being pressed). To do this you need to add a couple of things to the end of the AnimationController() instance. These are the properties changeAnimationEvent and currentAnimationReference. As the names imply, the changeAnimationEvent lets you declare a custom event type that the animation controller should listen for so it knows when to change the animations; and currentAnimationReference lets you define what the current animation is that is in operation. In the case of this example I have declared them with the following values

1
2

_animationController.changeAnimationEvent = "ufoUpdatedEvent";
_animationController.currentAnimationReference = new PropertyReference("@Controller.activeAnimation");

Don’t worry about the value assigned to currentAnimationReference, it’s a callback to the keyboard controller class that you have yet to create (we’ll be doing that next). All you need to make sure is that your AnimationController() code now looks something along the lines of this:

1
2
3
4
5
6
var _animationController	:AnimationController = new AnimationController();
_animationController.spriteSheetReference = new PropertyReference("@Renderer.spriteSheet"); 
_animationController.currentFrameReference = new PropertyReference("@Renderer.spriteIndex");
_animationController.defaultAnimation = "idle";
_animationController.changeAnimationEvent = "ufoUpdatedEvent";
_animationController.currentAnimationReference = new PropertyReference("@Controller.activeAnimation");  


Now your animation controller knows what to listen for to trigger an animation change. All you need to do now is trigger that change

Triggering Animation Changes
So far you’ve been working within the main game class in the initializeHero() method. Now we are going to create a new class to provide keyboard control support for our ‘Hero’ object. You may recall we covered this in one of the earlier articles. The process is pretty straight forward and to save covering old ground let’s look at how you tie the keyboard controller in to the AnimationController() of your component.

Here is the custom KeyboardController() class, note it inherits from TickComponent(), so the onTick() event will get triggered, well, every game tick. The class instance contains a few properties, two of which are public and two if which are private. The private ones are _isIdle and _animation. _isIdle is just a Boolean flag, in this case it is to indicate whether or not the associated component (in this case the ‘Hero’ component) is it an idle state or not. _animation just holds a reference to a String, which happens to be the current animation playing (by default it is the ‘idle’ animation but that is set on the AnimationController() within the component).

The public properties you probably already recognise. The positionProperty allows the position of the component to be updated and the animationEventName allows you to declare a custom event name for the controller to dispatch (you’ll see how that is set once we move back in to the main game class). One last property is activeAnimation. This is actually a getter/setter at the bottom of the KeyboardController() class and it maps back to the _animation variable.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107

package com.flashgen.gaming.controllers
{
	import com.pblabs.engine.PBE;
	import com.pblabs.engine.components.TickedComponent;
	import com.pblabs.engine.core.InputKey;
	import com.pblabs.engine.entity.PropertyReference;
 
	import flash.events.Event;
	import flash.geom.Point;
 
	public class KeyboardController extends TickedComponent
	{
		public var positionProperty		:PropertyReference;
		public var animationEventName	:String;
 
		private var _isIdle				:Boolean = true;
		private var _animation			:String;
 
 
		public function KeyboardController()
		{
			super();
		}
 
		override public function onTick(deltaTime:Number):void
		{
			super.onTick(deltaTime);
 
			var _position	:Point = owner.getProperty(positionProperty);
 
			if(PBE.isKeyDown(InputKey.UP))
			{	
				if(!_isIdle)
				{
					_isIdle = true;
					_animation = "idle";
					owner.eventDispatcher.dispatchEvent(new Event(animationEventName));
				}
				_position.y -= 6;
			}
 
			else if(PBE.isKeyDown(InputKey.DOWN))
			{	
				if(!_isIdle)
				{
					_isIdle = true;
					_animation = "idle";
					owner.eventDispatcher.dispatchEvent(new Event(animationEventName));
				}
				_position.y += 6;
			}
 
			else if(PBE.isKeyDown(InputKey.LEFT))
			{
				if(_isIdle)
				{
					_isIdle = false;
					_animation = "moveLeft";
					owner.eventDispatcher.dispatchEvent(new Event(animationEventName));				
				}	
				_position.x -= 6;
			}
 
			else if(PBE.isKeyDown(InputKey.RIGHT))
			{
				if(_isIdle)
				{
					_isIdle = false;
					_animation = "moveRight";				
					owner.eventDispatcher.dispatchEvent(new Event(animationEventName));				
				}
				_position.x += 6;
			}
 
			else if(!_isIdle)
			{
				_isIdle = true;
				_animation = "idle";
				owner.eventDispatcher.dispatchEvent(new Event(animationEventName));
			}
 
			if(_position.y > 285)
				_position.y = 285;
 
			if(_position.y < -285)
				_position.y = -285;
 
			if(_position.x > 385)
				_position.x = 385;
 
			if(_position.x < -385)
				_position.x = -385;	
 
			owner.setProperty(positionProperty, _position);			
		}
 
		public function get activeAnimation():String
		{
			return _animation;
		}
 
		public function set activeAnimation(value:String):void
		{
			_animation = value;
		}
	}
}

Inside the onTick() method you can see there is a conditional statement that checks to see which key is pressed. In all of them there is a check to see if the _isIdle flag is set or not. In the case of UP or DOWN there is no animation associated with either of them so there is only a check to see if the _isIdle flag is false (!_isIdle). If it isn’t false – i.e an animation other than the ‘idle’ animation is active – it sets the _animationName variable to ‘idle’ and dispatches an event to notify the AnimationController() instance of the change.

1
2
3
4
5
6
7
8
9
10

if(PBE.isKeyDown(InputKey.UP))
{	
	if(!_isIdle)
	{
		_isIdle = true;
		_animation = "idle";
		owner.eventDispatcher.dispatchEvent(new Event(animationEventName));
	}
	_position.y -= 6;
}

The Left and Right key press conditions are fairly similar, except they update the animation, you guessed it, only when the animation is set to ‘idle’. They then set the animationName to either moveLeft or moveRight, and like UP and DOWN, dispatch a notification event to show there has been a change in the animation selection.

1
2
3
4
5
6
7
8
9
10

else if(PBE.isKeyDown(InputKey.LEFT))
{
	if(_isIdle)
	{
		_isIdle = false;
		_animation = "moveLeft";
		owner.eventDispatcher.dispatchEvent(new Event(animationEventName));
	}	
	_position.x -= 6;
}

Finally there is a check in the condition to see if the user has stopped pressing any of the aforementioned keys (think of this as a catchall or automatic reset):

1
2
3
4
5
6

else if(!_isIdle)
{
	_isIdle = true;
	_animation = "idle";
	owner.eventDispatcher.dispatchEvent(new Event(animationEventName));
}

Tying It All Together
So that’s the keyboard controller class finished. Now all there is eft to do if for you to map this back in to the ‘Hero’ component. Switch back to the main game class and inside the initializeHero() method, just above the _hero.initialize() call add the following code:

1
2
3
4
5
6

// Add keyboard controls
var _input	:KeyboardController = new KeyboardController();
_input.positionProperty = new PropertyReference("@Spatial.position");
_input.animationEventName = "ufoUpdatedEvent";
 
_hero.addComponent(_input, "Controller");

As you can see this is where you set the animationEventName – beware of typos, otherwise the AnimationController() instance won’t respond to the event due to incorrect spelling. In fact I would suggest declaring a constant and reference that instead to avoid these types of mishaps. Now with the KeyboardController() class mapped in you are ready to test it out. Or are you?

Resources, Resources
Before you get too excited there is one last thing to do. You need to update the SpriteResources() class as it only contains the idle animation and the background at this point. If you were to test the game now you would see the idle animation but when you moved the UFO left or right it would disappear as the sprite sheet hasn’t been embedded. To fix this just open up the SpriteResources() class an below the entry for the UFO’s idle sprite sheet add the following.

1
2
3
4
5

[Embed(source="../../../../assets/spritesheets/ufoMoveLeft.png")]
public var fg_ufoMoveLeft		:Class;
 
[Embed(source="../../../../assets/spritesheets/ufoMoveRight.png")]
public var fg_ufoMoveRight	:Class;

Save the file and test your game and you should now have the three different animations working. I admit the transition between them isn’t that smooth (as you can see below), but at least you can see the change – I’ll leave the fine tuning up to you :p

[kml_flashembed publishmethod=”dynamic” fversion=”10.0.0″ replaceId=”pbe005-animatingsprites_complete” movie=”http://blog.flashgen.com/swfs/pbe005-animatingsprites_complete.swf” width=”450″ height=”400″ targetclass=”flashmovie”]

Get Adobe Flash player

[/kml_flashembed]

Summary
In this article you saw how you can easily add additional animations to any game component (in this case the player controlled ‘Hero’ object) by creating additional AnimationInfoController() instances and adding them to the AnimationController() instance assigned to that particular component. You also saw how to link up an animation by providing a custom event type and triggering it through user input, via the left and right arrow keys.

There are quite a few additional animation classes that you can leverage to fine tune various processes and while this is only a brief introduction to animating the visual elements of a component it should give you a good idea of how you approach it. I’ll be looking at some of the other animation classes in future articles, so stay tune.

Related FIles
PBE005_AnimatingBitmapSprites_pt2.zip

Leave a Reply

Your email address will not be published. Required fields are marked *