A Game Engine in the Elm Style!

A ‘Nu’ way to make games!

The Nu Game Engine was the world’s first practical, purely-functional game engine. And it has recently accomplished another first — allowing developers to use Elm-style architecture (AKA, model-view-update) to build their games in the cleanest, most understandable possible way!

This article will go over two examples often used by the Elm developer written in Nu. The first shows an tiny Elm-style UI in Nu, and the second a tiny little Mario-like example.

For a quick intro to Elm and the Elm-style, see here – https://guide.elm-lang.org/

Because it is simpler, let’s start by looking at Nu’s way of implementing the canonical Elm-style UI –

The full code is as follows (see it on Github) –

namespace Nelmish
open Prime
open Nu
open Nu.Declarative

// this is our Elm-style model type
type Model =
    int

// this is our Elm-style message type
type Message =
    | Decrement
    | Increment
    | Reset
    interface Nu.Message

// this is our Elm-style game dispatcher
type NelmishDispatcher () =
    inherit GameDispatcher<Model, Message, Command> (0) // initial model value

    // here we handle the Elm-style messages
    override this.Message (model, message, _, _) =
        match message with
        | Decrement -> just (model - 1)
        | Increment -> just (model + 1)
        | Reset -> just 0

    // here we describe the content of the game including its one screen, one group, three
    // button entities, and one text control.
    override this.Content (model, _) =
        [Content.screen "Screen" Vanilla []
            [Content.group "Group" []
                [Content.button "Decrement"
                    [Entity.Position == v3 -256.0f 64.0f 0.0f
                     Entity.Text == "-"
                     Entity.ClickEvent => Decrement]
                 Content.button "Increment"
                    [Entity.Position == v3 0.0f 64.0f 0.0f
                     Entity.Text == "+"
                     Entity.ClickEvent => Increment]
                 Content.text "Counter"
                    [Entity.Position == v3 -128.0f -32.0f 0.0f
                     Entity.Text := string model
                     Entity.Justification == Justified (JustifyCenter, JustifyMiddle)]
                 if model <> 0 then
                    Content.button "Reset"
                       [Entity.Position == v3 -128.0f -128.0f 0.0f
                        Entity.Text == "Reset"
                        Entity.ClickEvent => Reset]]]]

This example code creates a Nu game program that shows a + button as well as a button that automatically change the numeric value of the counter button when clicked. Additionally, there is a reset button that will revert the counter to its original value (and only exists when the value has changed).

Let’s step through each part of the code, from the top –

// this is our Elm-style model type
type Model =
    int

Here we have the Model type that users may customize to represent their simulant’s ongoing state. Here we use just an int to represent the counter value shown by the Counter label. If we were to write, say, a custom Text widget, the Model type would be a string instead of an int. You’re not limited to primitive types, however — you may make your model type as sophisticated as you see fit.

// this is our Elm-style message type
type Message =
    | Decrement
    | Increment
    | Reset
    interface Nu.Message

This is the Message type that represents all possible changes that the Message function will handle (in Elm, the Message function is called Update, but we don’t call it Update in Nu because that function already exists as a lower-level function for simulant ticking).

// this is our Elm-style game dispatcher
type NelmishDispatcher () =
    inherit GameDispatcher<Model, Message, Command> (0) // initial model value

This code does three things –

  1. It declares a containing scope for the Elm-style function overrides (seen coming up next) and packages them as a single plug-in for use by external programs such as Nu’s world editor, Gaia.
  2. It allows the user to specify the Elm-style model, message, and command types. Here we pass the empty Command type for the command paramter since this simulant doesn’t utilize commands.
  3. The base constructor function takes as a parameter the initial Model value (here, 0).

Let’s look at the next bit –

        // here we handle the Elm-style messages
        override this.Message (model, message, _, _) =
            match message with
            | Decrement -> just (model - 1)
            | Increment -> just (model + 1)
            | Reset -> just 0

This is the Message function itself. All it does is match each message it receives to an expression that changes the Model value in an appropriate way.

“But what is that just function?”

Good question! Strictly speaking, the Message functions return both a new Model as well as a list of signals for processing by Message and (here unused) Command functions. But since we don’t need to generate additional signals, I use the just function to automatically pair an empty signal list with the new model value. It’s just a little bit of syntactic sugar to keep things maximally readable!

    // here we describe the content of the game including its one screen, one group, three
    // button entities, and one text control.
    override this.Content (model, _) =
        [Content.screen "Screen" Vanilla []
            [Content.group "Group" []
                [Content.button "Decrement"
                    [Entity.Position == v3 -256.0f 64.0f 0.0f
                     Entity.Text == "-"
                     Entity.ClickEvent => Decrement]
                 Content.button "Increment"
                    [Entity.Position == v3 0.0f 64.0f 0.0f
                     Entity.Text == "+"
                     Entity.ClickEvent => Increment]
                 Content.text "Counter"
                    [Entity.Position == v3 -128.0f -32.0f 0.0f
                     Entity.Text := string model
                     Entity.Justification == Justified (JustifyCenter, JustifyMiddle)]
                 if model <> 0 then
                    Content.button "Reset"
                       [Entity.Position == v3 -128.0f -128.0f 0.0f
                        Entity.Text == "Reset"
                        Entity.ClickEvent => Reset]]]]

Here we have the Content function. The Content function is mostly equivalent to the View function in Elm. Here the Content function defines the game’s automatically-created (and destroyed as is needed) simulants. Studying this structure, we can see that we have a simulant structure like this –

Screen/
    Group/
        Increment
        Decrement
        Counter
        Reset

The Content function declares that the above hierarchy is instantiated at run-time. Each Content clause can also define its respective simulant’s properties and event handlers in a declarative way.

                [Content.button "Decrement"
                    [Entity.Text == "-"
                     Entity.Position == v2 -256.0f 64.0f
                     Entity.ClickEvent ==> Decrement]

Here we have the DecrementButton’s Text property defined as “-”, its Position translated up and to the left, and its ClickEvent producing the Decrement message that was handled above.

                 Content.button "Increment"
                    [Entity.Text == "+"
                     Entity.Position == v2 0.0f 64.0f
                     Entity.ClickEvent ==> Increment]

Here is the IncrementButton, which produces the Increment message.

                 Content.text "Counter"
                    [Entity.Position == v3 -128.0f -32.0f 0.0f
                     Entity.Text := string model
                     Entity.Justification == Justified (JustifyCenter, JustifyMiddle)]

Here we see first use of the := operator. This tells the Text property to sets its value to the result of the expression on the right hand side whenever the result of that expression changes.

                 if model <> 0 then
                    Content.button "Reset"
                       [Entity.Position == v3 -128.0f -128.0f 0.0f
                        Entity.Text == "Reset"
                        Entity.ClickEvent => Reset]]]]

And lastly, here we have a button that exists only while the outer if expression evaluates to true. So, as long as the model value is non-zero, the button entity will exist. Otherwise, it will not. The engine takes care of creating and destroying the button entity accordingly.

Now let’s look at Elm-Mario in Nu (see it on Github here) –

The code here is a bit more involved. It demonstrates how Nu’s uses its scalable built-in physics engine by applying forces rather than using ad-hoc physics routines –

namespace Elmario
open Prime
open Nu
open Nu.Declarative

// this module provides global handles to the game's key simulants.
// having a Simulants module for your game is optional, but can be nice to avoid duplicating string literals across
// the code base.
[<RequireQualifiedAccess>]
module Simulants =

    let Screen = Nu.Screen "Screen"
    let Group = Screen / "Group"
    let Elmario = Group / "Elmario"

// this is our Elm-style command type
type Command =
    | Update
    | Jump
    | Nop
    interface Nu.Command

// this is our Elm-style game dispatcher
type ElmarioDispatcher () =
    inherit GameDispatcher<unit, Message, Command> (())

    // here we define the game's properties and event handling
    override this.Initialize (_, _) =
        [Game.UpdateEvent => Update
         Game.KeyboardKeyDownEvent =|> fun evt -> if evt.Data.KeyboardKey = KeyboardKey.Up && not evt.Data.Repeated then Jump else Nop]

    // here we handle the Elm-style commands
    override this.Command (_, command, _, world) =
        match command with
        | Update ->
            let physicsId = Simulants.Elmario.GetPhysicsId world
            if World.isKeyboardKeyDown KeyboardKey.Left world then
                let world =
                    if World.isBodyOnGround physicsId world
                    then World.applyBodyForce (v3 -2500.0f 0.0f 0.0f) physicsId world
                    else World.applyBodyForce (v3 -750.0f 0.0f 0.0f) physicsId world
                just world
            elif World.isKeyboardKeyDown KeyboardKey.Right world then
                let world =
                    if World.isBodyOnGround physicsId world
                    then World.applyBodyForce (v3 2500.0f 0.0f 0.0f) physicsId world
                    else World.applyBodyForce (v3 750.0f 0.0f 0.0f) physicsId world
                just world
            else just world
        | Jump ->
            let physicsId = Simulants.Elmario.GetPhysicsId world
            if World.isBodyOnGround physicsId world then
                let world = World.playSound Constants.Audio.SoundVolumeDefault (asset "Gameplay" "Jump") world
                let world = World.applyBodyForce (v3 0.0f 140000.0f 0.0f) physicsId world
                just world
            else just world
        | Nop -> just world

    // here we describe the content of the game including elmario, the ground he walks on, and a rock.
    override this.Content (_, _) =
        [Content.screen Simulants.Screen.Name Vanilla []
            [Content.group Simulants.Group.Name []
                [Content.sideViewCharacter Simulants.Elmario.Name
                    [Entity.Position == v3 0.0f 0.0f 0.0f
                     Entity.Size == v3 108.0f 108.0f 0.0f]
                 Content.block2d "Ground"
                    [Entity.Position == v3 -384.0f -256.0f 0.0f
                     Entity.Size == v3 768.0f 64.0f 0.0f
                     Entity.StaticImage == asset "Gameplay" "TreeTop"]
                 Content.block2d "Rock"
                    [Entity.Position == v3 320.0f -192.0f 0.0f
                     Entity.Size == v3 64.0f 64.0f 0.0f
                     Entity.StaticImage == asset "Gameplay" "Rock"]]]]

First we see something new – an explicit simulant reference for our main entity, Elmario –

// this module provides global handles to the game's key simulants.
// having a Simulants module for your game is optional, but can be nice to avoid duplicating string literals across
// the code base.
[<RequireQualifiedAccess>]
module Simulants =

    let Screen = Nu.Screen "Screen"
    let Group = Screen / "Group"
    let Elmario = Group / "Elmario"

This simply allows us to refer to our simulants from multiple places in the code without duplicating their address names.

Also new here is the use of our game physics system. Because the physics here update the engine state (yet still in a purely functional manner), Elm-style commands are used rather than messages.

// this is our Elm-style command type
type Command =
    | Update
    | Jump
    | Nop
    inherit Nu.Command

Here we have three commands, one to update the character for left and right movement, one to make him jump, and Nop, which is a command that doesn’t do any operations but does allow us to make bindings that may or may not result in a command (as we’ll see below).

// this is our Elm-style game dispatcher
type ElmarioDispatcher () =
    inherit GameDispatcher<unit, Message, Command> (())

Since we don’t need a model this time, we simply pass unit for the first type parameter. And since we’re using just commands (no messages this time), we pass the empty Message type for the second type parameter and Command for the third type parameter.

Next up, we will use Nu’s ability to define properties explicitly instead of from simulant property lists –

    // here we define the game's properties and event handling
    override this.Initialize (_, _) =
        [Game.UpdateEvent => Update
         Game.KeyboardKeyDownEvent =|> fun evt -> if evt.Data.KeyboardKey = KeyboardKey.Up && not evt.Data.Repeated then Jump else Nop]

Let’s break each part down –

        [Game.UpdateEvent => Update

The first initializer creates an Update command every frame (when the Game simulant itself is updated).

         Game.KeyboardKeyDownEvent =|> fun evt -> if evt.Data.KeyboardKey = KeyboardKey.Up && not evt.Data.Repeated then Jump else Nop]

The second initializer creates either a Jump or Nop command depending on the state of the keyboard keys as described by its lambda expression.

Let’s take an overview of the this.Command function that handles these commands (with some code snipped for brevity) –

// here we handle the Elm-style commands
override this.Command (_, command, _, world) =
    match command with
    | Update -> // snipped here...
    | Jump -> // snipped here...
    | Nop -> just world

Commands are different from Messages in Nu. While Messages can transform the simulant’s Model value, the Command can transform the World itself. Thus, a Command can effectively ‘side-effect’ the world whereas Message cannot. Think of it as a way of controlling effects.

Anyways, let’s look in detail at the Update command handler –

    | Update ->
        let physicsId = Simulants.Elmario.GetPhysicsId world
        if World.isKeyboardKeyDown KeyboardKey.Left world then
            let world =
                if World.isBodyOnGround physicsId world
                then World.applyBodyForce (v3 -2500.0f 0.0f 0.0f) physicsId world
                else World.applyBodyForce (v3 -750.0f 0.0f 0.0f) physicsId world
            just world
        elif World.isKeyboardKeyDown KeyboardKey.Right world then
            let world =
                if World.isBodyOnGround physicsId world
                then World.applyBodyForce (v3 2500.0f 0.0f 0.0f) physicsId world
                else World.applyBodyForce (v3 750.0f 0.0f 0.0f) physicsId world
            just world
        else just world

All this code does is look at the state of the keyboard and determine whether or not to apply a linear force to the physics body correlated with the entity from which the PhysicsId is found, as well as how much force depending on whether the physics body is on the ground or in the air. It is necessary to put this type of code inside of Command because the application of physical force is a ‘side-effect’ on the World.

Additionally, note that when dealing with physics-driven entities, we usually prefer to move them by applying forces rather than setting their position directly.

        | Jump ->
            let physicsId = Simulants.Elmario.GetPhysicsId world
            if World.isBodyOnGround physicsId world then
                let world = World.playSound Constants.Audio.SoundVolumeDefault (asset "Gameplay" "Jump") world
                let world = World.applyBodyForce (v3 0.0f 140000.0f 0.0f) physicsId world
                just world
            else just world

Jumping is done similarly, but we also tell the engine to play a jump sound when the entity performs his acrobatic feat.

Having studied the code for previous example, Nelmish, the rest of the code for Elmario should be self-explanatory.

Zoom Out

So that wraps up the introductory explanation, but let’s zoom out to add some interesting conceptual detail. In Nu, unlike Elm, this approach is fractal. Each simulant, be it a Game, a Screen, a Layer, or an Entity, is its own self-contained Elm-style program. In this way, Nu is perhaps more sophisticated than Elm — it’s more like a hierarchy of dynamically-configured Elm programs where each one can be individually loaded by the game as needed, and configured by external programs such as Gaia, the real-time world editor. This gives Nu an additional level of pluggability and dynamism that is required for game development.

Nu’s Elm-style / MVU architecture is a great new way to build games. By leveraging this powerful architecture, we turn game development from being big-ball-of-mud OO nightmare into a task that is fun again!

Likely Questions

Let’s wrap up with some questions that people might be likely to ask about this approach –

Q. “Why not just use Elm to make games?”

A. Well, unfortunately, Elm isn’t really set up for that. Unlike Nu, Elm does not integrate a fast, imperative physics engine. Elm doesn’t include a WYSIWYG editor like Gaia. Elm doesn’t have a game-centric asset pipeline. Elm was not built to scale in the way that a modern game engine must. There are so many game-specific tasks that an engine needs that are entirely out of the scope of Elm’s intended use case. In short, Elm wasn’t designed to build games — but Nu was.

Q. “What about the performance of using this high-level programming interface?”

A. Nu’s Elmish implementation is so fast that it really should handle about anything you throw at it save for things that need to go into an ECS. Even bullets in a bullet hell shooter should be workable with Nu’s declarative Elmish API. However, you can always utilize Nu’s classic API for when you need a slight speed-up, or again, its built-in ECS. This is great because you get simplicity-by-default while being able to opt-in to additional scalability where you need it. And that’s what functional programming is all about!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: