Skip to content

Latest commit

 

History

History
414 lines (323 loc) · 10.5 KB

4. Game loop.md

File metadata and controls

414 lines (323 loc) · 10.5 KB

4. Game loop

Now that we have all our subscriptions set up, it's time to start making things move around.

Updating the ball

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 🎉

Moving the paddles

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
        }