Trending
Opinion: How will Project 2025 impact game developers?
The Heritage Foundation's manifesto for the possible next administration could do great harm to many, including large portions of the game development community.
Featured Blog | This community-written post highlights the best of what the game industry has to offer. Read more like it on the Game Developer Blogs or learn how to Submit Your Own Blog Post
Part 2 of 2 in a quick series on how we organize and architect our screen management classes for easy extensibility and control. This post focuses on an Animation sequencer.
In this part 2 of 2 posts, I’m going to describe an animation system that I’ve used for quite some time. There are a few tricks that are slightly clever, and the system allows for a lot of complexity with a fairly simple framework. If you haven’t read my previous post about the Display/Logic split, you may want to get through it for a bit of context for this section.
Previously, I discussed how we would handle game logic in one class, and the display of the game state with a separate class. In typical casual games (poker, match-three, and jumping games for example) what I would do is have the logic update the game state all at once, and then have the display “catch up” by playing pretty animations. Of course, we could just have the display instantly match the logic, but that’s not quite as magical for the player.
While there are plenty of tweening and motion animation libraries that are available on the internet, I realized that I needed to solve a different problem: Sequencing. For example, let’s say that we have some Checkers-type game, where the player jumps pieces on a board over other pieces, making the jumped-over pieces vanish for points. There are score multipliers for many jumps in a row, and we want to communicate the score progression and the multipliers to the player.
Given our previous discussion, we may have our Display handle mouse clicks that select a board square with a piece to jump, or with a destination for that piece to jump to. The method in IGameLogic may be something like playerClickedSpot(x,y), where x,y determines the board square that was clicked. There are many different things that can happen to that board spot. Maybe there’s nothing there and the click causes no action. Maybe a player clicks a game piece they can’t move. Maybe a player clicks a piece they can move, then clicks an invalid jump destination. Maybe they’re rapidly clicking a series of valid locations. We need to handle all of these in a way that makes sense to the player.
As I stated above, we typically have the game logic update the game state instantaneously. This example isn’t for a physics based FPS where we would have the logic track the arc of the jump curve for the piece on a frame-tick counter. It’s fine for the logic to instantly move a piece from origin to destination and tabulate the new score, as well as track any multipliers. This way, we know if a subsequent input is valid or not, which is important.
So our game logic can respond by respectively calling methods exposed in our IGameDisplay interface such as NoAction(), InvalidPiece(x,y), PieceSelect(x,y), InvalidDest(x,y), PieceJump(startX, startY, endX, endY, jumpedX, jumpedY, totalScore, multiplier). These are fine methods that the Display can respond to, perhaps by playing a buzzing sound for InvalidPiece or InvalidDest while wiggling the selected piece on the board. For PieceJump, we can kick off the motion of the jumping game piece, and update the numeric score. If multiplier is nonzero, we can show another fancy number floating upwards while playing a great sound.
But how do we know exactly when to do these things on the Display side? And what about that last case, where the player rapidly clicks a series of valid jump destinations in order? If the logic updates instantly, and calls back to display with a new PieceJump call instantly, won’t those motions override each other, confusing the player? Yes, so we can’t handle the updates instantly on the display side, and we need a way to sequence those animations.
This is where the AnimationSequencer comes in. The AnimationSequencer is a class that handles ordering and playback of animations in our game. Every animation that it handles implements an interface, IAnimation, which exposes the following methods:
start()
tick(deltaT)
finish()
isStarted()
isComplete()
isBlocking()
Let’s focus on that last one, isBlocking(), because that’s the key to a huge amount of power here. Every class that implements IAnimation must set whether or not it is blocking, either as an option in the constructor, or as a default. Blocking is just a flag, true or false, either this animation blocks or it doesn’t.
The rest of the methods are straightforward. Start does setup, and is called on the first frame (if isStarted returns false). Tick is called every frame thereafter, and when the animation realizes it is done, it internally calls finish on itself, which will set the isComplete flag to true.
Here’s an example concrete class for doing linear motion:
package com.syncbuildrun.Engine.Animations
{
import flash.display.DisplayObject;
public class LinearMoveAnimation implements IAnimation
{
protected var m_started:Boolean;
protected var m_complete:Boolean;
protected var m_blocking:Boolean;
private var m_element:DisplayObject;
private var m_startX:Number;
private var m_startY:Number;
private var m_endX:Number;
private var m_endY:Number;
private var m_time:Number;
private var m_totalTime:Number;
private var m_preDelay:Number;
private var m_deltaX:Number;
private var m_deltaY:Number;
public function LinearMoveAnimation(element:DisplayObject, startX:Number, startY:Number, endX:Number, endY:Number, time:Number, preDelay:Number,isBlocking:Boolean)
{
m_started = false;
m_complete = false;
m_blocking = isBlocking;
m_element = element;
m_startX = startX;
m_startY = startY;
m_endX = endX;
m_endY = endY;
m_time = time;
m_totalTime = time;
m_preDelay = preDelay;
m_deltaX = m_endX - m_startX;
m_deltaY = m_endY - m_startY;
}
public function start():void
{
m_started = true;
m_complete = false;
}
public function tick(deltaTime:Number):void
{
// empty function
if(m_preDelay > 0.0)
{
m_preDelay -= deltaTime;
if(m_preDelay <= 0.0)
{
m_element.x = m_startX;
m_element.y = m_startY;
}
}
else
{
m_time -= deltaTime;
if(m_time <= 0.0)
{
return finish();
}
var percentage:Number = 1.0 - (m_time/m_totalTime);
m_element.x = m_startX + (m_deltaX * percentage);
m_element.y = m_startY + (m_deltaY * percentage);
}
}
public function finish():void
{
m_complete = true;
m_element.x = m_endX;
m_element.y = m_endY;
}
public function isComplete():Boolean
{
return m_complete;
}
public function isStarted():Boolean
{
return m_started;
}
public function isBlocking():Boolean
{
return m_blocking;
}
}
}
The AnimationSequencer operates on the frame tick of our game, so it’s called by the onEnterFrame method of our IDisplayLayer. The Sequencer has two data structures, a queue and a list. The queue is a pending queue; The rest of our code adds animations to be played into this queue. The list is the current playing list; The Sequencer ensures that the animations in this list are playing, or clears them out when they are complete.
Here’s the magic: Every frame, the Sequencer first checks if there are Blocking Animations in the list. If there aren’t, it starts pulling IAnimations from the queue until either the queue is empty, or there’s a blocking animation in the list. Then it stops, and services the IAnimations in the currently playing list by calling tick (or start). It checks every animation that is started to see if isComplete returns true, in which case it pulls that IAnimation from the list and discards it – it’s done. If a Blocking Animation was removed, the next frame we’ll pull more animations from the queue onto the list until the above conditions are met.
This seems incredibly simplistic, but it affords amazing power. Now, by correctly setting queued animations as blocking, we can control the order and pacing of every update from the Game Logic. In our above example, where we get a first call into PieceJump(), we might queue up the following animations:
PlaySound(jumpSound)
ScoreRollup(newScoreval, duration=1sec)
PieceJump(selectedPieceSprite, startX, startY, endX, endY, duration=1sec, BLOCK)
PieceRemove(removeX, removeY, BLOCK)
Our Sequencer will start playing a sound, start rolling the score up to the new value from the presently displayed value, and will start the selectedPieceSprite jumping from the board square at StartX, StartY to the square at endX,endY over the duration of 1 second. Because this last animation is blocking, it will wait until that completes before it pulls PieceRemove into the list, which will vanish the just jumped piece at removeX, removeY.
But wait, half a second into this, the player clicked another square for a second valid jump. Now we get a call into PieceJump() while these animations are still playing. But that’s fine! Because now we add the following to the queue:
PlaySound(jumpSound)
ScoreRollup(newScoreval, duration=1sec)
FloatMultiplier(removeX,removeY, MultiplierValue, duration=0.5sec)
PieceJump(selectedPieceSprite, startX, startY, endX, endY, duration=1sec, BLOCK)
PieceRemove(removeX, removeY, BLOCK)
We added a multipler float up animation, but that won’t start playing until the first PieceRemove from above has finished. All these new animations will sit and wait until the first move completed, and then they will proceed in a normal fashion, which is what the player expects.
Here’s the tick function for our Sequencer:
public function tick(deltaTime:Number):void
{
var blockerExists:Boolean = false;
var removeArray:Array = new Array(); // of ints
for(var i:int = 0;i<m_activeList.length;i++)
{
var anim:IAnimation = m_activeList[i];
if(anim.isStarted() == true)
{
anim.tick(deltaTime);
}
else
{
anim.start();
}
if(anim.isComplete() == false)
{
blockerExists = blockerExists || anim.isBlocking();
}
else
{
removeArray.push(i); // add index of finished anim
}
}
// now walk backwards to remove finished anims from the active array
for(var j:int=(removeArray.length - 1);j >= 0;j--)
{
m_activeList.splice(removeArray[j],1);
}
while(blockerExists == false)
{
var newAnim:IAnimation = m_pendingQueue.shift();
if(newAnim != null)
{
blockerExists = blockerExists || newAnim.isBlocking();
m_activeList.push(newAnim);
}
else
{
blockerExists = true;
}
}
}
Two final notes: First, PlaySound is not really an animation, but we can queue anything into the AnimationSequencer that conforms to the IAnimation interface. In fact, I found one of the most useful animation types is the BlockingDelayAnimation. It doesn’t affect a single thing on screen, but it takes up time, and it blocks further animations down the queue. It’s very useful for pacing apart screen events into digestible chunks for the player.
Second, I stated above that an IAnimation will call finish on itself when it reaches the end of it’s runtime inside of the it’s tick method. However, exposing finish via the interface allows us to have a clean way to skip animations while ensuring that everything winds up in the correct final position. If you want to allow players to skip through sequences, or you want to shut down all animations before leaving a display mode, simply call finish() on every animation in the list and then every animation in the queue in order. If the Animation classes correctly put all of their final object positioning/state/whatever code in the finish method, then everything will look exactly as if all the animations had completed normally.
Read more about:
Featured BlogsYou May Also Like