What is an animation?
As the last day of this part, we are going to discuss something more interesting about sprites. What brings sprites to life is them evolving and moving around, and that’s what animations are. Below is a simple example of an animation. The picture below shows an atlas containing 5 different poses of the character “Grace” from the game “Give Me a Kiss!”
As you might guess, these frames, when played in the right order, give the impression of her walking. Take a look at the animation below:
If we give numbers 0 to 4 to the frames in the atlas from left to right, then for this animation the frames are played in this order: 0, 1, 2, 1, 0, 3, 4, 3. This is the simplest form of animation for image sprites. What we simply did with a sprite here was to modify its frame with an interval. Sprites can be animated over their other properties, too. This includes position, rotational angle, and scale or even transparency. So, this brings our definition of an animation to change of a sprite’s properties over time. This is what we are going to implement today.
Animation handler
Simple interface
There are many ways to implement animation for game engines. Some high-end engines use reflection to directly modify an object’s properties. In our finalized framework, we can plug that approach in as well. But for now we like things simple. Artenus approaches animation handling through an interface called AnimationHandler. This interface is called back by a sprite for each frame to perform the required modification. Create a package called animation
as a sibling to sprites
package, and add the following interface to it:
package com.annahid.libs.artenus.animation; import com.annahid.libs.artenus.sprites.Sprite; public interface AnimationHandler { public void advance(Sprite sprite, float elapsedTime); }
This interface only has one method, which takes a sprite, and the time elapsed in terms of seconds (it is a floating point number so it can be a very small fraction of a second). How can this be any useful? The main answer is that this way each animation handler can process the sprite in the exclusive way it desires. Later on we will design an example animation handler to make this clearer.
Modifications to Sprite
The next step is to plug animation handling into sprites. We have animation handlers, so we just need to assign them to sprites. So, open your Sprite class and add the following field to it:
protected AnimationHandler anim = null;
A single animation handler should handle all animations corresponding to a sprite. If a sprite needs to be animated over several properties, it is the handler’s responsibility to handle all of them. Group and chain animation handlers can be implemented to enable multi-dimensional animations. We also add a getter and a setter:
public final void setAnimation(AnimationHandler animation) { anim = animation; } public final AnimationHandler getAnimation() { return anim; }
We now get to the most important method we need to add to Sprite. Important as it is, it is simple and self-explanatory:
final void advanceAnimation(float elapsedTime) { if(anim == null) return; anim.advance(this, elapsedTime); }
This method is called only from within the framework to advance the sprite’s animation. That’s why we implemented it with package access level. What happens when the scene is to move on to the next frame is that it goes through all sprites, and it asks them to advance their animation if they have any through this very method.
Image animation
We now implement a simple animation handler to demonstrate how the interface is used. Image animation is a kind of animation handler in Artenus that is responsible for frame animations in image sprites.
How it works
An image animation holds an array of frames, for example 0, 1, 2, 1, 0, 3, 4, 3 in the initial example. It also holds frame delay. Each time its advance method is called, it checks to see if enough time has passed to advance one frame. When the time is right, it advances by one frame and resets its timer. This is the main mechanism. We also define three kinds of animation trends:
- Once: animation is played only once. When it reaches the last frame, it stops there.
- Loop: animation will loop forever. When it reaches the last frame, it goes back to the first frame.
- Ping pong: animation is looped forever, but when it reaches the last frame, it reverses and plays backwards until it reaches the first frame. Then it reverses again and plays forward, and it keeps on playing this way.
Basic structure
We begin with what we explained so far. We define constants for different trend types, and add in basic fields. We also need to add the advance
method since we are implementing AnimationHandler
:
package com.annahid.libs.artenus.animation; import com.annahid.libs.artenus.sprites.Sprite; public final class ImageAnimation implements AnimationHandler { public static final int TREND_LOOP = 0; public static final int TREND_ONCE = 1; public static final int TREND_PINGPONG = 2; private int[] frames; private int trend; private int currentFrame; private int frameDelay = 33; public int getFrame() { return currentFrame; } public int getTrend() { return trend; } public void advance(Sprite sprite, float elapsedTime) { // TODO: We'll get to this later } }
In case you are wondering, frameDelay
is the time distance between frames in terms of milliseconds, and currentFrame
is the field that is going to change per frame. trend
is just a piece of information we will use later.
Constructors
We add three constructors for most convenience.
public ImageAnimation(int[] animationFrames) { this(animationFrames, TREND_LOOP, 0); } public ImageAnimation(int[] animationFrames, int trend) { this(animationFrames, trend, 0); } public ImageAnimation(int[] animationFrames, int trend, int startIndex) { frames = animationFrames; this.trend = trend; currentFrame = startIndex; }
The third constructor is rarely needed, but we just keep it there in case it is needed.
Advance method
In order to advance frames according to our frame delay, we need to keep a small frame timer. The simplest approach is to keep the last time the frame changed. This is where System.currentTimeMillis()
comes to help, since it provides current system time in milliseconds. Add the following field to the class:
private long lastFrame = 0;
Now, we begin the advance
method with the following lines:
if(System.currentTimeMillis() - lastFrame >= frameDelay) lastFrame = System.currentTimeMillis(); else return;
What these lines do is to check whether the time passed since lastFrame
is more than frameDelay
, which indicates that the frame should be advanced. In such case it sets lastFrame
to now and moves on. Otherwise it returns from the method as it is not yet time.
Next comes the tricky part. As we have three animation trends, we need to plan how we advance frames. For our ping-pong trend, the animation is played backwards at times. So, first thing that comes to mind is that the direction of frames is not fixed. So, we add a field, indicating this direction:
private int delta = 1;
We add this value to the current frame each time we advance it. So, if it is 1, the animation will go forward, and if it’s -1, it’ll go backward. Now, let’s get back to the advance method. We have to handle different trends differently.
The simplest code is for TREND_ONCE
. We just advance the frame if it is not the last frame:
if(currentFrame < frames.length - 1) currentFrame++;
For TREND_LOOP
, we should go back to the first frame after the last. We do this by getting the remainder of the current frame over the total number of frames:
currentFrame = (currentFrame + 1) % frames.length;
For TREND_PINGPONG
we should take care of delta reversal:
currentFrame += delta; if(currentFrame == 0 || currentFrame == frames.length - 1) delta = -delta;
After we are done deciding what the next frame will be, we simply update the image sprite with it. The final advance
method becomes like this:
public void advance(Sprite sprite, float elapsedTime) { if(System.currentTimeMillis() - lastFrame >= frameDelay) lastFrame = System.currentTimeMillis(); else return; if(trend == TREND_LOOP) currentFrame = (currentFrame + 1) % frames.length; else if(trend == TREND_PINGPONG) { currentFrame += delta; if(currentFrame == 0 || currentFrame == frames.length - 1) delta = -delta; } else if(currentFrame < frames.length - 1) currentFrame++; ((ImageSprite)sprite).gotoFrame(frames[currentFrame]); }
Setting frame delay
For more flexibility, we enable setting frame delay after the animation is created, or even while it is played. Internally, this delay is saved in terms of milliseconds. But since our framework speaks to the developer in seconds, we also add a method to set this value in seconds:
public void setFrameDelay(int delay) { frameDelay = delay; lastFrame = System.currentTimeMillis(); } public void setFrameDelay(float delay) { frameDelay = (int)(delay * 1000); lastFrame = System.currentTimeMillis(); }
Example of usage
Using this framework is simple. To reproduce the animation at the beginning of this article, you only need to do the following:
ImageSprite sprite = new ImageSprite(R.drawable.anim_girl, girlCutout); sprite.setAnimation(new ImageAnimation(new int[] {0, 1, 2, 1, 0, 3, 4, 3}));
When you add this sprite to the scene, it will automatically walk on the stage. Note here that all building blocks are not implemented yet, so you won't be able to see results yet. But we will implement this animation during the next part of the guide, when everything else is in place.
Next steps
This part of the guide is over. We covered different aspects of sprites. There are still little things to do, but we leave them at that. Next part of the guide will revolve around stages and scenes. If everything goes as planned, we will be able to create an animated scene by the end of next part. So, bookmark GDD and get ready for the next part. Today's code can be downloaded below: