Now that we have all our subscriptions set up, it's time to start making things move around.
When the ball moves outside the board on either end, we just reset the ball in the middle. If not, we calculate the new velocity by checking wether the ball collides with a paddle or the bounds of the board. We also want to update the position by multiplying the velocity with the time delta.
To avoid getting a huge update
function we can create a separate functions for updating the ball. Let's call it updateBall
and pass in the the time delta and the model which holds the two paddles and the previous ball. Here we pass in the model as a parameter to avoid being dependent of order of left and right paddle:
updateBall : Float -> Model -> Ball
updateBall delta model =
...
The first thing we can check for is wether or not the ball is outside the bounds of the board. We're not doing any score tracking in this game, so we can just reset the position of the ball to the center of the board when that happens:
updateBall : Float -> Model -> Ball
updateBall delta model =
-- Get the ball from the model for convenience
let ball = model.ball in
-- Check if the ball is outside the board on either end
if ball.x < -ball.radius || ball.x > boardWidth + ball.radius then
{ ball
| x = boardWidth / 2
, y = boardHeight / 2
}
else
model
In Elm you can create local variables inside functions using the let .. in
syntax like above. Here we set ball to the inner record of model for convenience and to be able to return a updated value of the ball.
We can skip the extra step of storing ball
as a variable by using pattern matching to destructure the passed in model and get ball
directly in the parameter position:
updateBall : Float -> Model -> Ball
updateBall delta {ball} =
-- Check if the ball is outside the board on either end
if ball.x < -ball.radius || ball.x > boardWidth + ball.radius then
{ ball
| x = boardWidth / 2
, y = boardHeight / 2
}
else
model
If the ball is not outside the bounds, we need to check if it collides with any of the paddles or the top or bottom wall.
We can move the paddle detection into some helper functions. The within
function takes a Ball
and Paddle
and checks if the ball is within the paddle frame. You can just copy these into your file:
near : Float -> Float -> Float -> Bool
near a spacing b =
b >= a - spacing && b <= a + spacing
within : Ball -> Paddle -> Bool
within ball paddle =
near (paddle.x + paddle.width / 2) (paddle.width / 2 + ball.radius) ball.x
&& near (paddle.y + paddle.height / 2) (paddle.height / 2 + ball.radius) ball.y
We can now use these functions to update the direction of our ball. Let's start by calculating the updated velocity vx
. We also extend the pattern matching of the model to retrieve paddleLeft
and paddleRight
:
updateBall : Float -> Model -> Ball
updateBall delta {ball, paddleLeft, paddleRight} =
...
else
let
vx =
if within ball model.paddleLeft then
abs ball.vx
else if within ball model.paddleRight then
-(abs ball.vx)
else
ball.vx
in
{ ball | vx = vx }
If the ball is within the left paddle we take the absolute value of the previous velocity. This will make the paddle move to the right. If the paddle is within the right paddle, we make the ball go the opposite direction. If not we just keep the current velocity.
We also need to update our vy
property:
updateBall : Float -> Model -> Ball
updateBall delta {ball, paddleLeft, paddleRight} =
...
else
let
vx =
if within ball model.paddleLeft then
abs ball.vx
else if within ball model.paddleRight then
-(abs ball.vx)
else
ball.vx
vy =
if ball.y < ball.radius then
abs ball.vy
else if ball.y > boardHeight - ball.radius then
-(abs ball.vy)
else
ball.vy
in
{ ball
| vx = vx
, vy = vy
}
Here we flip the direction of the velocity if the ball hits the top or bottom wall.
The last thing we need for our updateBall
function is to calculate the new x
and y
position. To do this we simply need to multiply the time delta with our velocity and add that to the previous x
and y
values:
updateBall : Float -> Model -> Ball
updateBall delta {ball, paddleLeft, paddleRight} =
...
else
let
...
in
{ ball
| x = ball.x + vx * delta
, y = ball.y + vy * delta
, vx = vx
, vy = vy
}
Last thing we need to do update the actual ball in our update
function:
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
Tick delta ->
( { model
| ball = updateBall delta model.paddleLeft model.paddleRight model.ball
}
, Cmd.none
)
If your run the application, that ball should now bounce around on the screen 🎉
Updating the paddles is much easier. We just need to multiply the current velocity with the time delta:
updatePaddle : Int -> Float -> Paddle -> Paddle
updatePaddle direction delta paddle =
{ paddle | y = paddle.y + paddle.vy * delta }
This would work, but the paddle can still move outside the bounds of our board. Let's constrain their movement by using the clamp
function:
updatePaddle : Float -> Paddle -> Paddle
updatePaddle delta paddle =
{ paddle
| y =
clamp 0
(boardHeight - paddle.height)
(paddle.y + paddle.vy * delta)
}
Now that we have our functions for updating our paddles, we can use this in our update
function:
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
Tick delta ->
( { model
| ball = updateBall delta model.paddleLeft model.paddleRight model.ball
, paddleLeft = updatePaddle delta model.paddleLeft
, paddleRight = updatePaddle delta model.paddleRight
}
, Cmd.none
)
Your entire game should look something like this now:
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 delta ->
( { model
| ball = updateBall delta model
, paddleLeft = updatePaddle delta model.paddleLeft
, paddleRight = updatePaddle delta model.paddleRight
}
, Cmd.none
)
updatePaddle : Float -> Paddle -> Paddle
updatePaddle delta paddle =
{ paddle
| y =
clamp 0
(boardHeight - paddle.height)
(paddle.y + paddle.vy * delta)
}
updateBall : Float -> Model -> Ball
updateBall delta {ball, paddleLeft, paddleRight} =
if ball.x < -ball.radius || ball.x > boardWidth + ball.radius then
{ ball
| x = boardWidth / 2
, y = boardHeight / 2
}
else
let
vx =
if within ball paddleLeft then
abs ball.vx
else if within ball paddleRight then
-(abs ball.vx)
else
ball.vx
vy =
if ball.y < ball.radius then
abs ball.vy
else if ball.y > boardHeight - ball.radius then
-(abs ball.vy)
else
ball.vy
in
{ ball
| x = ball.x + vx * delta
, y = ball.y + vy * delta
, vx = vx
, vy = vy
}
near : Float -> Float -> Float -> Bool
near a spacing b =
b >= a - spacing && b <= a + spacing
within : Ball -> Paddle -> Bool
within ball paddle =
near (paddle.x + paddle.width / 2) (paddle.width / 2 + ball.radius) ball.x
&& near (paddle.y + paddle.height / 2) (paddle.height / 2 + ball.radius) ball.y
-- 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
}