Simple AnimationController for animating game objects
This tutorial shows you a simple to way animate other objects, in a way that doesn't require you to add a lot of extra code to your existing classes. We will end up by creating an AnimationController class, which can be used to animate anything which has a Vector2 Position property, by moving it from point A to point B in a straight line.
Presentation of what we want to achieve
In the video below you can see why I created the AnimationController. I wanted to juice-up the "setup next level" phase of the game board in my Simple Platformer Tutorial. This game was only used to illustrate the concept, you can use the AnimationController for anything you want moved automatically
What we will cover
- Designing intuitive APIs
- What is the point of putting this functionality in a controllerclass?
- Defining what something looks like (creating an interface)
- Creating the AnimationController
- Putting the level in place in random ways
- Getting Funky with "Func"
- Learning to love Lambda Expressions
- Putting a bow on it
Designing intuitive APIs
An Application Programming Interface (API) is a set of classes which enable you perform complex actions just by calling methods, and instantiating objects from the API. APIs are also frequently referred to as "class libraries". The .NET framework is a huge API with thousands of classes. A lot of planning went into making the .NET framework accessible and intuitive. One thing the designers did was ask themselves:
What code would I like to write, if I were the user of this API?
And we will start out by asking ourselves this very same question
My answer is:
- I would like to write as little code as possible to be up and running
- I would like to be able to add "anything which has a position" to the controller and ask it to move the object automatically from one point to another
- I would like to be able to check whether all animations are done
Notice that these are the answers ... in this case.
Another time we may want to achieve different objectives
Well - considering the requirements, we would like to be able to write something like this:
//ask the controller to animate all the items //in the list of game objects //in a line, ending 500 pixels to the right //of their starting position Vector2 fivehundredPixelsRight = new Vector2(500,0); foreach(var item in listOfItemsWhichHaveAPosition) { animationController.MoveTo(item, item.Position + fivehundredPixelsRight); }
and somewhere in our Game's Update() be able to ask whether all animation is done
if(animationController.Done) { //perform normal gameloop }
...that would be perfect for us, since we could add all the Tiles to the AnimationController, and have it move them back to their ending position from randomly selected starting positions we provide.
What is the point of putting this functionality in a controllerclass?
Q: why don't we just add all the animation functionality inside the classes we want to animate?
A: because this way we can
- keep our other classes (the ones we want moved) much leaner which increases readability, and...
- have the animation functionality as a separate little toolbox which can be used to animate classes which don't inherit any of our code, as long as the class has a Vector2 Position property
Defining what something looks like (creating an interface)
(If you are familiar with interfaces, you can skip to "Creating the AnimationController")
Okay - I've mentioned it a couple of times already:
"as long as the class has a Vector2 Position property"
Q: how do we go about defining that restriction in C#?
A: By using an interface!
Here is the definition we will use:
Note: The prefixed "I" in IPositionable is a convention used in .NET, so interfaces are easily recognized in code.
How do we benefit from this?
With this definition, a new "Type" like string or bool has been created. It can't perform anything but we can use it to describe any class which fulfills the requirements described in the interface.
Note: it isn't enough that a class fulfills the requirements, it must declare it in its class declaration.
See the difference from here:
public class Sprite { public Vector2 Position { get; set; } public Texture2D Texture { set; get; } public SpriteBatch SpriteBatch { get; set; } }
to here?
public class Sprite : IPositionable { public Vector2 Position { get; set; } public Texture2D Texture { set; get; } public SpriteBatch SpriteBatch { get; set; } }
Both sprites have the same functionality, but only one of them explicitly tells the compiler that it can be used anywhere that an IPositionable is needed.
What this means is that we can reference very different kinds of classes through the same datatype (interface): IPositionable. Let's consider a control we've made for a game, a TextBox, which we want to move on to the screen for receiving input and then move back out of the screen:
public class TextBox : IPositionable { public string Text { get; set;} public SpriteFont Font {get; set} ...etc... public Vector2 Position {get; set;} }
Since both the Sprite and TextBox above are IPositionables, they will both be able to be referenced through a variable of type IPositionable.
IPositionable textbox = new TextBox(); IPositionable sprite = new Sprite();
Furthermore, any method we write which accepts an IPositionable will take either the Sprite or the TextBox gladly, and work with the Position property .
Creating the AnimationController
Okay - now that you know how we can define the bare minimum for an external class to alter the position of something, let's start doing it.
What we will need
- a way of storing all the IPositionables together with the positions where we want them to end up
- an Update method to move all IPositionables in small steps closer and closer to their destination
- a way of telling from the outside whether all IPositionables have reached their destination
Storing all the IPositionables together with the positions where we want them to end up
A very efficient way of storing objects as KeyValuePairs, where you can use one of them to look up the other one is the Dictionary class.
Using this, we can define the type of the item we use for lookups and the other object to store with it. And voilà we automatically have a list of "things with a position" and their destinations:
public class AnimationController { private readonly Dictionary<IPositionable, Vector2> PositionablesWhichHaventCompletedMovement = new Dictionary<IPositionable, Vector2>(); }
To enable people to add items, we also add a public method to store the items as pairs:
public void MoveTo(IPositionable positionableToMove, Vector2 destination) { PositionablesWhichHaventCompletedMovement.Add(positionableToMove, destination); }
An Update method to move all IPositionables in small steps closer and closer to their destination
This Update() method will
- figure out the direction to move from from where the IPositionables are now
- calculate a reasonable "step" to take in this Update()
- move the IPositionable that step
- remove all the IPositionable which have reached their goal from the dictionary
Note: We will be using some vector math here. If you are unfamiliar with using vectors for directions and steps in XNA, have a look at this tutorial.
We will also require the current GameTime to be sent to the Update method to smooth the animation. We will multiply the length of each step with the time passed since last Update().
Figure out the direction to move from from where the IPositionables are now
foreach (var positionableAndTarget in PositionablesWhichHaventCompletedMovement) { IPositionable positionable = positionableAndTarget.Key; Vector2 destination = PositionablesWhichHaventCompletedMovement[positionable]; Vector2 remainingMovement = destination - positionable.Position; }
this way we have the entire remaining movement in which variable? ... you guessed it: remainingMovement!
The positionableAndTarget is the KeyValuePair I mentioned earlier, which has both the IPositionable (the Key) and its Vector2 destination (the Value).
Calculate a reasonable "step" to take in this Update()
Vector2 oneStep = remainingMovement * 6f * (float)gameTime.ElapsedGameTime.TotalSeconds;
Note that we use the total seconds passed since last update in the equation. This will usually be around 1/60th of a second, which means that one step will even out at around 6/60ths or 1/10th (10%) of the remaining movement each update. This gives us a nice slowing down effect as 10% becomes fewer and fewer pixels as the remaining movement is diminished.
Move the IPositionable that step
positionable.Position += oneStep;
Remove all the IPositionable which have reached their goal from the dictionary
Now we *could* just run through the list again, and remove all the keyvaluepairs, where the position of the IPositionable and the destination are the same, but since we already iterate over the list once, let's just create another list "FinishedPositionables" and store all the finished IPositionables in that. Then we can run through that (presumably much shorter) list of finished positionables afterwards and remove those from the dictionary.
public void Update(GameTime gameTime) { List<IPositionable> FinishedPositionables = new List<IPositionable>(); foreach (var positionableAndTarget in PositionablesWhichHaventCompletedMovement) { IPositionable positionable = positionableAndTarget.Key; Vector2 destination = PositionablesWhichHaventCompletedMovement[positionable]; Vector2 remainingMovement = destination - positionable.Position; Vector2 oneStep = remainingMovement * 6f * (float)gameTime.ElapsedGameTime.TotalSeconds; positionable.Position += oneStep; if (remainingMovement.Length() == 0) { positionable.Position = destination; FinishedPositionables.Add(positionable); } } foreach (var finishedPositionable in FinishedPositionables) { PositionablesWhichHaventCompletedMovement.Remove(finishedPositionable); } }
A way of telling from the outside whether all IPositionables have reached their destination
This one is easy. If the list of IPositionables is empty, we're done. We will implement this as a read-only property:
public bool Done { get { return PositionablesWhichHaventCompletedMovement.Count == 0; } }
This property returns true when Count is zero and false in other cases (meaning that there are still items in the list).
So here is our code now:
public class AnimationController { private readonly Dictionary<IPositionable, Vector2> PositionablesWhichHaventCompletedMovement = new Dictionary<IPositionable, Vector2>(); private readonly List<IPositionable> FinishedPositionables = new List<IPositionable>(); public bool Done { get { return PositionablesWhichHaventCompletedMovement.Count == 0; } } public void Update(GameTime gameTime) { FinishedPositionables.Clear(); foreach (var positionableAndTarget in PositionablesWhichHaventCompletedMovement) { IPositionable positionable = positionableAndTarget.Key; Vector2 destination = PositionablesWhichHaventCompletedMovement[positionable]; Vector2 remainingMovement = destination - positionable.Position; Vector2 oneStep = remainingMovement * 6f * (float)gameTime.ElapsedGameTime.TotalSeconds; positionable.Position += oneStep; if (remainingMovement.Length() < 1f) { positionable.Position = destination; FinishedPositionables.Add(positionable); } } RemoveAllFinishedPositionables(); } private void RemoveAllFinishedPositionables() { FinishedPositionables.ForEach(inplace => PositionablesWhichHaventCompletedMovement.Remove(inplace)); } public void MoveTo(IPositionable positionableToMove, Vector2 destination) { PositionablesWhichHaventCompletedMovement.Add(positionableToMove, destination); } }
Adding a useful method
In our case we have a level setup, with all the tiles in their final positions, and we want to animate them from somewhere else to where they already are. This means that we will have to move the tiles away (maybe to individually random places) while storing their original position, and then pass the moved Tile along with the original position to the MoveTo() method. This complicates things unnecessarily . So why don't we just add a method which does exactly this for os.
Introducing the MoveBackFromPosition() method . Try reading it and see whether you can figure out how this method will help us make cleaner code outside, by just sending it some IPositionable and where to move it back from.
public void MoveBackFromPosition(IPositionable positionableToMove, Vector2 startingPosition) { Vector2 endPosition = positionableToMove.Position; positionableToMove.Position = startingPosition; MoveTo(positionableToMove, endPosition); }
Basically it stores the current position (endPosition) of the IPositionable, and then moves the IPositionable out to the startingPosition, and then calls the MoveTo method to start the long move back - nifty, eh? .
Putting the level in place in random ways
Since we now have the AnimationController class working, let's test it .
All we need is a way to insert the original tiles and some starting point.
The easiest way to perform this would be to have all the tiles start out in the top left corner (0,0) and then move to their respective places. Like this:
private void RandomizeTilePositions() { foreach (var tile in Tiles) { AnimationController.MoveBackFromPosition(tile, Vector2.Zero); } }
...where we use the MoveBackFromPosition method above to "teleport" all the tiles to (0,0) and then have the AnimationController move the tiles back whenever we call its Update().
Giving this result (slowed to 1/4th of ordinary movement to show it more clearly):
This way we could have all sorts of fun mathematical ways of sending Tiles to another starting position and then moving them back, spicing up the "getting ready for next level" part of a game .
"But why doesn't the Jumper fall while we're putting everything into place?" I hear you ask .
Well, that's because in SimplePlatformerGame.Update(), I've put an IF around the update of the Jumper:
if (_board.AnimationController.Done) { _jumper.Update(gameTime); }
..so it doesn't move before we're done setting up the board .
Getting Funky with "Func"
To spice things up even further, we want to randomize things a bit, by having different ways of putting the tiles in place. We can then "roll a die" and see which method to use for this "setting up the level", or just take them one at a time.
This could be done in the Board class like this:
Random rnd = new Random(); switch (rnd.Next(3)) { case 0: //move out from top left corner (0,0) foreach (var tile in Tiles) { AnimationController.MoveBackFromPosition(tile, Vector2.Zero); } break; case 1: //move from a random place foreach (var tile in Tiles) { AnimationController.MoveBackFromPosition(tile, new Vector2(rnd.Next(1000), rnd.Next(1000))); } break; case 2: //swap x and y axis to "unfold" level foreach (var tile in Tiles) { AnimationController.MoveBackFromPosition(tile, new Vector2(tile.Position.Y, tile.Position.X)); } break; }
...but it tends to get long-winded and has the loop-code in every case in the switch. And as soon as we start writing the same code over and over - programmers think "do this in a function!".
Here, what we want to swap out is what happens inside the foreach loop, so we would need some way of changing what code is called from there.
Func to the rescue!
In recent versions of C# we are able to reference functions using the Action (for functions which don't return a value) and Func (for functions which do). This way we can add a method to our AnimationController which accepts a Func with this signature:
Func<IPositionable, Vector2>
This means: The Func variable of this type accepts an IPositionable and returns a Vector2.
Now we can store a list of these kinds of references and just pass them to this method on AnimationController:
public void MoveBackFromPosition(IPositionable positionableToMove, Func<IPositionable, Vector2> startPositioningMethod) { Vector2 endPosition = positionableToMove.Position; positionableToMove.Position = startPositioningMethod(positionableToMove); MoveTo(positionableToMove, endPosition); }
Compare it to the MoveBackFromPosition method we already had:
public void MoveBackFromPosition(IPositionable positionableToMove, Vector2 startingPosition) { Vector2 endPosition = positionableToMove.Position; positionableToMove.Position = startingPosition; MoveTo(positionableToMove, endPosition); }
and you'll see they are awfully similar.
All we have changed is that in the new method:
- we accept "a function which can give us a Vector2 when we feed it an IPositionable
- we use this function to give us the new starting position and then move to the original position as usual
This would enable us to rewrite the "randomized entry" code in the Board class above like this:
Random rnd = new Random(); public Vector2 FromTopLeft(IPositionable tile) { return Vector2.Zero; } public Vector2 FromRandomPosition(IPositionable tile) { return new Vector2(rnd.Next(1000), rnd.Next(1000)); } public Vector2 FromSwappedXandY(IPositionable tile) { return new Vector2(tile.Position.Y, tile.Position.X); } private void RandomizeTilePositions() { Func<IPositionable, Vector2> selectedPositioningFunction = null; switch (rnd.Next(3)) { case 0: //move out from top left corner (0,0) selectedPositioningFunction = FromTopLeft; break; case 1: //move from a random place selectedPositioningFunction = FromRandomPosition; break; case 2: //swap x and y axis to "unfold" level selectedPositioningFunction = FromSwappedXandY; break; } foreach (var tile in Tiles) { AnimationController.MoveBackFromPosition(tile, selectedPositioningFunction); } }
At the top we declare and instantiate a Random for rolling dice, we create three methods which fit our Func signature, and in the RandomizeTilePositions, we find a reference to one of the three, and then use that in the foreach loop.
Very elegant - I like it!
Let's see it in action:
Learning to love lambda expressions
Since we're not doing a whole lot in our "IPositionable to Vector2" methods, we could sum them up using Lambda Expressions. If you don't feel ready to take this last step, just stick with the code we have created already. It's fine, and Lambda Expressions, though elegant, are not guaranteed to make you happier. Basically, they are an even more compact way of writing methods. If you're still up to a bit more learning, you can follow along and see what the benefits are .
The central part of a Lambda Expression is an arrow
=>
... which separates the parameter(s) on the left and the return value on the right.
Let's take the FromSwappedXandY() method and rewrite it using lambda:
//regular method public Vector2 FromSwappedXandY(IPositionable tile) { return new Vector2(tile.Position.Y, tile.Position.X); } //lambda method tile => new Vector2(tile.Position.Y, tile.Position.X);
Notice that we don't have the return keyword either, as the Lambda knows that "I should return whatever is to the right of the arrow".
Syntactically you can't place a Lambda Expression on the class like we do with regular methods. Lambda Expressions are meant to be used right away (as a parameter to a method which accepts a Func/Action of that type) or stored in a variable for use later. Now we can rewrite the RandomizeTilePositions as follows:
Random rnd = new Random(); private void RandomizeTilePositions() { Func<IPositionable, Vector2> selectedPositioningFunction = null; switch (rnd.Next(3)) { case 0: //move out from top left corner (0,0) selectedPositioningFunction = tile => Vector2.Zero; break; case 1: //move from a random place selectedPositioningFunction = tile => new Vector2(rnd.Next(1000), rnd.Next(1000)); break; case 2: //swap x and y axis to "unfold" level selectedPositioningFunction = tile => new Vector2(tile.Position.Y, tile.Position.X); break; } foreach (var tile in Tiles) { AnimationController.MoveBackFromPosition(tile, selectedPositioningFunction); } }
When replacing regular methods (with a meaningful name) with a Lambda Expression, what you have to be careful about is losing readability. Your code will almost invariably be harder to understand the more succinct it is. You must choose how to find that balance
So why don't we go ahead and store a little toolbox of positioning methods in a class where we can get the Funcs from when needed?
Putting a bow on it
I have taken eight different ways of animating the tiles into place (see video at the top of this tutorial), and stored them in variable with meaningful names. This is a principle of Clean Code, to avoid comments and use self-commenting code.
Func<IPositionable, Vector2> fromTopLeftCorner = tile => Vector2.Zero; ; Func<IPositionable, Vector2> fromTopRow = tile => new Vector2(tile.Position.X, 0); ; Func<IPositionable, Vector2> fromRandomStartingPoint = tile => new Vector2(Rnd.Next(1000), Rnd.Next(1000)); Func<IPositionable, Vector2> fromCenter = tile => Vector2.One * 500; ; Func<IPositionable, Vector2> fromLeftColumn = tile => new Vector2(0, tile.Position.Y); Func<IPositionable, Vector2> fromBelow = tile => tile.Position + Vector2.UnitY * 1000; Func<IPositionable, Vector2> fromScaledOut = tile => tile.Position * 5 - Vector2.One * 64; Func<IPositionable, Vector2> fromSwappedXY = tile => new Vector2(tile.Position.Y, tile.Position.X);
You might want to remove the absolute values I've used (500, 1000, etc.) and rewrite this code for use in any screen resolution, but you get the idea from this .
These methods I place in a static class, so there is no need to "new it up", and store them in a list for easy access.
public static class PositioningFunctions { public static readonly List<Func<IPositionable, Vector2>> positioningFunctions; private static readonly Random Rnd = new Random(); static PositioningFunctions() { Func<IPositionable, Vector2> fromTopLeftCorner = tile => Vector2.Zero; Func<IPositionable, Vector2> fromTopRow = tile => new Vector2(tile.Position.X, 0); Func<IPositionable, Vector2> fromRandomStartingPoint = tile => new Vector2(Rnd.Next(1000), Rnd.Next(1000)); Func<IPositionable, Vector2> fromCenter = tile => Vector2.One * 500; Func<IPositionable, Vector2> fromLeftColumn = tile => new Vector2(0, tile.Position.Y); Func<IPositionable, Vector2> fromBelow = tile => tile.Position + Vector2.UnitY * 1000; Func<IPositionable, Vector2> fromScaledOut = tile => tile.Position * 5 - Vector2.One * 64; Func<IPositionable, Vector2> fromSwappedXY = tile => new Vector2(tile.Position.Y, tile.Position.X); positioningFunctions = new List<Func<IPositionable, Vector2>>() { fromTopRow, fromTopLeftCorner, fromRandomStartingPoint, fromCenter, fromLeftColumn, fromBelow, fromScaledOut, fromSwappedXY }; } }
Finally I add a method to get a Func and move to the next using a private variable, so we end up with this:
public static class PositioningFunctions { private static readonly List<Func<IPositionable, Vector2>> positioningFunctions; private static readonly Random Rnd = new Random(); static int _currentAnim; static PositioningFunctions() { Func<IPositionable, Vector2> fromTopLeftCorner = tile => Vector2.Zero; ; Func<IPositionable, Vector2> fromTopRow = tile => new Vector2(tile.Position.X, 0); ; Func<IPositionable, Vector2> fromRandomStartingPoint = tile => new Vector2(Rnd.Next(1000), Rnd.Next(1000)); Func<IPositionable, Vector2> fromCenter = tile => Vector2.One * 500; ; Func<IPositionable, Vector2> fromLeftColumn = tile => new Vector2(0, tile.Position.Y); Func<IPositionable, Vector2> fromBelow = tile => tile.Position + Vector2.UnitY * 1000; Func<IPositionable, Vector2> fromScaledOut = tile => tile.Position * 5 - Vector2.One * 64; Func<IPositionable, Vector2> fromSwappedXY = tile => new Vector2(tile.Position.Y, tile.Position.X); positioningFunctions = new List<Func<IPositionable, Vector2>>() { fromTopRow, fromTopLeftCorner, fromRandomStartingPoint, fromCenter, fromLeftColumn, fromBelow, fromScaledOut, fromSwappedXY }; } public static Func<IPositionable, Vector2> NextPositioningFunction { get { _currentAnim++; if (_currentAnim == positioningFunctions.Count) { _currentAnim = 0; } return positioningFunctions[_currentAnim]; } } }
This is nice! We have definitions of the setup logic in variables with meaningful names. We have stored them in a list (which I have now made private to only enable access through the readonly NextPositioningFunction property), and we have an autoincrement for every get().
Now our Board class' RandomizeTilePositions looks like this:
private void RandomizeTilePositions() { Func<IPositionable, Vector2> positioningFunctionToUse = PositioningFunctions.NextPositioningFunction; foreach (var tile in Tiles) { AnimationController.MoveBackFromPosition(tile, positioningFunctionToUse); } }
Which is how I want it - clean and readable!
Source code (XNA 4.0, VS 2010)
Here is the completed solution.
I hope you learned something useful along the way. Thanks for reading!