Components, State, and Hot-Swapping Animations
by Greg Baker · in Torque X 2D · 03/28/2007 (1:21 pm) · 6 replies
In the process of working on a game for the Dream, Build, Play challenge, I've developed the following approach to managing state within my game and changing animations based on the current state of an object.
Components
PlayerComponent (attached to a single, player controlled animated sprite)
EnemyComponent (generic 'bad guy' component, should be inherited for specific 'bad guy' instances)
Classes
CState - Contains a single ulong member variable that acts as bit mask. It exposes methods for setting and removing bits as well as testing whether a particular bit is set or not.
CPlayerState - Derived from CState. Defines bit masks for specific player states.
CEnemyState - Derived from CState. Defines bit masks for specific enemy states.
CAnimationManager - Contains a single collection (Dictionary) that holds a T2DAnimationData object keyed to a ulong bit mask.
Usage
Using the EnemyComponent as an example, the EnemyComponent would have both a CEnemyState object and a CAnimationManager object as member variables. When _InitComponent is fired, the component initializes the CEnemyState to its default state, initializes the CAnimationManager to store a T2DAnimationData object for each state an enemy can be in, and then registers itself to respond to ProcessTick events. During the processing of a tick event, the component checks its current state. If the current state has changed since the last tick, it grabs the T2DAnimationData object from the AnimationManager and updates the object it is assigned to with the new animation data.
I'm getting very close to the point where I can simply drag-n-drop animated sprites into the level editor, apply a few components, and require no additional programming. The only thing I would like to add is the ability to specify state masks and animation data from within the level editor. It's a minor inconvenience though, but I may look into it.
If anyone thinks they may find this code useable in their own projects, just let me know and I'll make it available to the community.
Components
PlayerComponent (attached to a single, player controlled animated sprite)
EnemyComponent (generic 'bad guy' component, should be inherited for specific 'bad guy' instances)
Classes
CState - Contains a single ulong member variable that acts as bit mask. It exposes methods for setting and removing bits as well as testing whether a particular bit is set or not.
CPlayerState - Derived from CState. Defines bit masks for specific player states.
CEnemyState - Derived from CState. Defines bit masks for specific enemy states.
CAnimationManager - Contains a single collection (Dictionary) that holds a T2DAnimationData object keyed to a ulong bit mask.
Usage
Using the EnemyComponent as an example, the EnemyComponent would have both a CEnemyState object and a CAnimationManager object as member variables. When _InitComponent is fired, the component initializes the CEnemyState to its default state, initializes the CAnimationManager to store a T2DAnimationData object for each state an enemy can be in, and then registers itself to respond to ProcessTick events. During the processing of a tick event, the component checks its current state. If the current state has changed since the last tick, it grabs the T2DAnimationData object from the AnimationManager and updates the object it is assigned to with the new animation data.
I'm getting very close to the point where I can simply drag-n-drop animated sprites into the level editor, apply a few components, and require no additional programming. The only thing I would like to add is the ability to specify state masks and animation data from within the level editor. It's a minor inconvenience though, but I may look into it.
If anyone thinks they may find this code useable in their own projects, just let me know and I'll make it available to the community.
#2
CState
CEnemyState
CAnimationMgr
Note: Error handling is minimal. In particular, if you attempt to call GetAnimation() and there is no animation data associated with the passed in state, you will get an exception. You may or may not want to handle that exception.
03/28/2007 (5:55 pm)
EnemyComponentusing System;
using System.Collections.Generic;
using System.Text;
using System.Timers;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using GarageGames.Torque.Core;
using GarageGames.Torque.Sim;
using GarageGames.Torque.T2D;
using GarageGames.Torque.MathUtil;
using GarageGames.Torque.XNA;
using GarageGames.Torque.Util;
using GarageGames.Torque.Platform;
namespace StarterGame
{
class EnemyComponent : TorqueComponent, ITickObject
{
#region Private Methods / Properties
T2DAnimatedSprite _sprite;
T2DAnimationData _cAnimData;
CAnimationMgr _cAnimMgr;
void InitState()
{
//set our state to alive and idle
_cState.SetState(CEnemyState._nAlive | CEnemyState._nIdle);
}
void InitAnimMgr()
{
//create a new animation manager object
_cAnimMgr = new CAnimationMgr();
//assuming your animation is dropped into the level somewhere using the editor, this will find the animation
//by its name and add it to the animation manager
_sprite = T2DSceneGraph.Instance.Manager.FindObject("MyAnimation") as T2DAnimatedSprite;
if(_sprite != null)
_cAnimMgr.AddAnimation(CEnemyState._nAlive | CEnemyState._nIdle, _sprite.AnimationData);
_sprite = T2DSceneGraph.Instance.Manager.FindObject("MyOtherAnimation") as T2DAnimatedSprite;
if(_sprite != null)
_cAnimMgr.AddAnimation(CEnemyState._nAlive | CEnemyState._nJump, _sprite.AnimationData);
//clean up
_sprite = null;
}
#endregion
#region Public Methods / Properties
CEnemyState _cState;
//constructor
public EnemyComponent()
{
//create a new enemy state object
_cState = new CEnemyState();
}
public void ProcessTick(Move move, float elapsed)
{
//make sure we're playing the correct animation
if(_sprite != null)
{
//get the animation based on our current state
_cAnimData = _cAnimMgr.GetAnimation(_cState._nState);
//if the animation data is different than what we're currently
//using, play the new animation
if(_cAnimData != null && _sprite.AnimationData != _cAnimData)
_sprite.PlayAnimation(_cAnimData);
}
}
public void InterpolateTick(float k)
{
}
#endregion
#region Overrides
protected override bool _InitComponent(TorqueObject owner)
{
if(!base._InitComponent(owner) || !(Owner is T2DAnimatedSprite))
return false;
//initialize our default state
InitState();
//initialize the animation manager
InitAnimMgr();
//we've already verified our owner is an animated sprite, so keep a reference to it
_sprite = (T2DAnimatedSprite)owner;
//if you want to do some processing every time a sprite's frame changes or ends
//comment either of the following two lines and their corresponding functions below
//_sprite.OnAnimationEnd += new T2DAnimatedSprite.OnAnimationEndDelegate(OnAnimationEnd);
//_sprite.OnFrameChange += new T2DAnimatedSprite.OnFrameChangeDelegate(OnFrameChange);
// tell the process list to notifiy us with ProcessTick and InterpolateTick events
ProcessList.Instance.AddTickCallback(Owner, this);
return true;
}
#endregion
#region Delegates
/*
public void OnFrameChange(int nFrame)
{
}
public void OnAnimationEnd()
{
}
*/
#endregion
}
}CState
using System;
using System.Collections.Generic;
using System.Text;
namespace StarterGame
{
public class CState
{
#region Public Methods / Properties
//would recommend you replace these with enums
public static uint _nAlive = 0x1;
public static uint _nDead = 0x2;
public static uint _nWalk = 0x4;
public static uint _nJump = 0x8;
public static uint _nFall = 0x16;
public static uint _nAttacking = 0x32;
public static uint _nIdle = 0x64;
public static uint _nDying = 0x128;
public uint _nState;
public CState()
{
_nState = 0;
}
public CState(uint nState)
{
SetState(nState);
}
public void SetState(uint nState)
{
_nState |= nState;
}
public void RemState(uint nState)
{
_nState &= ~nState;
}
public bool InState(uint nState)
{
return Convert.ToBoolean(_nState & nState);
}
#endregion
}
}CEnemyState
using System;
using System.Collections.Generic;
using System.Text;
namespace StarterGame
{
public class CEnemyState : CState
{
#region Public Methods / Properties
public CEnemyState()
{
}
public CEnemyState(uint nState)
{
SetState(nState);
}
#endregion
}
}CAnimationMgr
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using GarageGames.Torque.Core;
using GarageGames.Torque.Sim;
using GarageGames.Torque.T2D;
using GarageGames.Torque.MathUtil;
using GarageGames.Torque.XNA;
using GarageGames.Torque.Util;
namespace StarterGame
{
class CAnimationMgr
{
//dictionary holding our t2danimation data keyed on state
Dictionary<uint, T2DAnimationData> _Animations;
#region Public Methods / Properties
public CAnimationMgr()
{
//create a new dictionary
_Animations = new Dictionary<uint, T2DAnimationData>();
}
public void AddAnimation(uint nID, T2DAnimationData data)
{
//add an animation to the dictionary
_Animations.Add(nID, data);
}
public T2DAnimationData GetAnimation(uint nID)
{
//return the t2danimation data associated with the passed in state
return _Animations[nID];
}
public void RemAnimation(uint nID)
{
//remove an entry from the dictionary
_Animations.Remove(nID);
}
#endregion
}
}Note: Error handling is minimal. In particular, if you attempt to call GetAnimation() and there is no animation data associated with the passed in state, you will get an exception. You may or may not want to handle that exception.
#3
I think it's a very interesting component, I like the fact you can manage and combine states without touching the animations. Based on the first reading I'd like to suggest a couple of enhancements:
- Move animation management to TGBX: Instead of having "MyAnimation" in InitAnimMgr, it should be managed in TGBX. The goal should be that you could create a totally new enemy, assign your component to it, create and configure new animations for that enemy, all within TGBX without changing your component code. You could possibly do it by assigning special ObjectTypes and/or Names to the animations.
- Consider cloning: You probably need to create several enemies, and it might be nice the to capsulate the cloning of different templates in your component.
- Use enum to make a type of the states (makes clearer in my opinion).
Matias
03/28/2007 (11:56 pm)
Hi,I think it's a very interesting component, I like the fact you can manage and combine states without touching the animations. Based on the first reading I'd like to suggest a couple of enhancements:
- Move animation management to TGBX: Instead of having "MyAnimation" in InitAnimMgr, it should be managed in TGBX. The goal should be that you could create a totally new enemy, assign your component to it, create and configure new animations for that enemy, all within TGBX without changing your component code. You could possibly do it by assigning special ObjectTypes and/or Names to the animations.
- Consider cloning: You probably need to create several enemies, and it might be nice the to capsulate the cloning of different templates in your component.
- Use enum to make a type of the states (makes clearer in my opinion).
Matias
#4
1) I agree. I did mention above in my original post that my ultimate goal would be to use the level editor (TGBX) without having to modify the source.
2) I don't quite follow. There's nothing extra that would need to be done to support cloning. If you created an animated sprite, assigned these components to it, and flagged the animated sprite as a template in TGBX there's no reason you shouldn't be able to clone it. I think I may not understand your comment.
3) Enums. No doubt about it. However, I left that up as an exercise for the user in their specific implementation. For my initial proof of concept the static member variables worked just fine, but enums would be the way to go.
Thanks for the feedback!
03/29/2007 (7:46 am)
@Matias1) I agree. I did mention above in my original post that my ultimate goal would be to use the level editor (TGBX) without having to modify the source.
2) I don't quite follow. There's nothing extra that would need to be done to support cloning. If you created an animated sprite, assigned these components to it, and flagged the animated sprite as a template in TGBX there's no reason you shouldn't be able to clone it. I think I may not understand your comment.
3) Enums. No doubt about it. However, I left that up as an exercise for the user in their specific implementation. For my initial proof of concept the static member variables worked just fine, but enums would be the way to go.
Thanks for the feedback!
#5
It uses a custom FSM manager class that hashes one instance of each state for each object type that requests it and it uses an FSM interface (to allow any object to become an FSM - specifically, components). The states themselves are lightweight objects and the state switching conditions are defined on a per-state basis. All states are associated with a string name when they are registered with the FSM manager (the string is used to hash the state), which allows users to manipulate and reference states in a meaningful way without manipulating bits. Animation states, physics states, and AI states all use this system.
I'd normally share the code, but we're going to be selling the Starter Kit pretty soon. If anyone's interested, here's a farily long and boring video of out-of-the-box template building and level construction using the starter kit in TGBX: link.
03/29/2007 (5:45 pm)
The Platformer Starter Kit for Torque X has a similar system. You can specify animations for each animation state and additionally add any number of transitional animations between any two animation states all from the editor. You can also add sound events to any frame of any animation.It uses a custom FSM manager class that hashes one instance of each state for each object type that requests it and it uses an FSM interface (to allow any object to become an FSM - specifically, components). The states themselves are lightweight objects and the state switching conditions are defined on a per-state basis. All states are associated with a string name when they are registered with the FSM manager (the string is used to hash the state), which allows users to manipulate and reference states in a meaningful way without manipulating bits. Animation states, physics states, and AI states all use this system.
I'd normally share the code, but we're going to be selling the Starter Kit pretty soon. If anyone's interested, here's a farily long and boring video of out-of-the-box template building and level construction using the starter kit in TGBX: link.
#6
Thanks for posting the link, Thomas.
Edit: I just opened my game and removed collision from the individual tiles and replaced them with collidable scene objects as shown in the video...I feel foolish. Thanks again for the link, that just saved me loads of time.
03/29/2007 (6:20 pm)
The Platformer Starter Kit looks promising from the video. I liked the way the collision objects where overlayed on top of the terrain pieces. I've been adding collision to individual tiles in a tile-map, tedious to say the least, so watching that video certainly has given me some ideas to reduce the amount of work I've been doing.Thanks for posting the link, Thomas.
Edit: I just opened my game and removed collision from the individual tiles and replaced them with collidable scene objects as shown in the video...I feel foolish. Thanks again for the link, that just saved me loads of time.
Torque 3D Owner Jonathon Stevens