Is Elm Effective for Games?
I’ve been participating in game jams regularly over the past two years to rekindle my love for software development. After all, wanting to make my own games was why I bothered to learn programming at all. Quite honestly, it’s been a blast. It demands a lot more creativity and technical knowledge than any job or academic program ever has of me. It allows me to work with graphics and audio, stitching it all together with software that delights (or at least ought to). It forces me to stop thinking about the perfect and the logical, because more often than not, it’s easier to just e.g. hide something and reposition it on relevance rather than to spawn it when it becomes important. Like animation, game design is a set of tricks to simulate a logically consistent environment and its representation.
Unfortunately this can run afoul of purity in some functional programming languages. Owing a lot more of their design and philosophy to the perfect world of pure math than the imperfect world of discrete bits, there are caveats to using them for game development. There are also, some amazing benefits.
The best parts
The Elm framework is amazing for interactive visual content like games. Its approach to the event loop is well attuned to what a reactive game loop must achieve. Setting up callbacks for animation frames, for mouse or keyboard input, and for network responses is so easy and so elegant.
Functional programming, with the lowercase f, as in programming dealing with functions as first class citizens, helps with writing very concise code, especially in regard to game programming. Data structure processing with the map, fold and filter functions makes it very easy to mass process entities and graphical assets.
Elm has some absolutely great libraries. Interoperable libraries for units and geometry are incredible for avoiding certain glitches (especially as regarding unit conversion from world units to screen units, rates of speed to time or distance, etc). It also has a quite enviable testing library, at least compared to anything I’ve found in the JavaScript world. Being able to design fuzzers that return arbitrary game states has helped a lot in squashing bugs.
With its pipeline operator |>
(and tacit >>
), Elm is well-suited to railway oriented programming, which in some parts of the game stack, is very convenient. Being able to design small APIs based on chain sequential success-fail calls has proven very useful for organizing code. My favorite example of this is game over condition checks, e.g.
checkForGameOver state =
state
|> startGameOverCheck
|> didShipCollideWithEnemy
|> didShipDefeatBoss
|> evaluateToGameOver
In this example, startGameOverCheck
packages state in a way that the following functions use to first access the state to evaluate their check, and second to return so that the next check either knows it doesn’t need to run any expensive checking code because the game state doesn’t permit that check to be true, or it maintains access to the state to run its own checks. The final function responds similarly to checkers, and if it’s passed a state marked game over it will set up ending triggers and score reporting, and if it’s just passed a plain state it knows the game continues as played.
The strong type system helps with documentation and error prevention. I’ve worked places where they were proud of their not having any documentation. I did not work there long in part because documentation is a valuable thing to have, especially when onboarding new employees! But most places just don’t have a lot of documentation, because the code evolves, documentation rots, and it’s a challenging cycle to keep up with. Game jams are like this except they’ve been freebasing. By the end of the jam probably 30% of my code is dead. Writing documentation is a waste of time but having those type annotations, as enforced by the compiler, helps prevent dead code and changing requirements from introducing many simple bugs.
The bad parts
Elm is a bit slow on the inclusion of web standards. The purpose is to maintain a stable language and standard library, but it can be tedious not to have access to certain web APIs, for example Web Audio or the Gamepad API, without having to resort to ports.
Elm’s approach to random values is also quite strange! I won’t yet say it’s bad, in fact it does have its benefits, and if there were some interop between the Random
and Fuzzer
types, that would make some of my test code much simpler! But at the same time it feels clunky to fetch random values during a game loop, to respond to random values recipience during an event loop, etc. It requires a lot more care about how you know when you need random values, because they can only be requested via Cmd
types and these have to be handled delicately in the update
function of your event loop.
Old patterns do not apply. A lot of key strategies from decades of game development are not very applicable anymore. Data-oriented design is not very effective and can in fact lead to a lot of Maybe/Result checking that obscures code and may even negate the efficiency gains data packing is meant to achieve. I learned this trying to make a rhythm game: so many of the resources on the internet assume access to global IO systems. A state in Elm is going to be very heavy weight compared to that in an imperative language.
The Elm debugger. This thing is so great until the game starts requesting ticks or animation frames. It’s so great until the model is tracking hundreds of data points for the various entities of the game. There comes a point in the development of many games in Elm where this ceases to serve debugging and starts to actively hinder it. Which sucks! Because the debugger, awkward as its interface is, is great for model inspection, for fast-forwarding and rewinding state. I really hope for an update to this tool in the near future.
Animation/cutscenes are really strange to work with. It’s brutally declarative but I almost always end up writing APIs that look imperative:
endingCutscene state =
state
|> moveCharactersToCliff
|> animateSettingSun
|> narrate "The end"
It looks like this moves the characters into position to watch the sunset and then animates the sun going down whereas in fact it is probably actually adding a list of key frames first to the character’s animation timelines to mutate their position and set their animation to the walking frames and then adds a list of key frames to the sun’s animation timeline taking place after that walking animation is complete to change its position. The update function then consumes these as animation frames pass.
Takeaway
I love Elm and I’m going to keep using it for game development. But I do worry that someday, I may have to leave it behind. Fortunately, when that time comes, my options will probably include native/WASM compiled languages. There are plenty of options available for me to choose from there.