Varun Ramesh's Blog

Composable, Programmatic Animations for Games

Monday, September 4th 2017

When making a game, you often have animations that play during runtime. These animations often occur in relation to other animations - either in sequence or in parallel. To solidify this concept, let’s look at the following GIF of Candy Crush.

When the user swaps two candies, two animations play in parallel - one in which the the orange candy moves to the the blue candy’s spot, and one in which the blue candy moves to the orange candy’s spot. After those two animations finish, another set of animations play in parallel. These animations include the following:

After the blue candies disappear, the candies above them fall into place.

As you can see, this single game event fires off many animations that either happen in sequence or in parallel with each other. If you have a Game loop that uses Update(dt) methods for all time-based logic, this code is going to be messy.

Enter Composable Animations

I first encountered this pattern in LibGDX, where it is simply called Actions. Using LibGDX, you can create Action objects, which can then be applied to any actor.

// Create an action that smoothly moves an object to a position.
Action action = Actions.moveTo(x, y, duration);
// Apply the action to an actor.

What sets LibGDX’s system apart is that you can use combinators to turn lists of actions into an action that can be further composed. For example, the code below creates an action that applies multiple actions in series.

Action action = sequence(moveTo(200, 100, 2), color(Color.RED, 6),
    delay(0.5f), rotateTo(180, 5));

You can use the parallel combinator to perform two actions at the same time, and the forever combinator to perform an action in an indefinite loop. Furthermore, you can simply subclass Action in order to add your own primitives that can then be composed with other primitives using the combinators.

Unfortunately, there’s a defeciency in LibGDX’s system - actions are applied to individual actors, which means that we can’t encode the dependencies between animations on different objects. Take a look at the example below from one of my projects, where two objects must animate after the first object.

To implement this, the first object applies a MoveToAction, while the other two objects apply an Action with a delay.

// The moving piece.
    worldPosition.y, 1.0f, Interpolation.pow3Out));

// The captured pieces.
        Actions.moveBy(0, 2024.0f, 2.0f, Interpolation.pow3In),
        Actions.fadeOut(2.0f, Interpolation.pow3In)

The rectify this problem, we should make Actions store the particular actor they are acting on. Then, instead of applying an action to an actor, we “play” the action, or rather “apply” it to the current Scene. To implement this pattern, I created my own library. The library is pretty simple, and in fact wraps an existing callback-based timer library. An example of an animation using my library is shown below.

local objectA = {val = 0}, objectB = {val = 0}
local action = Actions.Sequence(
    Actions.Print("Started action..."),
    Actions.Print("3 seconds passed..."),
        Actions.Tween(3.0, objectA, {val=10}, 'linear'),
        Actions.Tween(2.0, objectB, {val=10}, 'linear')

action(Timer, function() print("Continuation...") end)

This library includes an Action called Actions.Do(func) which allows you to easily interact with existing imperative animation code.

Scripting an Entire Cutscene using Composable Animations

I first applied the composable animations pattern in a prototype that I worked on. This prototype involved cutscenes that would transition seamlessly into turn-based tactical combat on a grid. Here’s a link to the code that describes the first cutscene.

Every game has unique requirements, so you can add your own Action primitives. In this particular example, I defined Actions.PlaySound and Actions.DestroyEntity, but I could even add actions for NPC barks, fade outs and much more.

#game-dev #libgdx #lua #love2d #java

Similar Posts

How I Structure GameObjects - Components and...
Proctor Postmortem
Lua Gotchas