elm/Examples/elm-js/Pong/Pong.elm

284 lines
10 KiB
Elm
Raw Normal View History

2012-08-17 14:38:41 +00:00
module Pong where
import Foreign.JavaScript
import Signal.Keyboard.Raw
import Signal.Window as Win
------------------------------------------------------------------------
------ Extracting timesteps from JavaScript ------
------------------------------------------------------------------------
2012-08-17 17:05:18 +00:00
-- Export our desired FPS to JavaScript. We just want a steady 30 frames
-- per second for this game.
2012-08-17 14:38:41 +00:00
desiredFPS = constant (castIntToJSNumber 30)
foreign export jsevent "desiredFPS"
desiredFPS :: Signal JSNumber
2012-08-17 17:05:18 +00:00
-- Import the current time from JavaScript. Events will come in roughly
-- 30 times per second.
2012-08-17 14:38:41 +00:00
foreign import jsevent "trigger" (castIntToJSNumber 0)
jsTime :: Signal JSNumber
time = lift castJSNumberToFloat jsTime
2012-08-17 17:05:18 +00:00
-- Determine the time that has elapsed since the last event. This is
-- our timestep, telling us how far everything should move in the next
-- frame.
2012-08-17 14:38:41 +00:00
delta = lift snd $ foldp (\t1 (t0,d) -> (t1, t1-t0)) (0,0) time
2012-08-17 17:05:18 +00:00
2012-08-17 14:38:41 +00:00
------------------------------------------------------------------------
------ Modelling user input ------
------------------------------------------------------------------------
-- Each paddle can be moving up, down, or not at all. We'll call this
-- the `direction' of the paddle.
data Direction = Up | Neutral | Down
-- During gameplay, all keyboard input is about the position of the two
-- paddles. So the keyboard input can be reduced to two `Directions'.
2012-08-17 17:05:18 +00:00
-- Furthermore, the SPACE key is used to start the game between rounds,
-- so we also need a boolean value to represent whether it is pressed.
2012-08-17 14:38:41 +00:00
data KeyInput = KeyInput Bool Direction Direction
2012-08-17 17:05:18 +00:00
defaultKeyInput = KeyInput False Neutral Neutral
2012-08-17 14:38:41 +00:00
-- Now we determine how to update the direction of a paddle based on
-- keyboard input. The first two args of `updatePaddle` are the key
2012-08-17 17:05:18 +00:00
-- codes of the up and down keys. The next argument is the key that
-- has been pressed. The last argument is the previously calculated
-- direction of the paddle, which is getting updated.
2012-08-17 14:38:41 +00:00
updateDirection upKey downKey key direction =
case direction of
{ Up -> if key == downKey then Neutral else Up
; Down -> if key == upKey then Neutral else Down
; Neutral -> if key == upKey then Up else
if key == downKey then Down else Neutral
}
updateDirection1 = updateDirection 87 83 -- 'w' for up and 's' for down
updateDirection2 = updateDirection 38 40 -- 'UP' for up and 'DOWN' for down
2012-08-17 17:05:18 +00:00
-- Update the keyboard input representation based on a particular key press.
2012-08-17 14:38:41 +00:00
updateInput key (KeyInput space dir1 dir2) =
KeyInput (space || key == 32)
(updateDirection1 key dir1)
(updateDirection2 key dir2)
2012-08-17 17:05:18 +00:00
-- `keysDown` has type (Signal [Int]) so we need to fold `updateInput`
-- over all of the keys that are currently pressed to get the current
-- keyboard state.
keyInput = lift (foldl updateInput defaultKeyInput) keysDown
2012-08-17 14:38:41 +00:00
------------------------------------------------------------------------
2012-08-17 17:05:18 +00:00
------ Combining all inputs ------
2012-08-17 14:38:41 +00:00
------------------------------------------------------------------------
-- The inputs to this game include a timestep (which we extracted from
-- JavaScript) and the keyboard input from the users.
data Input = Input Float KeyInput
2012-08-17 17:05:18 +00:00
-- Combine both kinds of inputs and filter out keyboard events. We only
-- want the game to refresh on clock-ticks, not key presses too.
input = sampleOn delta (lift2 Input delta keyInput)
2012-08-17 14:38:41 +00:00
------------------------------------------------------------------------
------ Modelling Pong / a State Machine ------
------------------------------------------------------------------------
2012-08-17 17:05:18 +00:00
-- Pong has two obvious components: the ball and two paddles.
2012-08-17 14:38:41 +00:00
data Paddle = Paddle Float -- y-position
data Ball = Ball (Float,Float) (Float,Float) -- position and velocity
2012-08-17 17:05:18 +00:00
-- But we also want to keep track of the current score and whether
-- the ball is currently in play. This will allow us to have rounds
-- of play rather than just having the ball move around continuously.
2012-08-17 14:38:41 +00:00
data Score = Score Int Int
data State = Play | BetweenRounds
2012-08-17 17:05:18 +00:00
-- Together, this information makes up the state of the game. We model
-- Pong by using the inputs (defined above) to update the state of the
-- game!
2012-08-17 14:38:41 +00:00
data GameState = GameState State Score Ball Paddle Paddle
2012-08-17 17:05:18 +00:00
-- I have chosen to parameterize the size of the board, so it can
-- be changed with minimal effort.
2012-08-17 14:38:41 +00:00
gameWidth = 600
gameHeight = 400
halfWidth = gameWidth / 2
halfHeight = gameHeight / 2
2012-08-17 17:05:18 +00:00
-- Before we can update anything, we must first define the default
-- configuration of the game. In our case we want to start between
-- rounds with a score of zero to zero.
2012-08-17 14:38:41 +00:00
defaultGame = GameState BetweenRounds
(Score 0 0)
(Ball (halfWidth, halfHeight) (150,150))
(Paddle halfHeight)
(Paddle halfHeight)
2012-08-17 17:05:18 +00:00
------------------------------------------------------------------------
------ Stepping from State to State ------
------------------------------------------------------------------------
-- Now to step the game from one state to another. We can break this up
-- into smaller components.
-- First, we define a step function for updating the position of
-- paddles. It depends on our timestep and a desired direction (given
-- by keyboard input).
2012-08-17 14:38:41 +00:00
stepPaddle delta dir (Paddle y) =
case dir of
{ Up -> Paddle . clamp 20 (gameHeight-20) $ y - 200 * delta
; Down -> Paddle . clamp 20 (gameHeight-20) $ y + 200 * delta
; Neutral -> Paddle y
}
2012-08-17 17:05:18 +00:00
-- We must also step the ball forward. This is more complicated due to
-- the many kinds of collisions that can happen. All together, this
-- function figures out the new velocity of the ball based on
-- collisions with the top and bottom borders and collisions with the
-- paddles. This new velocity is used to calculate a new position.
-- This function also determines whether a point has been scored and
-- who receives the point. Thus, its output is a new Ball and points
-- to be added to each player.
2012-08-17 14:38:41 +00:00
stepBall delta (Ball (x,y) (vx,vy)) (Paddle y1) (Paddle y2) =
let { makePositive n = if n > 0 then n else 0-n
; makeNegative n = if n > 0 then 0-n else n
; near epsilon n x = x > n - epsilon && x < n + epsilon
; vx' = if near 20 y1 y && near 8 25 x
then makePositive vx else
if near 20 y2 y && near 8 (gameWidth - 25) x
then makeNegative vx else vx
; vy' = if y < 7 then makePositive vy else
if y > gameHeight - 7 then makeNegative vy else vy
; scored = x > gameWidth || x < 0
; x' = if scored then halfWidth else x + vx' * delta
; y' = if scored then halfHeight else y + vy' * delta
}
in ( Ball (x',y') (vx',vy')
, if x > gameWidth then 1 else 0
, if x < 0 then 1 else 0
)
2012-08-17 17:05:18 +00:00
-- Finally, we define a step function for the entire game. This steps from state to
-- state based on the inputs to the game.
2012-08-17 14:38:41 +00:00
stepGame (Input delta (KeyInput space dir1 dir2))
(GameState state (Score s1 s2) ball paddle1 paddle2) =
2012-08-17 17:05:18 +00:00
let { (ball',s1',s2') = if state == Play then stepBall delta ball paddle1 paddle2
else (ball, 0, 0)
2012-08-17 14:38:41 +00:00
; state' = case state of { Play -> if s1' /= s2' then BetweenRounds else state
; BetweenRounds -> if space then Play else state }
2012-08-17 17:05:18 +00:00
} in GameState state'
(Score (s1+s1') (s2+s2'))
ball'
(stepPaddle delta dir1 paddle1)
(stepPaddle delta dir2 paddle2)
-- Now we put it all together. We have a signal of inputs that changes whenever there
-- is a clock tick. This input signal carries the all the information we need about
-- the keyboard. We also have a step function that steps from one game-state to the
-- next based on some inputs.
-- The `gameState` signal steps forward every time a new input comes in. It starts
-- as the default game and progresses based on user behavior.
2012-08-17 14:38:41 +00:00
gameState = foldp stepGame defaultGame input
2012-08-17 17:05:18 +00:00
------------------------------------------------------------------------
------ Displaying the Game ------
------------------------------------------------------------------------
-- This function takes a GameState and turns it into something a user
-- can see and understand. It is totally independent of how the game
-- updates, it only needs to know the current game state. This allows us
-- to change how the game looks without changing any of the logic of the
-- game.
2012-08-17 14:38:41 +00:00
display (w,h) (GameState state (Score p1 p2) (Ball pos _) (Paddle y1) (Paddle y2)) =
let score = width w . centeredText . Text.height 4 $
show p1 ++ toText " " ++ show p2
in layers
[ if state == Play then score else
score `above` (width w . centeredText $ toText "Press SPACE to begin.")
, let pongGreen = rgb 60 100 60 in
size w h . box 5 $ collage gameWidth gameHeight
[ filled pongGreen (rect gameWidth gameHeight (halfWidth,halfHeight))
, filled white (oval 15 15 pos)
, filled white (rect 10 40 ( 20, y1))
, filled white (rect 10 40 (gameWidth - 20, y2))
]
]
2012-08-17 17:05:18 +00:00
-- We can now define a view of the game (a signal of Elements) that changes
-- as the GameState changes. This is what the users will see.
2012-08-17 14:38:41 +00:00
view = lift2 display Win.dimensions gameState
2012-08-17 17:05:18 +00:00
-- Here we tell the JavaScript FPS manager that all of the computations for
-- this frame are complete. This allows the manager to calculate when the next
-- step should happen.
2012-08-17 14:38:41 +00:00
done = lift (\_ -> castBoolToJSBool True) view
foreign export jsevent "finished"
done :: Signal JSBool
2012-08-17 17:05:18 +00:00
-- And finally, we display the view of the game to the user: Pong in Elm!
main = view
-- Note that the structure of this game neatly divides Pong into three major
-- parts: modeling the game, updating the game, and viewing the game. It may
-- be helpful to think of it as a more functional (i.e. less imperative)
-- version of the Model-View-Controller paradigm.
-- Hopefully this game also displays some of the strengths of Functional
-- Reactive Programming. We have written an entire GUI/game without any
-- imperative code! No global mutable state, no flipping pixels, no
-- destructive updates. In fact, Elm disallows all of these things at the
-- language level. Thus, good design is a requirement, not just a self-inforced
-- suggestion.