title |
---|
Paddle |
Now that we know keyboard signals, we will use them to create a game. The Paddle.elm program is a game. The player uses the keyboard arrows to move the paddle left or right to keep the moving ball within the area limited by the blue walls. Before continuing, try the game here, to have an idea of how it works.
The code starts with the Paddle
module declaration and a list of imports.
% Paddle.elm module Paddle where
import Color (blue, green, orange, red, white)
import Graphics.Collage (Form, circle, collage, filled, group, move, moveY, rect)
import Graphics.Element (Element, container, empty, layers, middle)
import Keyboard
import Signal ((<~), Signal, foldp, merge)
import Text as T
import Time (Time, fps)
import Window
After the imports, four functions which draw various elements of the
game view are defined. The borders
function draws the blue wall by
drawing a blue square and a slightly smaller white rectangle on top of
it.
% Paddle.elm
borders : Form
borders =
group [
rect 440 440 |> filled blue,
rect 400 420 |> filled white |> moveY -10
]
The paddle
function draws the green paddle, given its horizontal
position passed as the parameter.
% Paddle.elm
paddle : Float -> Form
paddle x =
rect (toFloat 100) (toFloat 20)
|> filled green
|> move (x,-210)
The ball
function takes the coordinates of the ball as arguments and
draws an orange circle.
% Paddle.elm
ball : Float -> Float -> Form
ball x y = filled orange (circle 10) |> move (x,y)
Finally the gameOver
function draws the “Game Over” text presented
to the user when the game is over.
% Paddle.elm
gameOver : Element
gameOver =
T.fromString "Game Over"
|> T.color red
|> T.bold
|> T.height 60
|> T.centered
|> container 440 440 middle
The State
type represents the state of the game.
% Paddle.elm
type alias State =
{
x: Float,
y: Float,
dx: Float,
dy: Float,
paddlex: Float,
paddledx: Float,
isOver: Bool
}
The x
and y
members are the coordinates of the ball. The dx
and
dy
members represent the horizontal and vertical velocity of the
ball. The paddlex
and paddledx
represent the horizontal position
and velocity of the paddle (the vertical position is fixed and does
not have to be part of the state). The isOver
flag holds the
information whether the game is over or not.
The initialState
function creates the initial state of the game.
% Paddle.elm
initialState : State
initialState =
{
x = 0,
y = 0,
dx = 0.14,
dy = 0.2,
paddlex = 0,
paddledx = 0,
isOver = False
}
The view
function takes the state value as argument and draws the
game view using the functions described above.
% Paddle.elm
view : State -> Element
view s =
layers [
collage 440 440 [
borders,
paddle s.paddlex,
ball s.x s.y
],
if s.isOver then gameOver else empty
]
The state of the game will change in response to a time-based signal —
for moving the ball — and to a keyboard-based signal — for moving the
paddle. Both signals will be merged. In order to do that, we will need
a data type representing events from both signals. The Event
data
type represents the events.
% Paddle.elm
type Event = Tick Time | PaddleDx Int
The Tick
constructor takes a Time
value as parameter and creates
an event representing a time-based tick. The PaddleDx
constructor
takes a numeric value and creates a keyboard-based event representing
the new value of the horizontal paddle velocity.
The clockSignal
function creates a signal of ticks using the
standard library fps
function.
% Paddle.elm
clockSignal : Signal Event
clockSignal = Tick <~ fps 100
The keyboardSignal
function uses the Keyboard.arrows
signal to
create a signal of keyboard-based events. Only the x
members from
the values produced by the Keyboard.arrows
signal are needed in our
game. The numeric values carried by the PaddleDx
events will only
have one of three possible values coming from the Keyboard.arrows
events: -1
, 0
, or 1
.
% Paddle.elm
keyboardSignal : Signal Event
keyboardSignal = (.x >> PaddleDx) <~ Keyboard.arrows
The eventSignal
merges both signal into one combined signal.
% Paddle.elm
eventSignal : Signal Event
eventSignal = merge clockSignal keyboardSignal
The gameSignal
function creates a signal representing how the game
state changes over time by folding (foldp
) the eventSignal
events using the step
function.
% Paddle.elm
gameSignal : Signal State
gameSignal = foldp step initialState <| eventSignal
The foldp
function takes three arguments. The first one is the
step
function — it is a function that takes two arguments (the
current event and the current state) and produces the new state. The
second argument is the initial state, returned by the initialState
function. The third one is the signal of input events (returned by
eventSignal
).
The step
function combines an event and the current game state to
produce a new game state.
% Paddle.elm
step : Event -> State -> State
step event s =
if s.isOver
then s
else case event of
Tick time ->
{ s |
x <- s.x + s.dx*time,
y <- s.y + s.dy*time,
dx <- if (s.x >= 190 && s.dx > 0) ||
(s.x <= -190 && s.dx < 0)
then -1*s.dx
else s.dx,
dy <- if (s.y >= 190 && s.dy > 0) ||
(s.y <= -190 && s.dy < 0 &&
s.x >= s.paddlex - 50 &&
s.x <= s.paddlex + 50)
then -1*s.dy
else s.dy,
paddlex <- ((s.paddlex + s.paddledx*time) `max` -150) `min` 150,
isOver <- s.y < -200
}
PaddleDx dx -> { s | paddledx <- 0.1 * toFloat dx }
The function first verifies whether the game is over already. If it is, the state is returned unchanged. Otherwise, the event is pattern-matched agains the possible constructors.
The Tick
event triggers an update of almost all of the state
member. The ball coordinates x
and y
are changed by adding the
current velocity (dx
and dy
) multiplied by the amount of time
carried by the tick event. Since the tick events are generated by the
fps
function, the time
value represents the time elapsed since the
previous event was generated. Likewise, the paddlex
value is
updated, but the code makes sure that the new value stays withing the
range of -150 to 150.
The step
functions also verifies whether changes are required to the
values of the vertical and horizontal ball velocity, and if they are,
the velocity values are negated. The horizontal velocity is changed
when the ball hits the left or right wall. The vertical velocity is
changed when the ball hits the top wall or the paddle.
If the ball misses the paddle and its vertical coordinate falls below
-200, the game is over and the isOver
value is updated accordingly.
The PaddleDx
event only results in setting a new value of the
horizontal paddle velocity paddledx
.
The main
function lifts the view
function into the gameSignal
producing the final game signal that is rendered on the screen.
% Paddle.elm
main : Signal Element
main = view <~ gameSignal
In order to transform the game state, we have used a monolitic step
function, that reacts to each possible combination of input event and
current state. The solution works, but it has the disadvantage that
the function which transforms the state may become big and difficult
to maintain for larger programs. We will explore alternatives to that
approach in the subsequent chapters. The
next chapter presents a program which uses
an alternative approach.