Day 3: It’s All a Game

When I was in high school, I made extra spending money by writing games. We basically painted one screen after another in a loop, and the game went as fast as your hardware. The more complex the game, the slower it ran. These days, writing a game using conventional technologies is much tougher. The processors are much faster, so you need to spend more time dealing with timing and state. I quit writing games when I was in college. It just got too hard to crank out a game in an afternoon.

Until now. The game I wrote for this chapter is the first I’ve written in 20 years. The experience has been incredibly rewarding. The flow of this section is going to be a little different than most of the others in this book. I’m going to show you what a game skeleton looks like, we’ll invent a game concept, and then we’ll work through one giant example, under 200 lines long, piece by piece. When we’re done, you will have a working game that you can customize. Hopefully, Elm will spark a new wave of game designers.

First things first, though. We need to start with a basic shell.

Describing a Skeleton

You’ve already seen how animation, user input, and graphics display work in Elm, so you probably know at least a little about what a game will look like. The basic strategy will be to build one time slice of the game. Then, we’ll work on moving the game from one time slice to the next, based on user input. Using that strategy, all basic games will have the same basic components:

Keep in mind that all games have to do this work. It’s just harder to express games in many other languages because the abstractions are not as clean. Let’s take a look at a basic skeleton of our game, without any specifics attached.

Defining the Model

Here’s the skeleton in code. As with most games in Elm, I’ve based the initial design on Evan’s excellent game skeleton.[61] It’s a free, open source project that you can use to get started. Alternatively, you can check out the many game examples at the Elm language site.[62] Let’s check out my revised skeleton, piece by piece.

elm/skeleton.elm
 
module​ ​SomeGame​ ​where​...
 
 
type​ alias ​Input​ = { ... }
 
type​ alias ​Player​ = { ... }
 
type​ alias ​Game​ = { player:​Player​, ... }

First, we model the game. Elm games are modules. Inside these you build a model of your game with simple data types. You can combine those simple types into higher level types. Generally, your model will represent the player, other computer-controlled elements, game state, and user inputs at one point in time. You’re looking to collect everything you will need when it comes time to display the game, or to transition from one slice of time to the next.

Looping with Signals and foldp

Next, define a couple of signals. One will grab the user inputs we need, and the other will build each time slice based on the last one. As you can imagine, we’ll use map for the input and foldp to move from one slice to the next.

elm/skeleton.elm
 
delta = inSeconds <~ fps n
 
input = sampleOn delta (...)
 
main = map display gameState
 
gameState = foldp stepGame initialGameState input

delta is a signal (using the operator equivalent of map, <~) that represents one slice of time. fps is a signal that means “frames per second.” We’re left with a signal that regularly updates every n seconds. Just like that, Elm has taken over the sensitive game timing so our math can focus on one point in time.

We then build another signal based on that one called input, which will capture all of the user input we need. gameState is a signal that builds the next state of the game based on user inputs and the previous game state. foldp is the perfect choice to do this work because it allows us to use the previous state in our definition of the next game.

main, then, needs only animate the game. That’s easy since we can map the gameState signal onto display. This code has a lot going on. Feel free to spend a little time with it to make sure you understand what’s going on.

Stepping and Displaying the Game

Of course, stepping from one state to the next and displaying our game are the essence of our game and should command most of our attention. In Elm, they do. Further, both stepGame and display are simple functions that operate on one flat slice of time. We don’t have to worry about input and output beyond its state at any instance of time. The framework lets us represent what is often the most difficult part of the game with a trivial four lines of code. Fantastic!

Describing Language Head

images/src/elm/game_start.png

Before we start to code, we need to know one more thing. What are the rules of the game?

A strange language deserves an equally twisted game. We’re going to build a game called Language Head. The object of the game is to bounce some balls across the screen, without letting each hit the ground. When a ball hits the ground, the game ends. The player scores by staying alive and successfully getting a ball across the screen with a paddle they’ll move with their mouse. There’s a black line for a paddle on the bottom, a score on the upper right, and a primitive background, including a red rectangle representing a building on the left, and a gray area that will be our road on the bottom.

There’s a twist. We’re going to put the pictures of people on each ball, and call them heads. We’ll drop more and more of them as the game goes on.

I’m going to break protocol for this chapter. Rather than work on this project iteratively, I’m going to tell you about each part of the completed game. Then, we’ll talk about how to run the program, and you can take a well-earned break to play the game. Some of the parts will be a little long, but that’s OK. We’ll carve those long examples into shorter sections so you’ll be able to see what’s going on.

Modeling Language Head

Let’s look at the first code section: the module definition and imports.

elm/game/languageHeads.elm
 
module​ ​LanguageHead​ ​where
 
 
import​ ​Color​ exposing (..)
 
import​ ​Graphics​.​Collage​ exposing (..)
 
import​ ​Graphics​.​Element​ exposing (​Element​, image, leftAligned)
 
import​ ​Keyboard
 
import​ ​List​ exposing ((::), all, filter, length)
 
import​ ​Mouse
 
import​ ​Random​ exposing (​Seed​, generate, initialSeed, int)
 
import​ ​Signal​ exposing (​Signal​, (<~), (~), foldp, map, sampleOn)
 
import​ ​Text​ exposing (color, fromString, height, monospace)
 
import​ ​Time​ exposing (​Time​, every, fps, inSeconds)

Longer Elm programs are broken down into modules. This module is called LanguageHead. We need mouse input for the paddle and keyboard input to capture the spacebar to start the game. We will also need a random signal to choose which head to present and some graphics modules to display our game. All of the code for this whole game is in a single module.

Let’s take a look at the data models.

elm/game/languageHeads.elm
​ 
type​ ​State​ = ​Play​ | ​Pause​ | ​GameOver
 
 
type​ alias ​Input​ = { space:​Bool​, x:​Int​, delta:​Time​ }
 
type​ alias ​Head​ = { x:​Float​, y:​Float​, vx:​Float​, vy:​Float​, img:​String​ }
 
type​ alias ​Player​ = { x:​Float​, score:​Int​ }
 
type​ alias ​Game​ = { state:​State​, heads: ​List​ ​Head​, player:​Player​, seed: ​Seed​ }
 
​ 
defaultHead n = {x=100.0, y=75, vx=60, vy=0.0, img=headImage n }
 
 
defaultGame = { state = ​Pause​,
 
heads = [],
 
player = {x=0.0, score=0},
 
seed = initialSeed 1234 }
 
 
headImage n =
 
if​ | n == 0 -> ​"/img/brucetate.png"
 
| n == 1 -> ​"/img/davethomas.png"
 
| n == 2 -> ​"/img/evanczaplicki.png"
 
| n == 3 -> ​"/img/joearmstrong.png"
 
| n == 4 -> ​"/img/josevalim.png"
 
| otherwise -> ​""
 
 
bottom = 550

This listing defines data types that describe our world. We first define data types, and then we declare some data that will come in handy when we introduce new data to the game. Let’s look at it piece by piece.

In short, we have an overarching Game model that contains the game State, user Input, all of the Heads, and the Player. The State is a primitive data type alias with the states we’ll need: Play, Pause, and GameOver. The game will behave differently in each of these states. We use type aliases because we’ll need to reuse these definitions throughout our game.

The Player and Game types are pretty simple, but the Head type needs a little more explanation. We need to save not just the x and y coordinates (these are on a 800×600 grid, with the origin anchored on the top left), but also the velocity that the heads are moving across both dimensions. The y velocity will change over time to simulate gravity, flipping when any head bounces. We’ll need this velocity as we step the game. We will also assign a random head image in img.

In this section, we’re done with type aliases. We are building functions to return actual type for the initial state. defaultGame is simple as expected, but the heads have to have more logic built in because we are going to move them around. We define a default head. We include its starting coordinates, and set the vx to a constant. Ignoring the laws of physics, our heads will keep a constant x velocity (vx) because I am not smart enough to calculate wind resistance across the wide variety of hairstyles our heads could have. I’m looking at you, Evan. Our vy values start with a velocity of zero, but that will pick up once our artificial gravity kicks in.

Now that we’ve defined the model, it’s time to define the signals that will drive our game.

Building the Game Signals

In this section, we handle all of the timing, the different user input states, the speed of the game, and the details that hold the application together from frame to frame. It’s also the shortest section of our entire example. You can probably see where this is, um, headed. Elm is going to handle these details through signals. We’ll just need to provide a little glue.

elm/game/languageHeads.elm
 
secsPerFrame = 1.0 / 50.0
 
delta = inSeconds <~ fps 50
 
input = sampleOn delta (​Input​ <~ ​Keyboard​.space
 
~ ​Mouse​.x
 
~ delta)
 
main = map display (gameState)
 
gameState = foldp stepGame defaultGame input

We first set the speed of the game. We define a function called secsPerFrame to return the size of each time slice. Next, we build a delta signal that updates 50 times per second. The <~ operator is shorthand for map, so you could write inSeconds <~ fps 50 as map inSeconds (fps 50). That means we’re just going to get a float representing the number of seconds that have actually passed.

Next, we build our input signal. You’ll see a new operator, the ~. (f <~ a ~ b) is equivalent to (map2 f a b). Think of the squiggly arrow as signals flowing into the function. Using that operator, we pick off the various elements of the Input type, whether the spacebar is pressed, the x position of the mouse, and the total amount of time that’s passed in this slice. We’ll sample 50 times a second, based on the delta signal.

Finally, all that remains is to build our foldp loop. This recursive loop will build each successive Game based on the previous Game slice and user inputs. You can see that we’re following the skeleton quite closely. The bulk of the code manages the creative side of the game, stepping and displaying each element. Herd on, Babe!

Stepping the Game

The trickiest part of this game is to manage all of the moving parts. We’ll break this process into three major parts:

There’s a lot going on here, but the code is remarkably concise because we don’t have to worry about timing, animation, or managing user input.

elm/game/languageHeads.elm
​ 
stepGame input game =
 
case​ game.state ​of
 
Play​ -> stepGamePlay input game
 
Pause​ -> stepGamePaused input game
 
GameOver​ -> stepGameFinished input game
 
​ 
stepGamePlay {space, x, delta} ({state, heads, player, seed} as game) =
 
let​ (rand, seed') =
 
generate (int 0 4) seed
 
in
 
{ game | state <- stepGameOver x heads
 
, heads <- stepHeads heads delta x player.score rand
 
, player <- stepPlayer player x heads
 
, seed <- seed' }
 
 
stepGameOver x heads =
 
if​ allHeadsSafe (toFloat x) heads ​then​ ​Play​ ​else​ ​GameOver
 
 
allHeadsSafe x heads =
 
all (headSafe x) heads
 
 
headSafe x head =
 
head.y < bottom || abs (head.x - x) < 50

Though we break it down into several different functions, stepGame is just a function. We take the input and game type aliases. We use case to branch on game.state, calling a function to step each possible game state.

The first such function is stepGamePlay, which steps the game in Play mode. We update the game structure, calling a function to build each element of the game structure. stepGameOver will tell us if a head has crashed, stepHeads will manage any changes in the heads, and stepPlayer will handle changes in the paddle position and score.

The game is over when we experience a cranial catastrophe, meaning one head reached the bottom without a paddle. stepGameOver, then, is easy to write. We call a function called allHeadsSafe to see if any heads have reached the bottom without a paddle. That function will be true if headSafe head is true for every head in heads. headSafe needs only check to see if a single head has reached the bottom without the paddle close by (abs (head.x - x) < 50).

Now, we know enough to tell whether the heads are safe, so we can successfully transition to GameOver at the right time. Note that we don’t care about any animation—we just check to see if all heads are safe at this point in time.

The next step is to move the heads according to the rules of the game. There are several steps to that process:

​ 
stepHeads heads delta x score rand =
 
spawnHead score heads rand
 
|> bounceHeads
 
|> removeComplete
 
|> moveHeads delta
 
​ 
spawnHead score heads rand =
 
let​ addHead = length heads < (score // 5000 + 1)
 
&& all (\head -> head.x > 107.0) heads ​in
 
if​ addHead ​then​ defaultHead rand :: heads ​else​ heads
 
​ 
bounceHeads heads = ​List​.map bounce heads
 
 
bounce head =
 
{ head | vy <- ​if​ head.y > bottom && head.vy > 0
 
then​ -head.vy * 0.95
 
else​ head.vy }
 
​ 
removeComplete heads = filter (\x -> not (complete x)) heads
 
 
complete {x} = x > 750
 
​ 
moveHeads delta heads = ​List​.map moveHead heads
 
 
moveHead ({x, y, vx, vy} as head) =
 
{ head | x <- x + vx * secsPerFrame
 
, y <- y + vy * secsPerFrame
 
, vy <- vy + secsPerFrame * 400 }

The stepHeads function needs several arguments to do the entire job. The whole function is a function pipe, rolling the result of each function into the next. The result is a clear, concise representation of the data. We need to add heads when it’s time with spawnHeads, bounce the heads when they reach the bottom with bounceHeads, remove heads that reach the right side of the window with removeComplete, and move the heads according to the rules of the game with moveHeads.

We’ll need to make sure the game has enough heads. addHead is a formula based on the score that determines how many heads are on the display. We add a head if there are not enough heads yet.

Heads bounce when they get to the bottom, if they haven’t already bounced. To bounce a head, we just make vy, the y velocity, negative if it’s on the bottom. We also multiply by 0.95 when we bounce, so each bounce doesn’t go quite as high as the last. It’s a nice touch that looks a little more realistic.

We remove all heads that are complete. A head is complete once it’s reached the right-hand side, or head.x > 750.

Each head has to move. We move the head in each direction based on the velocity per second, times the length of one time slice. We also adjust the y velocity to build in our gravity.

That’s all there is to the head movement. We just adjust the next head list based on the previous list and the rules of the game. Next, we’ll step the player data. We’ll need to update the score and the paddle position.

elm/game/languageHeads.elm
​ 
stepPlayer player mouseX heads =
 
{ player | score <- stepScore player heads
 
, x <- toFloat mouseX }
 
​ 
stepScore player heads =
 
player.score +
 
1 +
 
1000 * (length (filter complete heads))

Stepping the Player is comically simple. We just return a new player with the stepped score, and we capture the mouse position as a float. The float conversion will make it easier to display the paddle later.

Our scoring system is simple. We give the player a point for each time slice and 1000 points for getting a head across the screen.

That’s all for stepping the player. That was almost too easy. Let’s finish up stepGame next. We can write the functions that step the game when it’s in the Pause and GameOver states.

elm/game/languageHeads.elm
​ 
stepGamePaused {space, x, delta} ({state, heads, player, seed} as game) =
 
{ game | state <- stepState space state
 
, player <- { player | x <- toFloat x } }
 
​ 
stepGameFinished {space, x, delta} ({state, heads, player, seed} as game) =
 
if​ space ​then​ defaultGame
 
else​ { game | state <- ​GameOver
 
, player <- { player | x <- toFloat x } }
 
​ 
stepState space state = ​if​ space ​then​ ​Play​ ​else​ state

A game in the Pause state will need to step the state based on the space bar so players can start the game, and also update the paddle position so the player can move the paddle even if the game is paused.

A game in the Finished state needs to reset to a defaultGame when the user presses the spacebar, or just replace the player’s mouse position.

Stepping the state involves simply transitions to Play when space is true.

Let’s review what happened here. We used signals to allow us to grab the current user inputs we needed: the size of our time slice, the mouse x position, and whether the spacebar was pressed. We packaged those up in an Input data type. Then, we passed that input and the Game record we produced in the previous time slice into stepGame. Based on that data and the rules of the game, we built a new Game record.

Next, we can display the Game record and then we can let the heads fly.

Displaying the Game

We’re going to use many of the same techniques you learned in Day 2 to display the Game record we produced in Day 1. The code looks a lot like you’d see in any graphics library:

elm/game/languageHeads.elm
​ 
display ({state, heads, player, seed} as game) =
 
let​ (w, h) = (800, 600)
 
in​ collage w h
 
([ drawRoad w h
 
, drawBuilding w h
 
, drawPaddle w h player.x
 
, drawScore w h player
 
, drawMessage w h state] ++
 
(drawHeads w h heads))
 
​ 
drawRoad w h =
 
filled gray (rect (toFloat w) 100)
 
|> moveY (-(half h) + 50)
 
 
drawBuilding w h =
 
filled red (rect 100 (toFloat h))
 
|> moveX (-(half w) + 50)
 
​ 
drawHeads w h heads = ​List​.map (drawHead w h) heads
 
 
drawHead w h head =
 
let​ x = half w - head.x
 
y = half h - head.y
 
src = head.img
 
in​ toForm (image 75 75 src)
 
|> move (-x, y)
 
|> rotate (degrees (x * 2 - 100))
 
​ 
drawPaddle w h x =
 
filled black (rect 80 10)
 
|> moveX (x + 10 - half w)
 
|> moveY (-(half h - 30))
 
 
half x = toFloat x / 2
 
​ 
drawScore w h player =
 
toForm (fullScore player)
 
|> move (half w - 150, half h - 40)
 
 
fullScore player = txt (height 50) (toString player.score)
 
 
txt f = leftAligned << f << monospace << color blue << fromString
 
​ 
drawMessage w h state =
 
toForm (txt (height 50) (stateMessage state))
 
|> move (50, 50)
 
 
stateMessage state =
 
if​ state == ​GameOver​ ​then​ ​"Game Over"​ ​else​ ​"Language Head"

First, we write the main display function. This function draws a collage with parts we build in other functions. We’ll draw the building on the left, the road on the bottom, the paddle, the score, a message, and all of the heads. To keep this code short, we are hard-coding the display size to 800 by 600, but it’s possible to use map2 to use both the Window.dimensions signal and the gameState signal at the same time.

The collage takes forms that originate in the center of the canvas. You’ll move each element to where you want it after you create it.

The background elements are simple. The building is just a vertical rectangle that we move to the left, and the road is a horizontal rectangle that we move down.

The drawHeads function just maps the drawHead function onto the heads list. Remember, collages take shapes called forms. Since heads are images, we need to reference the head’s source image (you’ll have to copy them from the book’s source code), and convert that image to a form. Then, we do a little math to make sure the heads move to the right form on the page. A collage anchors the origin at the bottom left, so we do need to reverse the y position. Also, since the heads are initially drawn in the center of the canvas, we need to adjust for that with our move function. For good measure, we rotate the head based on the x coordinate. I hope Joe doesn’t get too dizzy.

Paddles are just rectangles, moved to the bottom of the canvas, and adjusted for the Mouse.x position and the central location on the page.

Working with text in Elm is a little tricky. We have a couple of conversions to do. We need to make sure we’re working with text elements, and we need to convert those to forms. I’m not going to go into too much detail here, because it deals with many data types we’ve not yet introduced. At a high level, these functions convert strings to text objects with the font, color, and size that we want. The txt function lets us apply uniform, common transformations to the text. Then, they translate that text into forms that will work in our collage.

The last element to display is a message for the game. We’ll show either the string "Language Head" or the string "Game Over", based on the game state.

And that’s all there is to it! To start the game, put your source code in a game directory. Also in that directory, put an img directory with all of the head images you referenced in the source. If you’d like to use our heads, you can copy them from the book’s source code (see the Online Resources section of the Preface). Finally, navigate to your game directory and start your local Elm server, like this:

 
> elm-reactor
 
Elm Reactor 0.3.2 (Elm Platform 0.15.1)
 
Listening on http://0.0.0.0:8000/
images/src/elm/language_heads.png

Then press the spacebar to start the game! You’ll see something like this image.

There you have it. We wrote a full game in fewer than 150 lines of code. The design also allows us to add many different bells and whistles without customary callbacks and the hallmark complexities of JavaScript approaches.

What We Learned in Day 3

Day 3 showed a single extended example of using Elm to build a game. We chose this problem because it wraps up many of the most demanding problems in user interface design. This example includes interacting with the mouse and keyboard; working with text and images; using animation, including simulated gravity; image presentation and manipulation; and more. By shaping the game with signals and functions, Elm allowed us to live in the realm of functions.

Elm’s structure let us simplify the most sophisticated problems, like the interplay between objects on the screen, scoring, and our simulated physics. If Elm can handle games with such grace and dexterity, other user interface problems should be a breeze.

Your Turn

Find…

Do (Easy):

Do (Medium):

Do (Hard):