The other night at NashFP I did a whirlwind tour of a game I wrote, PewPew (play it!). While I was going over the source code with the group, I realized that I learned quite a bit while doing this and needed to share.
About 4 months ago I had just launched the new website for Firefly Logic. I love websites with awesome 404 pages. That little bit of detail is just icing on the cake - it's how you know that someone behind it all gives a damn. What better way to solidify our nerdiness than to write a small game to play? I wanted people to know that our site was loved and so I pitched the game idea to the people who sign my paycheck: "I want to write a game for our 404 page, you cool with that?" They agreed and I went to work.
Elm is based on the paradigm of functional reactive programming (FRP). In other languages I've worked with, user inputs are modeled as events. We're all used to the pattern at this point: register for what events we care about and provide some callback. Then later in the app our callback gets invoked and we must reach into some shared state to see if we can even accept the user input and then write back into that state some new data. It's callback spaghetti and debugging it is hell. FRP is very different; instead of treating user inputs as some blip in time, they are modeled as a value that changes over time. That model is referred to as a signal. Signals aren't limited to only user inputs - time can also serve as a signal in terms of elapsed time or frames per second. The latter of which serves drives the game and replaces the traditional game loop.
This is the first game I've ever built. This took about 9 days from start to finish and that includes my time learning Elm on the fly. Most of the Elm examples are all in a single file. I started out this way too, but quickly started having trouble keeping up with what went where. Then that feeling started to creep in that I was introducing too much coupling in places, so I started hacking bits out and moving them to separate modules. I feel okay about how the code organization shook out, but looking back at it now I can see some places where there is opportunity for improvement. Here's a quick rundown of the modules:
- Entry - There's nothing too spectacular going on here. We create a level and initialize our game state with that level. I think the most interesting bit here is the
foldpwhich is essentially a left fold over time. That turns your input into a state signal which we finally feed into the renderer.
- Inputs - This game has very simple inputs: left/right to move and space to fire. On the last day I added touch controls (the
touchMovefunctions) and used the
mergefunction to merge the keyboard and touch signals into one. The resulting touch compatible version required no modifications to the game which I thought was awesome. One last note, the final signal is a combination of all of the above plus time sampled at 60 fps. This signal becomes our game loop that drives the
foldpin our entry point above.
- Step - This is where all of the logic for the game lives. Every tick of the game runs this function which advances game objects: ship, enemies, projectiles, and explosions. Here is where we keep track of scoring by doing (very crude) collision detection and then removing those objects from our state that have been destroyed or have flown off screen. The entire job of this module is to take the current inputs + past game state and produce a new game state.
- View - The view ends up being very straight forward. This module takes a game signal and produces the UI. No tricky business - just render exactly what the state of the game is. It was challenging for me because I was having a hard time overcoming prior web experience with layout. Elm uses Cartesian coordinates and that took a bit to adapt to. I also lost a bit of time to the
encodeURImethod on the windows. In the end I just pre-encoded it and moved on.
- Level - This is the level generator. It would have taken me forever to get the enemy layout right by hand, so I modeled the level as a bit of ascii (art?) and then parsed that into enemy coordinates. This allowed me to try several configurations and enemy sizes without reworking a whole bunch of manual coordinates entries. Spoiler Alert: There's only one level.
- Model - The data structures that make up the game state.
- Utils - The dreaded "I don't know where this stuff goes" module.
throttlewas an experiment in controlling firing rates of the ship at the signal level. Later in the game I modeled the enemy firing rates inside the game state to see what that would look like too. After trying both, I still don't know which place is the most sensible place to manage this.
This was a fun step up above the basic exercises you typically do when learning a new language. If you haven't looked at any functional languages, I encourage you to do so. Elm would be an excellent first functional language because it's very interactive. It's also pure which forces you down a path without mutability so you can avoid the temptation to fallback to your old object oriented habits. Even if you won't be using Elm day to day, learning functional programming will make your other code better. Give it a try!