@@ -10,19 +10,32 @@ import Signal.Window as Win
10------ Extracting timesteps from JavaScript ------
10------ Extracting timesteps from JavaScript ------
11------------------------------------------------------------------------
11------------------------------------------------------------------------
13+-- Export our desired FPS to JavaScript. We just want a steady 30 frames
14+-- per second for this game.
13desiredFPS = constant (castIntToJSNumber 30)
16desiredFPS = constant (castIntToJSNumber 30)
15foreign export jsevent "desiredFPS"
18foreign export jsevent "desiredFPS"
16 desiredFPS :: Signal JSNumber
19 desiredFPS :: Signal JSNumber
22+-- Import the current time from JavaScript. Events will come in roughly
23+-- 30 times per second.
18foreign import jsevent "trigger" (castIntToJSNumber 0)
25foreign import jsevent "trigger" (castIntToJSNumber 0)
19 jsTime :: Signal JSNumber
26 jsTime :: Signal JSNumber
21time = lift castJSNumberToFloat jsTime
28time = lift castJSNumberToFloat jsTime
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
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
26------------------------------------------------------------------------
39------------------------------------------------------------------------
27------ Modelling user input ------
40------ Modelling user input ------
28------------------------------------------------------------------------
41------------------------------------------------------------------------
@@ -34,13 +47,19 @@ data Direction = Up | Neutral | Down
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.
38data KeyInput = KeyInput Bool Direction Direction
53data KeyInput = KeyInput Bool Direction Direction
55+defaultKeyInput = KeyInput False Neutral Neutral
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.
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
76+-- Update the keyboard input representation based on a particular key press.
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)
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
61−keyInput = lift (foldl updateInput (KeyInput False Neutral Neutral)) keysDown
88+keyInput = lift (foldl updateInput defaultKeyInput) keysDown
64------------------------------------------------------------------------
92------------------------------------------------------------------------
65−------ Combining all inputs to game ------
93+------ Combining all inputs ------
66------------------------------------------------------------------------
94------------------------------------------------------------------------
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
71data Input = Input Float KeyInput
99data Input = Input Float 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.
73−input = lift2 Input delta keyInput
104+input = sampleOn delta (lift2 Input delta keyInput)
76------------------------------------------------------------------------
108------------------------------------------------------------------------
77------ Modelling Pong / a State Machine ------
109------ Modelling Pong / a State Machine ------
78------------------------------------------------------------------------
110------------------------------------------------------------------------
112+-- Pong has two obvious components: the ball and two paddles.
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
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.
83data Score = Score Int Int
121data Score = Score Int Int
84data State = Play | BetweenRounds
122data State = Play | BetweenRounds
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
86data GameState = GameState State Score Ball Paddle Paddle
128data GameState = GameState State Score Ball Paddle Paddle
131+-- I have chosen to parameterize the size of the board, so it can
132+-- be changed with minimal effort.
88gameWidth = 600
134gameWidth = 600
89gameHeight = 400
135gameHeight = 400
90halfWidth = gameWidth / 2
136halfWidth = gameWidth / 2
91halfHeight = gameHeight / 2
137halfHeight = gameHeight / 2
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.
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)
152+------------------------------------------------------------------------
153+------ Stepping from State to State ------
154+------------------------------------------------------------------------
156+-- Now to step the game from one state to another. We can break this up
157+-- into smaller components.
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).
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
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.
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.
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) =
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.
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 }
134− in GameState state'
210+ } in GameState state'
135− (Score (s1+s1') (s2+s2'))
211+ (Score (s1+s1') (s2+s2'))
137− (stepPaddle delta dir1 paddle1)
213+ (stepPaddle delta dir1 paddle1)
138− (stepPaddle delta dir2 paddle2)
214+ (stepPaddle delta dir2 paddle2)
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.
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.
140gameState = foldp stepGame defaultGame input
225gameState = foldp stepGame defaultGame input
229+------------------------------------------------------------------------
230+------ Displaying the Game ------
231+------------------------------------------------------------------------
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
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
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.
159view = lift2 display Win.dimensions gameState
257view = lift2 display Win.dimensions gameState
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.
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
269+-- And finally, we display the view of the game to the user: Pong in Elm!
165−main = view
271+main = view
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.
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