forked from grzegorzbalcerek/elm-by-example.0.14.1
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathChapter11Paddle.elm
253 lines (206 loc) · 8.8 KB
/
Chapter11Paddle.elm
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
-- -*- coding: utf-8; -*-
import Lib (..)
import Window
import Signal
import Markdown
content w = pageTemplate [content1]
"Chapter10KeyboardSignals" "toc" "Chapter12TicTacToe" w
main = Signal.map content Window.width
content1 = Markdown.toElement """
# Chapter 11 Paddle
Now that we know keyboard signals, we will use them to create a game.
The *[Paddle.elm](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](Paddle.html), 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](Chapter12TicTacToe.html) chapter presents a program which uses
an alternative approach.
"""