@@ -10,19 +10,32 @@ import Signal.Window as Win
1010------ Extracting timesteps from JavaScript ------
1111------------------------------------------------------------------------
1212
13+-- Export our desired FPS to JavaScript. We just want a steady 30 frames
14+-- per second for this game.
15+
1316desiredFPS = constant (castIntToJSNumber 30)
1417
1518foreign export jsevent "desiredFPS"
1619 desiredFPS :: Signal JSNumber
1720
21+
22+-- Import the current time from JavaScript. Events will come in roughly
23+-- 30 times per second.
24+
1825foreign import jsevent "trigger" (castIntToJSNumber 0)
1926 jsTime :: Signal JSNumber
2027
2128time = lift castJSNumberToFloat jsTime
2229
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+
2335delta = lift snd $ foldp (\t1 (t0,d) -> (t1, t1-t0)) (0,0) time
2436
2537
38+
2639------------------------------------------------------------------------
2740------ Modelling user input ------
2841------------------------------------------------------------------------
@@ -34,13 +47,19 @@ data Direction = Up | Neutral | Down
3447
3548-- During gameplay, all keyboard input is about the position of the two
3649-- 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.
3752
3853data KeyInput = KeyInput Bool Direction Direction
3954
55+defaultKeyInput = KeyInput False Neutral Neutral
56+
4057
4158-- Now we determine how to update the direction of a paddle based on
4259-- 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.
4463
4564updateDirection upKey downKey key direction =
4665 case direction of
@@ -53,16 +72,25 @@ updateDirection upKey downKey key direction =
5372updateDirection1 = updateDirection 87 83 -- 'w' for up and 's' for down
5473updateDirection2 = updateDirection 38 40 -- 'UP' for up and 'DOWN' for down
5574
75+
76+-- Update the keyboard input representation based on a particular key press.
77+
5678updateInput key (KeyInput space dir1 dir2) =
5779 KeyInput (space || key == 32)
5880 (updateDirection1 key dir1)
5981 (updateDirection2 key dir2)
6082
61−keyInput = lift (foldl updateInput (KeyInput False Neutral Neutral)) keysDown
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+
88+keyInput = lift (foldl updateInput defaultKeyInput) keysDown
89+
6290
6391
6492------------------------------------------------------------------------
65−------ Combining all inputs to game ------
93+------ Combining all inputs ------
6694------------------------------------------------------------------------
6795
6896-- 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
7098
7199data Input = Input Float KeyInput
72100
73−input = lift2 Input delta keyInput
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+
104+input = sampleOn delta (lift2 Input delta keyInput)
105+
74106
75107
76108------------------------------------------------------------------------
77109------ Modelling Pong / a State Machine ------
78110------------------------------------------------------------------------
79111
112+-- Pong has two obvious components: the ball and two paddles.
113+
80114data Paddle = Paddle Float -- y-position
81115data Ball = Ball (Float,Float) (Float,Float) -- position and velocity
82116
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+
83121data Score = Score Int Int
84122data State = Play | BetweenRounds
85123
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+
86128data GameState = GameState State Score Ball Paddle Paddle
87129
130+
131+-- I have chosen to parameterize the size of the board, so it can
132+-- be changed with minimal effort.
133+
88134gameWidth = 600
89135gameHeight = 400
90136halfWidth = gameWidth / 2
91137halfHeight = gameHeight / 2
92138
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+
93144defaultGame = GameState BetweenRounds
94145 (Score 0 0)
95146 (Ball (halfWidth, halfHeight) (150,150))
96147 (Paddle halfHeight)
97148 (Paddle halfHeight)
98149
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+
99163stepPaddle delta dir (Paddle y) =
100164 case dir of
101165 { Up -> Paddle . clamp 20 (gameHeight-20) $ y - 200 * delta
@@ -103,6 +167,17 @@ stepPaddle delta dir (Paddle y) =
103167 ; Neutral -> Paddle y
104168 }
105169
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+
106181stepBall delta (Ball (x,y) (vx,vy)) (Paddle y1) (Paddle y2) =
107182 let { makePositive n = if n > 0 then n else 0-n
108183 ; 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) =
123198 )
124199
125200
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+
126204stepGame (Input delta (KeyInput space dir1 dir2))
127205 (GameState state (Score s1 s2) ball paddle1 paddle2) =
128− let { (ball',s1',s2') = if state == Play
129− then stepBall delta ball paddle1 paddle2
130− else (ball, 0, 0)
206+ let { (ball',s1',s2') = if state == Play then stepBall delta ball paddle1 paddle2
207+ else (ball, 0, 0)
131208 ; state' = case state of { Play -> if s1' /= s2' then BetweenRounds else state
132209 ; BetweenRounds -> if space then Play else state }
133− }
134− in GameState state'
135− (Score (s1+s1') (s2+s2'))
136− ball'
137− (stepPaddle delta dir1 paddle1)
138− (stepPaddle delta dir2 paddle2)
210+ } in GameState state'
211+ (Score (s1+s1') (s2+s2'))
212+ ball'
213+ (stepPaddle delta dir1 paddle1)
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.
139224
140225gameState = foldp stepGame defaultGame input
141226
142227
143228
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+
144239display (w,h) (GameState state (Score p1 p2) (Ball pos _) (Paddle y1) (Paddle y2)) =
145240 let score = width w . centeredText . Text.height 4 $
146241 show p1 ++ toText " " ++ show p2
@@ -156,10 +251,34 @@ display (w,h) (GameState state (Score p1 p2) (Ball pos _) (Paddle y1) (Paddle y2
156251 ]
157252 ]
158253
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+
159257view = lift2 display Win.dimensions gameState
160258
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+
161264done = lift (\_ -> castBoolToJSBool True) view
162265foreign export jsevent "finished"
163266 done :: Signal JSBool
164267
165−main = view
268+
269+-- And finally, we display the view of the game to the user: Pong in Elm!
270+
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.