Space invaders in Elm

Space invaders in Elm

By Ossi Hanhinen

Overload, 25(138):4-6, April 2017

Elm is a functional language which compiles to JavaScript. Ossi Hanhinen provides an overview.

I first learned about Elm in May 2015. I fell in love. I also wrote an article describing how to get started with the language by implementing the base for a game. Its title, ‘Learning FP the hard way’ [ Hanhinen15 ], was supposed to be a joke of sorts, as Elm is in fact a very easy language to learn! I’m not sure how many people got that. ☺

Since then, I have used Elm in two separate customer projects, and it has definitely made my work better!

The recent update (0.17) meant a rather large shift in the way the language works, so I decided to revisit the original subject. So here it is: the base for a Space Invaders game in Elm 0.17!

You can find all of the code on GitHub, along with some setup instructions. [ SpaceInvaders ]

Elm, what is it again?

Elm is a “ delightful language for reliable webapps. ” [ Elm ]. It is a functional language which compiles to JavaScript. You can explore it online at . If you’re interested in an overview, I gave a talk about the language, called ‘Confidence in the frontend with Elm’ at GeeCON 2016. [ Hanhinen16 ]

Modeling the problem

First off, let’s re-iterate what we want to achieve.

From the player’s perspective the program should be like this:

  • There is a ship representing the player near the bottom of the screen
  • The player can move the ship left and right with corresponding arrow buttons
  • The player can shoot with the space bar

And from the ship’s perspective the same is:

  • Ship has a position on a 1D axis
  • Ship can have a velocity (positive or negative)
    • Ship changes position according to its velocity
  • Ship can shoot

This gives us a definition of what the Model of our little program should look like:

  type alias Model =
    { position : Float
    , velocity : Float
    , shotsFired : Int

This is an example of a data structure called Record. It is like a strongly typed and immutable cousin of the JavaScript object. Now, we have only defined the type of the data so far, so let’s create a model to start from:

  model : Model -- this is a type annotation
  model =
    { position = 0
    , velocity = 0
    , shotsFired = 0

What we have here is a simple value, or a constant. As everything in Elm is immutable, model will always be the same no matter what happens in the app. If we tried to redefine it, the compiler would simply complain that there are multiple definitions for the same name and the code would not compile.

Alright, moving on to moving the ship! I remember from high school that s = v * dt , or moved distance is the product of the velocity and the time difference. So that’s how we can update the ship. In Elm, that would be something like the following.

  applyPhysics : Time -> Model -> Model
  applyPhysics dt model =
    { model | position =
      model.position +   (model.velocity * dt) }

The above is the way to update a record. We start off with the model as the base, but update the position as per the formula. Note that { record | x = newX } creates a new record, as everything in Elm is immutable. We will never have to worry about affecting anyone else’s state by accident. Even better, we can be certain no one else is affecting our state either!

The type annotation on applyPhysics says: given a Float and a Model , I will return a Model , but also: given a Float , I will return Model -> Model . For example, (applyPhysics 16.7) would actually return a working function to which we can pass a Model , and get the physics-applied ship as the return value. This property is called Currying and all Elm functions automatically behave this way. Currying is very useful in many cases, but that is a topic for another article.

We can update the other properties in the very same way (see Listing 1).

updateVelocity : Float -> Model -> Model
updateVelocity newVelocity model =
    { model | velocity = newVelocity }

incrementShotsFired : Model -> Model
incrementShotsFired model =
  { model | shotsFired = model.shotsFired + 1 }
Listing 1

Using these little functions we can update all of our state, but we’re missing something quite necessary: 1. View of the current state, and 2. getting input from the user and turning that into updates.

Showing the state

Our game wouldn’t be much use if it couldn’t show the current state in some way. To keep things as simple as possible, let’s just print the model as text. We can do it like so:

  view : Model -> Html msg
  view model =
    text (toString model)

Here, toString turns the model record into a readable String representation of it, and text from the Html package turns a String into an HTML text node. Pretty handy! Now the strange part here might be the return type of our view function: Html msg . We don’t need to worry about it too much right now, but what the type annotation is saying is in essence: “I am returning some HTML, which may produce messages of the msg variety.”

This will do for now, so let’s move on to the interactive part!

Subscribing to user input

Elm 0.17 brought a new way of reacting to changes: Subscriptions. What we will do is this: we will subscribe to certain changes in the world, and when they happen, give the changes some names. We want to control the game by keyboard, so let’s start by taking a look at the Keyboard package. It seems we want to listen for both pressing down on buttons, and letting go of them. With these, we can determine when the user is pressing down on a certain key. We will need something else as well: to keep updating the position of our ship, we need to have a somewhat steady rhythm of applyPhysics with the time difference! That we can get using the AnimationFrame.diffs . Bundling that up into code works like this, defining the messages in our program:

  type Msg
    = TimeUpdate Time
    | KeyDown KeyCode
    | KeyUp KeyCode

Here we have a union type. For something to be considered a Msg in this module, it will have to be one of the above ( TimeUpdate , KeyDown or KeyUp ). Furthermore, the contents of e.g. TimeUpdate must be something that can be considered Time , and so on.

Okay, now let’s declare the subscriptions we need, and name them with our newly-defined message types.

  subscriptions : Model -> Sub Msg
  subscriptions model =
      [ AnimationFrame.diffs TimeUpdate
      , Keyboard.downs KeyDown
      , KeyUp

This again will just return a subscription, or a Sub Msg . It doesn’t do anything on its own, but we need it for the actual wiring part of our code (see Listing 2).

main =
    { init = init
    , view = view
    , update = update
    , subscriptions = subscriptions
Listing 2

If you are really paying attention, you might notice that we have view and subscriptions done by now, but both init and update are still missing. Luckily we already have all the building blocks, so taking this home shouldn’t be too much of a stretch anymore! In fact, init is so simple that we should get it out of the way right now.

  init : ( Model, Cmd Msg )
  init =
    ( model, Cmd.none )

That’s all there is to it! Again, we can leave the Cmd stuff for later, but just as a primer: commands are the only way to have effects in an Elm program. What are effects? They are anything that can affect the world outside the app (such as posting something to the Internet), or whose value can vary between program runs (such as the current time, or random numbers). Here we don’t need to do any commands, so we define it to be none .

Putting it all together: the update

All right, now’s the time to make it all work!

Let’s begin from the high level. The update function takes the incoming message and the old model, and returns the updated model along with possible commands. In this case we won’t need any commands, but we still need to fulfil the contract with none s (see Listing 3).

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
  case msg of
    TimeUpdate dt ->
      ( applyPhysics dt model, Cmd.none )

    KeyDown keyCode ->
      ( keyDown keyCode model, Cmd.none )

    KeyUp keyCode ->
      ( keyUp keyCode model, Cmd.none )
Listing 3

Note that each of the possible Msg options is handled. If they weren’t, the Elm compiler would catch the problem, which is pretty cool and impressive. Anyway, the TimeUpdate is nice and easy. We can simply use the applyPhysics function to get the updated model. For the keypressing cases, I decided to split the handling into their own functions as well.

When it comes to the packages, Elm 0.17 is still a bit of a work in progress. So to make the keyboard handling a little nicer, I made a tiny helper module. There is a function that can turn a KeyCode into a Key , which is a simple union type. It only has the keys we need for this exercise now, but could easily be extended (see Listing 4).

keyDown : KeyCode -> Model -> Model
keyDown keyCode model =
  case Key.fromCode keyCode of
    Space ->
      incrementShotsFired model

    ArrowLeft ->
      updateVelocity -1.0 model

    ArrowRight ->
      updateVelocity 1.0 model

    _ ->
Listing 4

The above should be pretty clear. Spacebar shoots once as soon as it is pressed down and doesn’t do anything else. The arrow keys set the velocity of the ship when pressed down. Notice that we need an “otherwise” case, customarily denoted as _. This is because there are many other possible keys on the keyboard besides the ones we’ve covered.

How about the release part? See Listing 5...

keyUp : KeyCode -> Model -> Model
keyUp keyCode model =
  case Key.fromCode keyCode of
    ArrowLeft ->
      updateVelocity 0 model

    ArrowRight ->
      updateVelocity 0 model

    _ ->
Listing 5

If the released key happened to be one of the movement keys, reset the velocity to 0, otherwise let’s just keep the current model. Pretty straightforward, right?

Now it should work!



[Hanhinen15] Learning FP the hard way: Experiences on the Elm language



Kindly republished from Ossi’s blog:

Your Privacy

By clicking "Accept Non-Essential Cookies" you agree ACCU can store non-essential cookies on your device and disclose information in accordance with our Privacy Policy and Cookie Policy.

Current Setting: Non-Essential Cookies REJECTED

By clicking "Include Third Party Content" you agree ACCU can forward your IP address to third-party sites (such as YouTube) to enhance the information presented on this site, and that third-party sites may store cookies on your device.

Current Setting: Third Party Content EXCLUDED

Settings can be changed at any time from the Cookie Policy page.