An FP game engine for Scala.

Frame context

Updating a frame in Indigo is principally about doing two things:

  1. Updating - ...the model and view model based on events and previous state
  2. Presenting - ...the frame to the player

For simplicity, we'll only talk about model updates from here on but this applies to model, view model and presentation updates equally.

One of the goals of Indigo is to make frame updates referentially transparent and testable. Superficially that's easy, here's a simple model class:

import indigo.Point

final case class Bob(position: Point):
  def update: Bob =
    this.copy(position = position + Point(1, 0))

So when we update Bob he's going to proceed across the screen. The trouble is that his movement is pretty limited because the only things we can base his next move on is Bob's own state.

What if we wanted to make Bob's movement totally erratic?

import indigo.Point
import scala.util.Random

final case class Bob(position: Point):
  def update: Bob =
    this.copy(position = position + Point(Random.nextInt(2), Random.nextInt(2)))

Remembering that in Random.nextInt(2), 2 is an exclusive upper limit - Bob is now going to jitter around by adding random coordinates made of 1's and 0's to his current position.

The problem is that Random goes against our referential transparency principle. What if you needed to write a test for Bob's update function, how would you know what value was going to come next? You could write a test to say that his next position would be at most 1 pixel away from his current position, but that's the best you could do.

Side note: Testing this kind of behavior with property based testing techniques works very well and often neatly side steps the issue of unknown values.

Similar problems occur if you attempt to implement frame independence based on the elapsed time. You could add a last updated field to Bob's model and use System.currentTimeMillis to get the delta, but again, how would you write a test for that.

The problem is that Random and System.currentTimeMillis are side effecting and that makes them hard to test.

FrameContext[_]

To make these things testable, a deterministic frame context is provided to give you a predictable environment to work in.

For some given starting definitions:

final case class StartUpData()
final case class Model()

Here is the definition of the model update function from IndigoDemo:

import indigo.*

def updateModel(context: FrameContext[StartUpData], model: Model): GlobalEvent => Outcome[Model] =
  _ => Outcome(model)

This function is supposed to update the model given some events, but it does so in the presence of context: FrameContext[StartUpData].

The frame context instance provides: