Subscriptions is the way we can listen to external input in Elm. When subscribing the some input we need to define a Msg
type that will be triggered in our update
function. Subscriptions is described in more detail here.
We need a way to hook into the browsers requestAnimationFrame
loop, so that our game tick is synchronized with the browser rendering. We can do this by installing the AnimationFrame
package:
elm package install elm-lang/animation-frame
And import it at the top:
import AnimationFrame
First off, we need to update our Msg
type. The AnimationFrame
subscription is going to include a value for the time delta between each frame. We therefore need to update the Tick
type to have a Float
associated to it:
type Msg =
Tick Float
Now we can set up the actual subscription. We can define our subscriptions in a function called subscriptions
that looks like this:
subscriptions : Model -> Sub Msg
subscriptions model =
...
As you can see from the type signature, this function returns a subscription of the Sub
. We're not really going to use the model
parameter here, but we have to include it since it's required by Html.program
.
The AnimationFrame
package has a subscription called diffs
which will trigger an event on each animation frame and pass in the time delta to our Tick
message. We can just return this in our subscriptions
function:
subscriptions : Model -> Sub Msg
subscriptions model =
AnimationFrame.diffs Tick
We also need to change our update
function so that it handles the new Tick
message:
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
Tick delta ->
( model, Cmd.none)
Here we use pattern matching on our msg
parameter to match on the Tick
type and get the time delta. We're just going to leave the model unchanged for now.
In the previous examples we were using Html.beginnerProgram
. This is a great way to get started, but it does not support subscriptions. To get that, we have to use Html.program
. Here's how the type signature looks like:
{ init : (model, Cmd msg)
, update : msg -> model -> (model, Cmd msg)
, subscriptions : model -> Sub msg
, view : model -> Html msg
}
We see that just like before it takes an update
and a view
function. The model
attribute that we had in beginnerProgram
is now called init
. We also see that we have a new field called subscriptions
.
One thing to notice is that our init
and update
functions have changed a little. Both now return a tuple with a model and a command.
In Elm, commands (Cmd
) are how we tell the runtime to execute things that involve side effects. For example, making http requests or saving something to local storage. We're not really going to use commands in our game, so we can just return Cmd.none
in places that require us to return a command. If you're interested, you can read more about commands.
Our init
function will now look something like this:
init : ( Model, Cmd Msg )
init =
( { ball = initBall
, paddleLeft = initPaddle 20
, paddleRight = initPaddle (boardWidth - 25)
}
, Cmd.none
)
It's pretty much identical to our previous implementation, only that we now put our model in a tuple along with some command.
Let's change our update
function as well:
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
Tick ->
( model, Cmd.none )
More details on commands and subscriptions
The final thing we need to do is update from Html.beginnerProgram
to Html.program
. Just update your main
function to this:
main =
Html.program
{ init = init
, update = update
, view = view
, subscriptions = subscriptions
}
Nothing is really happening on the screen, but that's just because we're not updating our model yet. If your using elm-reactor
though, your should be able to open the time-travel debugger and see that the Tick
event gets fired quite a lot.
By now, your code should look something like this:
module Main exposing (..)
import Html exposing (..)
import Svg exposing (..)
import Svg.Attributes exposing (..)
import AnimationFrame
boardWidth =
500
boardHeight =
300
-- MODEL
type alias Model =
{ ball : Ball
, paddleLeft : Paddle
, paddleRight : Paddle
}
type alias Ball =
{ x : Float
, y : Float
, vx : Float
, vy : Float
, radius : Float
}
type alias Paddle =
{ x : Float
, y : Float
, vx : Float
, vy : Float
, width : Float
, height : Float
}
init : ( Model, Cmd Msg )
init =
( { ball = initBall
, paddleLeft = initPaddle 20
, paddleRight = initPaddle (boardWidth - 25)
}
, Cmd.none
)
initBall : Ball
initBall =
{ x = boardWidth / 2
, y = boardHeight / 2
, vx = 0.3
, vy = 0.3
, radius = 8
}
initPaddle : Float -> Paddle
initPaddle x =
{ x = x
, y = 0
, vx = 0.4
, vy = 0.4
, width = 5
, height = 80
}
-- UPDATE
type Msg
= Tick Float
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
Tick time ->
( model, Cmd.none )
-- VIEW
view : Model -> Html Msg
view model =
svg
[ width (toString boardWidth)
, height (toString boardHeight)
]
[ rect
[ width (toString boardWidth)
, height (toString boardHeight)
, fill "black"
]
[]
, ballView model.ball
, paddleView model.paddleLeft
, paddleView model.paddleRight
]
ballView : Ball -> Svg Msg
ballView model =
circle
[ cx (toString model.x)
, cy (toString model.y)
, r (toString model.radius)
, fill "white"
]
[]
paddleView : Paddle -> Svg Msg
paddleView model =
rect
[ width (toString model.width)
, height (toString model.height)
, x (toString model.x)
, y (toString model.y)
, fill "white"
]
[]
-- SUBSCRIPTIONS
subscriptions : Model -> Sub Msg
subscriptions model =
AnimationFrame.diffs Tick
-- MAIN
main =
Html.program
{ init = init
, update = update
, view = view
, subscriptions = subscriptions
}