An FP game engine for Scala.

Scene Management

Scene's are a really handy way to organise your game into distinct 'scenes' or 'screens', such as a menu screen or a loading screen or the main game scene. The benefit is that, although they do not stop you from having full access to everything, they allow you to minify and encapsulate each screens functionality and data to keep it small and easy to reason about.

How to manage game scenes

In this example we'll set up a simple game with two scenes, where clicking anywhere moves from one scene to the next.

Scene's can either use the main game model and view model, or they can have their own. Custom models are made up of data stored in the main models. In this case, we'll define a simple model for each.

final case class SceneModelA(value: String)
final case class SceneModelB(value: String)

Scenes are simple objects then extend the Scene type.

Scenes are another instance of the TEA patterm themselves follow a very pattern to the main game functions, defining their own updateModel, updateViewModel, and present and so on.

object SceneA extends Scene[StartUpData, Model, ViewModel]:

The way that scene's get their own models and view models is via 'lenses'.

First We tell the scene what the type of it's own model and view models are. We then define lenses that tell the scene how to 'get' and 'set' those models from the main game models.

Scene models can be a piece of data that lives in a field on the main model, as in this example. They can also be ephemeral data types aggregated from various main model data points and pulled together each frame, as needed.

  type SceneModel     = SceneModelA
  type SceneViewModel = ViewModel

  val modelLens: Lens[Model, SceneModelA] =
    Lens(
      model => model.sceneA,
      (model, newMessage) => model.copy(sceneA = newMessage)
    )

  val viewModelLens: Lens[ViewModel, ViewModel] =
    Lens.keepLatest

There are a number of SceneEvents that you can experiment with, here we listen to the SceneChange event and use the JumpTo event to go to the scene we want, but there are others such as Next and Previous to explore.

  def updateModel(
      context: SceneContext[StartUpData],
      sceneModel: SceneModelA
  ): GlobalEvent => Outcome[SceneModelA] =
    case SceneEvent.SceneChange(from, to, at) =>
      println(s"A: Changed scene from '${from}' to '${to}' at running time: ${at}")
      Outcome(sceneModel)

    case MouseEvent.Click(_) =>
      Outcome(sceneModel)
        .addGlobalEvents(SceneEvent.JumpTo(SceneB.name))

    case _ =>
      Outcome(sceneModel)

The main game functions of an IndigoGame entry point are largely the same as you'd find in an IndigoGame, but there are two things to note:

  1. New methods called scenes and initialScene are declared. Scene's must be declared in order in the scenes NonEmptyList, and there must be at least one scene. The initial scene is optional, if None then the first scene loaded will be the first one in the scene's list.
  2. The usual functions like updateModel are declared as normal, but you can now thing of them as 'global' functions. The scene's implementation of updateModel will only be called when that scene is running, but updateModel in the main game runs all the time. This is helpful for dealing with any events that are not specific to any one scene, or presenting graphics that should appear all the time.
  def scenes(bootData: BootData): NonEmptyList[Scene[StartUpData, Model, ViewModel]] =
    NonEmptyList(SceneA, SceneB)

  def initialScene(bootData: BootData): Option[SceneName] = Option(SceneA.name)
def updateModel(context: Context[StartUpData], model: Model): GlobalEvent => Outcome[Model] =
  case _ => Outcome(model)