This series has two major goals:
In this post we will be looking into Pong in Elm: a functional game written in Elm, playable in any modern browser.
Making games is historically a very imperative undertaking, so it has long missed the benefits of purely functional programming. FRP makes it possible to program rich user interactions without resorting to traditional imperative idioms.
By the end of this post we will have written an entire GUI/game without any imperative code. No global mutable state, no flipping pixels, no destructive updates. In fact, Elm disallows all of these things at the language level. So good design and safe coding practices are a requirement, not just self-inforced suggestions.
Imperative programs allow you to reach into objects and data structures whenever you want, so it is not a huge deal if your code is somewhat disorganized. With functional game design, we must be more careful about how our programs are structured. In fact in Elm, all games will share the same underlying structure.
The structure of Elm games breaks into three major parts: modeling the game, updating the game, and viewing the game. It may be helpful to think of it as a functional variation on the Model-View-Controller paradigm.
To make this more concrete, lets see how Pong needs to be structured:
If you would like to make a game or larger application in Elm, use this structure! I provide both the source code for Pong and an empty skeleton for game creation which can both be a starting point for playing around with your own ideas.
Let's get into the code!
First we have the ever exciting module declaration. We are about to name our module Pong. Whoo!
module Pong where
Next we set our desired frames-per-second (FPS). We'll shoot for 50 FPS. The fps function produces a sequence of time deltas in milliseconds indicating how much time has passed since the previous frame. We then convert those time deltas into seconds so that it is nicer to think about.
delta = lift inSeconds (fps 50)
If someone's machine cannot handle 50 FPS, the updates will happen as quickly as possible within the limits of that machine.
Here we will define the data structures that will be used throughout the rest of the program. This is the foundation of our game, so changes here will likely cause changes everywhere else.
These models are a rough specification for your game. They force you to ask: Which features do I want? What information do I need for those features? How do I represent that information? Once you have figured out the core information needed for your game, you have already done a lot of planning about how everything else will work.
Do not be afraid to spend a lot of time thinking about this!
During gameplay, all keyboard input is about the position of the two paddles. So the keyboard input can be reduced to two directions, each represented by an integer in {-1,0,1}. Furthermore, the SPACE key is used to start the game between rounds, so we also need a boolean value to represent whether it is pressed.
data KeyInput = KeyInput Bool Int Int
defaultKeyInput = KeyInput False 0 0
keyInput = lift3 KeyInput Keyboard.space
(lift .y Keyboard.wasd)
(lift .y Keyboard.arrows)
We now have a concise representation of the keyboard input that updates automatically as new key presses occur.
The inputs to this game include a timestep (which we extracted from JavaScript) and the keyboard input from the users.
data Input = Input Float KeyInput
Combine both kinds of inputs and filter out keyboard events. We only want the game to refresh on clock-ticks, not key presses too.
input = sampleOn delta (lift2 Input delta keyInput)
This signal represents all of the input to our game!
Now we need to model the actual game.
Pong has two obvious components: the ball and two paddles.
data Paddle = Paddle Float -- y-position
data Ball = Ball (Float,Float) (Float,Float) -- position and velocity
But we also want to keep track of the current score and whether the ball is currently in play. This will allow us to have rounds of play rather than just having the ball move around continuously.
data Score = Score Int Int
data State = Play | BetweenRounds
Together, this information makes up the state of the game.
data GameState = GameState State Score Ball Paddle Paddle
This is the core information needed to step from one frame to the next. If you know these facts and have some user input, you know what the next frame should be.
There is other information that is not specific to a particular frame, like the size of our game. It is safe to define these things separately.
gameWidth = 600
gameHeight = 400
halfWidth = gameWidth / 2
halfHeight = gameHeight / 2
Before we can update anything, we must first define the default configuration of the game. In our case we want to start between rounds with a score of zero to zero.
defaultGame = GameState BetweenRounds
(Score 0 0)
(Ball (halfWidth, halfHeight) (150,150))
(Paddle halfHeight)
(Paddle halfHeight)
We now how a full model of Pong and its inputs!
Our GameState data structure holds all of the information needed to represent the game at any moment. In this section we will define a 'step function' that steps from GameState to GameState, moving the game forward as new inputs come in.
You can think of our game as a finite state machine. Here we are defining a transition function that takes an input and a state, and then steps to the next state. If you are not familiar with finite state machines, do not worry about this analogy!
To make our step function more managable, we can break it up into smaller components.
First, we define a step function for updating the position of paddles. It only depends on our timestep and a desired direction (given by keyboard input).
We use the clamp function to keep the paddle within the boundaries of the game. The distance moved is determined in part by delta, the time since the last update. This will make the motion of the paddles look smooth even if updates happen irregularly.
stepPaddle delta dir (Paddle y) =
Paddle $ clamp 20 (gameHeight-20) (y - dir * 200 * delta)
Before we define stepBall we want a couple helper functions to help detect collisions, a surprisingly tricky task, even in Pong.
Since updates may not happen regularly, the ball may move different distances for each update. So imagine that the ball moves into the paddle with a particularly large step. We want to reverse its velocity! So vx = -vx, right? But what if the ball moves out of the paddle with a small step. A step so small that it does not actually make it all the way. That is another collision and we reverse the velocity again! Now we are back where we started! The ball is 'stuck' in a collision.
To be extra careful, we use the makePositive and makeNegative functions to change the velocity of the ball when it hits a wall or paddle. We also use the within function to figure out if x is within epsilon of the number n, helping to detect collisions in the first place.
makePositive n = if n > 0 then n else 0-n
makeNegative n = if n > 0 then 0-n else n
within epsilon n x = x > n - epsilon && x < n + epsilon
The stepVelocity function is used to reverse the velocity of the ball when it has collided with a lower or upper boundary. Notice that the collisions set the direction of the velocity rather than simply negating it!
stepVelocity velocity lowerCollision upperCollision =
if | lowerCollision -> makePositive velocity
| upperCollision -> makeNegative velocity
| otherwise -> velocity
Now that we have a bunch of helper functions, we will actually step the ball forward. The stepBall function first figures out the new velocity of the ball. The velocity only changes when there is a collision with a paddle or a wall. More specifically, the x-velocity depends on paddle collisions, and the y-velocity depends on wall collisions. This new velocity is then used to calculate a new position.
This function also determines whether a point has been scored and who receives the point. Thus, its output is a new Ball and points to be added to each player.
stepBall delta (Ball (x,y) (vx,vy)) (Paddle y1) (Paddle y2) =
let hitPaddle1 = within 20 y1 y && within 8 25 x
hitPaddle2 = within 20 y2 y && within 8 (gameWidth - 25) x
vx' = stepVelocity vx hitPaddle1 hitPaddle2
vy' = stepVelocity vy (y < 7) (y > gameHeight - 7)
scored = x > gameWidth || x < 0
x' = if scored then halfWidth else x + vx' * delta
y' = if scored then halfHeight else y + vy' * delta
in ( Ball (x',y') (vx',vy')
, if x > gameWidth then 1 else 0
, if x < 0 then 1 else 0 )
Finally, we define a step function for the entire game. This steps from state to state based on the inputs to the game.
stepGame (Input delta (KeyInput space dir1 dir2))
(GameState state (Score s1 s2) ball paddle1 paddle2) =
let (ball',s1',s2') = if state == Play then stepBall delta ball paddle1 paddle2
else (ball, 0, 0)
state' = case state of Play -> if s1' /= s2' then BetweenRounds else state
BetweenRounds -> if space then Play else state
in GameState state'
(Score (s1+s1') (s2+s2'))
ball'
(stepPaddle delta dir1 paddle1)
(stepPaddle delta dir2 paddle2)
Now we put everything together. We combine the input, game representation, and step function into one signal.
gameState = foldp stepGame defaultGame input
The gameState signal carries the current state of the game. It starts with our default game and steps forward based on user input. All we need to do now is show the game to the user.
These functions take a GameState and turn it into something a user can see and understand. It is entirely independent of how the game updates, it only needs to know the current game state. This allows us to change how the game looks without changing any of the logic of the game.
This section gives you much more freedom than the previous ones because you can be as fancy and elaborate as you want. I will try to keep our display fairly simple for the sake of clarity!
The scoreBoard function displays the current score and some directions for the users. When the game is between rounds, this will display a message to users "Press SPACE to begin", letting them know how to start playing. This message disappears when the ball is in play because it is irrelevant at that point. The score is always displayed.
scoreBoard w inPlay p1 p2 =
let code = text . monospace . toText
stack top bottom = flow down [ code " ", code top, code bottom ]
msg = width w . centeredText . monospace $ toText "Press SPACE to begin"
score = width w . box 2 $ flow right
[ stack "W" "S", rectangle 20 1
, text . Text.height 4 $ show p1 ++ toText " " ++ show p2
, rectangle 20 1, stack "↑" "↓" ]
in if inPlay then score else score `above` msg
This function displays the entire GameState. Part of that is the score board, but the most vital part is the display of the game. This uses Elm's collage interface to draw shapes on screen. We just need to draw four things: a field, a ball, the left paddle, and the right paddle.
display (w,h) (GameState state (Score p1 p2) (Ball pos _) (Paddle y1) (Paddle y2)) =
layers
[ scoreBoard w (state == Play) p1 p2
, let pongGreen = rgb 60 100 60 in
size w h . box 5 $ collage gameWidth gameHeight
[ filled pongGreen (rect gameWidth gameHeight (halfWidth,halfHeight))
, filled white (oval 15 15 pos) -- ball
, filled white (rect 10 40 ( 20, y1)) -- first paddle
, filled white (rect 10 40 (gameWidth - 20, y2)) -- second paddle
]
]
We can now define a view of the game (a signal of Elements) that changes as the GameState changes. This is what the users will see.
And finally, we display the view of the game to the user!
main = lift2 display Window.dimensions gameState
And that is it! Pong in Elm!
If you want to learn more about making games in Elm, try tackling some of these challenges:
If you want to read more about FRP for games, see this paper. Note that it is specific to Arrowized FRP, which is supported by Elm's Automaton library.