An FP game engine for Scala.

Part 1: Animation Fundamentals

There are essentially two forms of animation in Indigo.

  1. Animations you have imported from a third party tool.
  2. Animations you have described in code, known as procedural animations.

Importing animations

At the time of writing, the only built-in support for importing animations, is from Aseprite (an excellent pixel art editor and animation tool) into Indigo as a Sprite or a Clip. For working examples of this, please see the relevant section on Sprites and Clips.

Aseprite animations can either be embedded using Indigo's built-in generators, or loaded from JSON data at runtime. Contributions to support other formats are welcome!

Procedural Animations

This series of guides explores on how to describe your animations in code.

To get started, we're going to look at the fundamentals.

Movies vs Video Games

Question: What do the earliest animations, from physical zoetropes to early animations on cellular film have in common with the latest and greatest 3D animated movie extravaganza?

The answer is: A fixed frame rate, say 24 frames per second (the standard for theatrical animation screenings at one time).

That means that if you want to animate something moving from one side of the screen to the other, at constant speed in a duration of 1 second, you need to move it exactly 1/24th of the total distance on every frame, for 24 frames.

Sounds good! Doing that in code is as simple as doing this every frame:

val increment = totalDistance / 24

sprite.moveBy(increment, 0)

The problem with this solution is that video games are not movies, and they DO NOT have a consistent frame rate. Each frame will take slightly less or more time to process and render than the last frame. Using the method above will result in jerky and jittery animation.

Frame time deltas to the rescue

Luckily, there is an easy solution, all we need to do is multiple the desired number of units (i.e. distance) per second (in our case, the total distance) by the amount of time that has elapse since the last frame was processed.

You may recall from school that speed = distance / time, well all we're going to do is re-arrange that to distance = speed * time where distance is how far we need to move our sprite, speed is the desired distance to move every second, and time is therefore the frame delta. Which gives us something like:

sprite.moveBy(totalDistance * frameDelta, 0)

Example of procedural animation

Let's look at a real example and illustrate some of the problems a challenges we need to overcome.

In this example we will simply move a red circle across the screen from left to right, using the frame delta to ensure a constant speed.

How hard could it be?

The first thing we need to do is model some state. State?! Oh yes. In animation you either need to know the current state, or write some clever code that can fully work on the current animation position based only on the initial conditions and the running time. More on that later, for now, we're going to update a known position.

The position is wrapped in a model case class in a nod to realism, but curiously, the position is modelled as a Vertex, which takes Doubles, rather that a Point which uses Int as its unit type. What is going on? Point would seem to be the obvious choice, since we're moving across the screen in pixels, but let's do the math:

  1. We want to move across the screen at 50 pixels per second, and our game runs at 60 frames per second.
  2. 60 FPS is on average 16.667 milliseconds per frame, but we need to convert that to seconds, so: 0.016667.
  3. 50 pixels per second * 0.016667 frame delta = 0.83335.

So we need to move our circle 0.83335 pixels every frame, on average.

If conversion of Double to Int rounded the value then we'd jitter and stutter across the screen - sometimes moving a bit, sometimes not at all.

Actually, conversion of Double to Int floors the value, meaning 0, so we never move... Oh dear!

By modelling the position as a type based on Double and converting to pixels at the last moment during presentation, we can avoid all these difficulties.

final case class Model(position: Vertex)

We need to initialise our state with some acceptable values.

def initialModel(startupData: Unit): Outcome[Model] =
  Outcome(Model(Vertex(60, 120)))

During update we do our now familiar bit of maths, multiplying the speed by the frame delta

  def updateModel(context: Context[Unit], model: Model): GlobalEvent => Outcome[Model] =
    _ =>
      val pixelsPerSecond = 50

      Outcome(
        model.copy(
          position = model.position.withX(
            model.position.x + (pixelsPerSecond * context.frame.time.delta.toDouble)
          )
        )
      )

When we draw the circle, we simply move it to the position held in the model.

  def present(context: Context[Unit], model: Model): Outcome[SceneUpdateFragment] =
    val circle =
      Shape.Circle(
        Circle(Point.zero, 50),
        Fill.Color(RGBA.Red),
        Stroke(2, RGBA.White)
      )

    Outcome(
      SceneUpdateFragment(
        circle.moveTo(model.position.toPoint)
      )
    )

Gosh that was hard work! Imagine trying to do that for every moving thing on the screen! There must be another way, surely?