Beginner Tutorials: How to build a game in Elm — Part 3

Part 3 of 12 — Add the Pause

Previous: Part 2 — Add Keyboard Support

Next: Part 4—Add the Players (coming soon)

Add the pause

This is what we are making in this part 3 of the tutorial

The Game Loop never stop also when nobody is playing the game. Some browser may put it on hold when the page is not seen (either because below some other tab or because below under other applications) but better to handle our own way to pause the game.

Doing so, we can save CPU cycles (and power consumption).

Using The Elm Architecture is very simple to unsubscribe from listening to an event because the subscription function also get the model as argument. So we can add this if condition:

if model.isPaused then
[]
else
[ Browser.Events.onAnimationFrameDelta Frame ]

So when the flag model.isPaused that we added to the model is true, we do not subscribe to the onAnimationFrameDelta.

Now we need to set this flag. The Browser.Events.onVisibilityChange function help us here.

This is the analogue of document.hidden in Javascript as per this Elm code.

So we subscribe to this function with

Browser.Events.onVisibilityChange OnVisibilityChange

and in the update:

OnVisibilityChange visibility ->
case visibility of
Browser.Events.Hidden ->
( { model | isPaused = True }, cmd )
_ ->
( model, Cmd.none )

We only care about the Browser.Events.Hidden because once the pause is activated we want to disable it manually (pressing any key) and not automatically when the visibility change again.

While Browser.Events.Hidden is working well when changing tab in the browser, it doesn’t fire when switching to another application.

For these cases we should rely on the Javascript window.onblur event happen. So we can create a simple port for this:

port onblur : (() -> msg) -> Sub msg

And in Javascript:

window.onblur = function () {
app.ports.onblur.send(null);
}

So now, just subscribing to the port we can handle this second case in the update function:

    OnBlur _ -> 
startPause ( { model | pressedKeys = [] }, Cmd.none )

Note 1: We moved the modification of the model into the function startPause. This because we are starting the pause from several places in our code and creating a function for that help to keep the behavior consistent.

Note 2: We clear the list of pressed Keys. This because on blur can happen if — for example — we press Command+ Tab to switch to another application. In this case the browser in not receiving the even onKeyUp for the Command key because that is happening after the browser lost the focus. To remediate to this issue we reset the list of pressed keys to an empty list.

We also want to add a key that would activate the pause directly. Let’s choose the Esc key. Moreover, as mentioned above, we want disable the pause when any key is pressed. Let’s add all this login in the update:

KeyMsg keyMsg ->
let
( newModel, newCmd ) =
( model, Cmd.none )
|> updatePressedKeys keyMsg
in
if model.isPaused then
if anyKeyPressed newModel then
( newModel, newCmd ) |> stopPause
else
( newModel, newCmd )
else if isKeyDown newModel Keyboard.Escape then
( newModel, newCmd ) |> startPause
else
( newModel, newCmd )

This part is more complicate than expected. The main reason is that there are two different behavior of button that we want to consider:

  1. Continuous Pressing. This is the case where, for example, you want to press a key and see your character moving ahead and when you release the key you want to see the character stop moving (or decelerate).
  2. Single Pressing. For cases like firing. We want the player to release the key and press it again to fire again

The way we implemented the detection of keyboard fit the case #1 but we need some extra work for the case #2.

What I have done is that I created a pressedKeysBefore where I store the key that were pressed at the previous cycle. This is done here:

updatePressedKeys : Keyboard.Msg -> ( Model, Cmd msg ) -> ( Model, Cmd msg )
updatePressedKeys keyMsg ( model, cmd ) =
let
pressedKeys =
Keyboard.update keyMsg model.pressedKeys
in
( { model
| pressedKeysBefore = model.pressedKeys
, pressedKeys = pressedKeys
}
, cmd
)

and during the Game Loop I do

updatePressedKeysBefore : ( Model, Cmd msg ) -> ( Model, Cmd msg )
updatePressedKeysBefore ( model, cmd ) =
( { model | pressedKeysBefore = model.pressedKeys }, cmd )

This process also for the activation of the pause otherwise pressing Esc would activate the pause but at the next cycle it will be interpreted as any key and the pause would be de-activated. And then Esc would be detected again and so on, crating an infinite loop until the key released.

This is probably one of the complex part that we will encounter during the development of the game, so do not worry also if it is not completely clear. You can move ahead and come back to this part later.

Last thing to add is the text “Pause, press any key to continue”:

viewCanvas : Model -> List Renderable
viewCanvas model =
if model.isPaused then
[]
|> viewPauseBackground model.window
|> viewLogo colorStartMenu
|> textComposable [ font 48, fill Color.black, align Center ] ( cX model.window, cY model.window - 10 ) "pause"
|> textComposable [ font 10, fill Color.black, align Center ] ( cX model.window, cY model.window + 30 ) "press any key to resume"
else
[]
|> viewFullscreenRect model.window (Color.rgb 0.9 0.7 1)
|> viewLogo colorWhilePlaying
|> viewTexts model

Here instead erasing the canvas, we just write in top of it. Everything in the background will just freeze.

Note that viewCanvas is only called once when the pause start, after that we unsubscribe from onAnimationFrameDelta so Elm will not detect any changes in the model anymore and will not ask for redrawing the canvas again, until the pause is released.

This is the code:

This is all for this third part, leave your feedback in the comments section below and stay tuned for the next posts!

  1. The Game Loop
  2. Add Keyboard Support
  3. Add Pause (this post)
  4. Add Player (coming soon)
  5. Add Score (coming soon)
  6. Add Random (coming soon)
  7. Add Shots (coming soon)
  8. Detect Collisions (coming soon)
  9. Add Explosions (coming soon)
  10. Add Slow Motion (coming soon)
  11. Add Sound (coming soon)
  12. Add Menu (coming soon)

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store