on master help sign in
Finish up comments for Pong.
d0da29fevancz
Examples/elm-js/Pong/Pong.elm+133−14
@@ -10,19 +10,32 @@ import Signal.Window as Win
10------ Extracting timesteps from JavaScript ------
10------ Extracting timesteps from JavaScript ------
11------------------------------------------------------------------------
11------------------------------------------------------------------------
12
12
13+-- Export our desired FPS to JavaScript. We just want a steady 30 frames
14+-- per second for this game.
15+
13desiredFPS = constant (castIntToJSNumber 30)
16desiredFPS = constant (castIntToJSNumber 30)
14
17
15foreign export jsevent "desiredFPS"
18foreign export jsevent "desiredFPS"
16 desiredFPS :: Signal JSNumber
19 desiredFPS :: Signal JSNumber
17
20
21+
22+-- Import the current time from JavaScript. Events will come in roughly
23+-- 30 times per second.
24+
18foreign import jsevent "trigger" (castIntToJSNumber 0)
25foreign import jsevent "trigger" (castIntToJSNumber 0)
19 jsTime :: Signal JSNumber
26 jsTime :: Signal JSNumber
20
27
21time = lift castJSNumberToFloat jsTime
28time = lift castJSNumberToFloat jsTime
22
29
30+
31+-- Determine the time that has elapsed since the last event. This is
32+-- our timestep, telling us how far everything should move in the next
33+-- frame.
34+
23delta = lift snd $ foldp (\t1 (t0,d) -> (t1, t1-t0)) (0,0) time
35delta = lift snd $ foldp (\t1 (t0,d) -> (t1, t1-t0)) (0,0) time
24
36
25
37
38+
26------------------------------------------------------------------------
39------------------------------------------------------------------------
27------ Modelling user input ------
40------ Modelling user input ------
28------------------------------------------------------------------------
41------------------------------------------------------------------------
@@ -34,13 +47,19 @@ data Direction = Up | Neutral | Down
34
47
35-- During gameplay, all keyboard input is about the position of the two
48-- During gameplay, all keyboard input is about the position of the two
36-- paddles. So the keyboard input can be reduced to two `Directions'.
49-- paddles. So the keyboard input can be reduced to two `Directions'.
50+-- Furthermore, the SPACE key is used to start the game between rounds,
51+-- so we also need a boolean value to represent whether it is pressed.
37
52
38data KeyInput = KeyInput Bool Direction Direction
53data KeyInput = KeyInput Bool Direction Direction
39
54
55+defaultKeyInput = KeyInput False Neutral Neutral
56+
40
57
41-- Now we determine how to update the direction of a paddle based on
58-- Now we determine how to update the direction of a paddle based on
42-- keyboard input. The first two args of `updatePaddle` are the key
59-- keyboard input. The first two args of `updatePaddle` are the key
43−-- codes of the up and down keys.
60+-- codes of the up and down keys. The next argument is the key that
61+-- has been pressed. The last argument is the previously calculated
62+-- direction of the paddle, which is getting updated.
44
63
45updateDirection upKey downKey key direction =
64updateDirection upKey downKey key direction =
46 case direction of
65 case direction of
@@ -53,16 +72,25 @@ updateDirection upKey downKey key direction =
53updateDirection1 = updateDirection 87 83 -- 'w' for up and 's' for down
72updateDirection1 = updateDirection 87 83 -- 'w' for up and 's' for down
54updateDirection2 = updateDirection 38 40 -- 'UP' for up and 'DOWN' for down
73updateDirection2 = updateDirection 38 40 -- 'UP' for up and 'DOWN' for down
55
74
75+
76+-- Update the keyboard input representation based on a particular key press.
77+
56updateInput key (KeyInput space dir1 dir2) =
78updateInput key (KeyInput space dir1 dir2) =
57 KeyInput (space || key == 32)
79 KeyInput (space || key == 32)
58 (updateDirection1 key dir1)
80 (updateDirection1 key dir1)
59 (updateDirection2 key dir2)
81 (updateDirection2 key dir2)
60
82
83+
84+-- `keysDown` has type (Signal [Int]) so we need to fold `updateInput`
85+-- over all of the keys that are currently pressed to get the current
86+-- keyboard state.
87+
61−keyInput = lift (foldl updateInput (KeyInput False Neutral Neutral)) keysDown
88+keyInput = lift (foldl updateInput defaultKeyInput) keysDown
89+
62
90
63
91
64------------------------------------------------------------------------
92------------------------------------------------------------------------
65−------ Combining all inputs to game ------
93+------ Combining all inputs ------
66------------------------------------------------------------------------
94------------------------------------------------------------------------
67
95
68-- The inputs to this game include a timestep (which we extracted from
96-- The inputs to this game include a timestep (which we extracted from
@@ -70,32 +98,68 @@ keyInput = lift (foldl updateInput (KeyInput False Neutral Neutral)) keysDown
70
98
71data Input = Input Float KeyInput
99data Input = Input Float KeyInput
72
100
101+-- Combine both kinds of inputs and filter out keyboard events. We only
102+-- want the game to refresh on clock-ticks, not key presses too.
103+
73−input = lift2 Input delta keyInput
104+input = sampleOn delta (lift2 Input delta keyInput)
105+
74
106
75
107
76------------------------------------------------------------------------
108------------------------------------------------------------------------
77------ Modelling Pong / a State Machine ------
109------ Modelling Pong / a State Machine ------
78------------------------------------------------------------------------
110------------------------------------------------------------------------
79
111
112+-- Pong has two obvious components: the ball and two paddles.
113+
80data Paddle = Paddle Float -- y-position
114data Paddle = Paddle Float -- y-position
81data Ball = Ball (Float,Float) (Float,Float) -- position and velocity
115data Ball = Ball (Float,Float) (Float,Float) -- position and velocity
82
116
117+-- But we also want to keep track of the current score and whether
118+-- the ball is currently in play. This will allow us to have rounds
119+-- of play rather than just having the ball move around continuously.
120+
83data Score = Score Int Int
121data Score = Score Int Int
84data State = Play | BetweenRounds
122data State = Play | BetweenRounds
85
123
124+-- Together, this information makes up the state of the game. We model
125+-- Pong by using the inputs (defined above) to update the state of the
126+-- game!
127+
86data GameState = GameState State Score Ball Paddle Paddle
128data GameState = GameState State Score Ball Paddle Paddle
87
129
130+
131+-- I have chosen to parameterize the size of the board, so it can
132+-- be changed with minimal effort.
133+
88gameWidth = 600
134gameWidth = 600
89gameHeight = 400
135gameHeight = 400
90halfWidth = gameWidth / 2
136halfWidth = gameWidth / 2
91halfHeight = gameHeight / 2
137halfHeight = gameHeight / 2
92
138
139+
140+-- Before we can update anything, we must first define the default
141+-- configuration of the game. In our case we want to start between
142+-- rounds with a score of zero to zero.
143+
93defaultGame = GameState BetweenRounds
144defaultGame = GameState BetweenRounds
94 (Score 0 0)
145 (Score 0 0)
95 (Ball (halfWidth, halfHeight) (150,150))
146 (Ball (halfWidth, halfHeight) (150,150))
96 (Paddle halfHeight)
147 (Paddle halfHeight)
97 (Paddle halfHeight)
148 (Paddle halfHeight)
98
149
150+
151+
152+------------------------------------------------------------------------
153+------ Stepping from State to State ------
154+------------------------------------------------------------------------
155+
156+-- Now to step the game from one state to another. We can break this up
157+-- into smaller components.
158+
159+-- First, we define a step function for updating the position of
160+-- paddles. It depends on our timestep and a desired direction (given
161+-- by keyboard input).
162+
99stepPaddle delta dir (Paddle y) =
163stepPaddle delta dir (Paddle y) =
100 case dir of
164 case dir of
101 { Up -> Paddle . clamp 20 (gameHeight-20) $ y - 200 * delta
165 { Up -> Paddle . clamp 20 (gameHeight-20) $ y - 200 * delta
@@ -103,6 +167,17 @@ stepPaddle delta dir (Paddle y) =
103 ; Neutral -> Paddle y
167 ; Neutral -> Paddle y
104 }
168 }
105
169
170+
171+-- We must also step the ball forward. This is more complicated due to
172+-- the many kinds of collisions that can happen. All together, this
173+-- function figures out the new velocity of the ball based on
174+-- collisions with the top and bottom borders and collisions with the
175+-- paddles. This new velocity is used to calculate a new position.
176+
177+-- This function also determines whether a point has been scored and
178+-- who receives the point. Thus, its output is a new Ball and points
179+-- to be added to each player.
180+
106stepBall delta (Ball (x,y) (vx,vy)) (Paddle y1) (Paddle y2) =
181stepBall delta (Ball (x,y) (vx,vy)) (Paddle y1) (Paddle y2) =
107 let { makePositive n = if n > 0 then n else 0-n
182 let { makePositive n = if n > 0 then n else 0-n
108 ; makeNegative n = if n > 0 then 0-n else n
183 ; makeNegative n = if n > 0 then 0-n else n
@@ -123,24 +198,44 @@ stepBall delta (Ball (x,y) (vx,vy)) (Paddle y1) (Paddle y2) =
123 )
198 )
124
199
125
200
201+-- Finally, we define a step function for the entire game. This steps from state to
202+-- state based on the inputs to the game.
203+
126stepGame (Input delta (KeyInput space dir1 dir2))
204stepGame (Input delta (KeyInput space dir1 dir2))
127 (GameState state (Score s1 s2) ball paddle1 paddle2) =
205 (GameState state (Score s1 s2) ball paddle1 paddle2) =
128− let { (ball',s1',s2') = if state == Play
129− then stepBall delta ball paddle1 paddle2
206+ let { (ball',s1',s2') = if state == Play then stepBall delta ball paddle1 paddle2
130− else (ball, 0, 0)
207+ else (ball, 0, 0)
131 ; state' = case state of { Play -> if s1' /= s2' then BetweenRounds else state
208 ; state' = case state of { Play -> if s1' /= s2' then BetweenRounds else state
132 ; BetweenRounds -> if space then Play else state }
209 ; BetweenRounds -> if space then Play else state }
133− }
134− in GameState state'
210+ } in GameState state'
135− (Score (s1+s1') (s2+s2'))
211+ (Score (s1+s1') (s2+s2'))
136− ball'
212+ ball'
137− (stepPaddle delta dir1 paddle1)
213+ (stepPaddle delta dir1 paddle1)
138− (stepPaddle delta dir2 paddle2)
214+ (stepPaddle delta dir2 paddle2)
215+
216+
217+-- Now we put it all together. We have a signal of inputs that changes whenever there
218+-- is a clock tick. This input signal carries the all the information we need about
219+-- the keyboard. We also have a step function that steps from one game-state to the
220+-- next based on some inputs.
221+
222+-- The `gameState` signal steps forward every time a new input comes in. It starts
223+-- as the default game and progresses based on user behavior.
139
224
140gameState = foldp stepGame defaultGame input
225gameState = foldp stepGame defaultGame input
141
226
142
227
143
228
229+------------------------------------------------------------------------
230+------ Displaying the Game ------
231+------------------------------------------------------------------------
232+
233+-- This function takes a GameState and turns it into something a user
234+-- can see and understand. It is totally independent of how the game
235+-- updates, it only needs to know the current game state. This allows us
236+-- to change how the game looks without changing any of the logic of the
237+-- game.
238+
144display (w,h) (GameState state (Score p1 p2) (Ball pos _) (Paddle y1) (Paddle y2)) =
239display (w,h) (GameState state (Score p1 p2) (Ball pos _) (Paddle y1) (Paddle y2)) =
145 let score = width w . centeredText . Text.height 4 $
240 let score = width w . centeredText . Text.height 4 $
146 show p1 ++ toText " " ++ show p2
241 show p1 ++ toText " " ++ show p2
@@ -156,10 +251,34 @@ display (w,h) (GameState state (Score p1 p2) (Ball pos _) (Paddle y1) (Paddle y2
156 ]
251 ]
157 ]
252 ]
158
253
254+-- We can now define a view of the game (a signal of Elements) that changes
255+-- as the GameState changes. This is what the users will see.
256+
159view = lift2 display Win.dimensions gameState
257view = lift2 display Win.dimensions gameState
160
258
259+
260+-- Here we tell the JavaScript FPS manager that all of the computations for
261+-- this frame are complete. This allows the manager to calculate when the next
262+-- step should happen.
263+
161done = lift (\_ -> castBoolToJSBool True) view
264done = lift (\_ -> castBoolToJSBool True) view
162foreign export jsevent "finished"
265foreign export jsevent "finished"
163 done :: Signal JSBool
266 done :: Signal JSBool
164
267
268+
269+-- And finally, we display the view of the game to the user: Pong in Elm!
270+
165−main = view
271+main = view
272+
273+
274+-- Note that the structure of this game neatly divides Pong into three major
275+-- parts: modeling the game, updating the game, and viewing the game. It may
276+-- be helpful to think of it as a more functional (i.e. less imperative)
277+-- version of the Model-View-Controller paradigm.
278+
279+-- Hopefully this game also displays some of the strengths of Functional
280+-- Reactive Programming. We have written an entire GUI/game without any
281+-- imperative code! No global mutable state, no flipping pixels, no
282+-- destructive updates. In fact, Elm disallows all of these things at the
283+-- language level. Thus, good design is a requirement, not just a self-inforced
284+-- suggestion.