A Game Engine that Allows You to Program 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 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.
Because it is simpler, let’s start by looking at the canonical Elm-style UI example in Nu -
The full code is as follows -
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 -
Here we specify the handles of each simulant used in the program, allowing us to refer to them in separate places of the program without having to copy and paste strings everywhere. The simulant hierarchy established here is -
Next, let’s look at the Model.
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 sting instead of an int. You’re not limited to primitive types, however — you may make your model type as sophisticated as you see fit.
Next 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 code does three things -
- 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 Gaia.
- It allows the user to specify the Elm-style model, message, and command types. Here we pass unit for the command type since this simulant doesn’t utilize commands.
- The base constructor function takes as a parameter the initial Model value (here, 0).
Here we have the bindings that determine how each engine event is wired up to send messages to the Message function. We can also wire events to the Command function by using the alternative =>! operator (tho again, this isn’t shown in this example).
Next 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 commands for processing by the unused Command function. But since we’re not using commands here, I use the just function to automatically pair the changed model value with an empty command list. It’s just a little bit of syntactic sugar to keep things maximally readable!
Finally, 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. Remember our hierarchy from above -
The Content function declares that the above hierarchy is instantiated at run-time.
Each Content clause can also define its respective simulant’s properties in a declarative way. Here we have the DecrementButton’s Text property defined as “-”, and its position translated up and to the left.
Here we see how the CounterText derives its Text property by mapping the model with the scstring function. scstring is just a special string conversion function and in functional terms the → operator maps the right-hand side function over the model.
And lastly, here we have the special entityIf function that defines an entity that exists only while the given predicate is satisfied. In this case, the predicate is (model → isNonZero). So, as long as the model value is non-zero, the entity will exist. Otherwise, it will not. The engine takes care of creating and destroying the entity accordingly.
In detail, the entityIf function is implemented in terms of the entities function. The entities function defines a list of entities that each exist so long as they have a corresponding mapping from the model. In this way, the Content function is like Elm’s View functions — simulants are automatically created and destroyed by the engine according to whether or not they map from the model. It’s quite sophisticated and in my opinion, one of the best features to steal from Elm!
Now let’s look at Elm-Mario in Nu -
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 -
Because the physics here update the engine state (yet still in a purely functional manner), Elm-style commands are used rather than messages.
Here we have three commands, one to move the character left, one to move him right, and one to make him jump! Oh, and then there’s 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).
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 unit for the second type parameter and Command for the third type parameter.
The first binding we encounter is one that updates every frame. It checks if the left or right arrow keys are down and, if so, issues a command to walk in that direction (otherwise, Nop).
The second binding works instantaneously on a key down event. When the up arrow key is pressed, the Jump command is issued.
When we encounter the MoveLeft command, we don’t actually move the character by setting its position. For a physics-based entity like the one we have here, that position would only be overridden by the physics system. Instead, when we have a physics-based object, we must issue apply forces to the physics body that correlates with its physics ID. So first, we get the entity’s physics ID, and then we apply a leftward force vector by calling World.applyBodyForce passing the physics ID. Note that we check to see if the physics body is on the ground, and if it is, we apply more force than if it were in the air. This is to limit how much movement can take place while jumping (just like an actual Mario game!)
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.
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.
Iterative Functional Reactive Programming (iFRP) is a great new way to build games, and the new Elm-style architecture makes it even easier to use! By leveraging these powerful new tools, we turn game development from being big-ball-of-mud OO nightmare into a task that is fun again!
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 (and perhaps cannot) integrate a fast, imperative physics engine. Elm doesn’t include a WYSIWYG editor like Gaia. Elm doesn’t have a game-centric asset pipeline. 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 is.
Q. “Can someone build a similar Elm-style programming API for Unity?”
A. The answer is firm ‘No’. In order to expose an Elm-style interface, you need some form of FRP on the back-end. As a matter of practicality, Unity is not equipped to do FRP in any form because its Entities do not expose the low-level property events required to form the basis of an FRP back-end. Unlike Unity — and all other existing commercial game engines, an FRP backend (specifically, iFRP) was built directly into Nu as one of its primary features.
Q. “What about the performance of using this high-level programming interface?”
A. Well, it depends on what you’re using it for. When you’re writing high-level simulants like UI controls, Character Controllers, Game, Screen, and Layer simulants, this is a sufficiently performant approach in all likely cases. Where it is not sufficiently performant, however, is when encoding things like bullet entities in a bullet hell shooter. The Elm-style API is a good approach by default, but when you need simulants that appear in the thousands, you’ll be using a more low-level approach. That’s good though, because you get simplicity-by-default — and complexity only as you need it. That’s what functional programming is all about, IMO!