|
| 1 | +-- A demo game put together in less than 2 hours to show how easy |
| 2 | +-- game programming is in Haskell. |
| 3 | +-- |
| 4 | +-- If you want to use the wiimote, compile the game with the flag |
| 5 | +-- -rtsopts |
| 6 | +-- and run it with |
| 7 | +-- +RTS -V0 |
| 8 | +-- import Control.Concurrent |
| 9 | +import Control.Monad |
| 10 | +import Graphics.UI.SDL as SDL |
| 11 | +import Graphics.UI.SDL.Image as SDL |
| 12 | +import Graphics.UI.SDL.TTF as TTF |
| 13 | +import System.CWiid |
| 14 | +import System.Random |
| 15 | + |
| 16 | +-- * Game State |
| 17 | +data GameState = GameState |
| 18 | + { raindrops :: [(Float, Float)] |
| 19 | + , paddle :: (Float, Float) |
| 20 | + , lives :: Int |
| 21 | + , points :: Int |
| 22 | + , lastRaindrop :: Float |
| 23 | + , lastFrame :: Float -- Time of last frame |
| 24 | + , randomGen :: StdGen |
| 25 | + } |
| 26 | + deriving Show |
| 27 | + |
| 28 | +level :: GameState -> Int |
| 29 | +level gameState = points gameState `div` dropsPerLevel |
| 30 | + |
| 31 | +updatePaddlePos :: GameState -> Float -> GameState |
| 32 | +updatePaddlePos gameState newX = |
| 33 | + gameState { paddle = (min newX (width - paddleW), height - paddleMargin) } |
| 34 | + |
| 35 | +initialGameState :: StdGen -> GameState |
| 36 | +initialGameState gen = |
| 37 | + GameState [(width / 2, dropMargin)] (0, height - paddleMargin) maxLives 0 0 0 gen |
| 38 | + |
| 39 | +-- * Runtime environment (resources, devices) |
| 40 | +data Env = Env |
| 41 | + { bgImg :: Surface |
| 42 | + , dropImg :: Surface |
| 43 | + , paddleImg :: Surface |
| 44 | + , font :: TTF.Font |
| 45 | + , cwiid :: Maybe CWiidWiimote |
| 46 | + } |
| 47 | + |
| 48 | +main :: IO () |
| 49 | +main = do |
| 50 | + -- Initialise SDL |
| 51 | + SDL.init [InitEverything] |
| 52 | + ttfOk <- TTF.init |
| 53 | + |
| 54 | + when ttfOk $ do |
| 55 | + font <- TTF.tryOpenFont "data/font.ttf" 32 |
| 56 | + bg <- load "data/background.png" |
| 57 | + |
| 58 | + -- Load with a mask |
| 59 | + drop <- load "data/cherry.png" |
| 60 | + t <- mapRGB (surfaceGetPixelFormat drop) 0 255 0 |
| 61 | + setColorKey drop [SrcColorKey, RLEAccel] t |
| 62 | + |
| 63 | + -- Load with a mask |
| 64 | + paddle <- load "data/player.png" |
| 65 | + t <- mapRGB (surfaceGetPixelFormat paddle) 0 255 0 |
| 66 | + setColorKey paddle [SrcColorKey, RLEAccel] t |
| 67 | + |
| 68 | + -- Create window, no mouse |
| 69 | + SDL.setVideoMode (round width) (round height) 32 [SWSurface] |
| 70 | + SDL.setCaption "Raindrops" "" |
| 71 | + SDL.showCursor False |
| 72 | + |
| 73 | + -- Create Random number generator |
| 74 | + gen <- getStdGen |
| 75 | + |
| 76 | + -- Initialise input devices |
| 77 | + wiimote <- initializeWiimote |
| 78 | + |
| 79 | + case font of |
| 80 | + Nothing -> return () |
| 81 | + Just ttf -> let env = Env bg drop paddle ttf wiimote |
| 82 | + gs = initialGameState gen |
| 83 | + in run env gs |
| 84 | + |
| 85 | +-- Game loop |
| 86 | +run :: Env -> GameState -> IO () |
| 87 | +run env gameState = do |
| 88 | + -- IO: Sense (input) |
| 89 | + gameStateP <- calculatePaddlePos env gameState |
| 90 | + |
| 91 | + -- IO: Sense (time) |
| 92 | + newTime <- fmap fromIntegral SDL.getTicks |
| 93 | + let dt = newTime - lastFrame gameStateP |
| 94 | + let gameStateT = gameStateP { lastFrame = newTime } |
| 95 | + |
| 96 | + -- Physics (movement) |
| 97 | + let gameStateP = moveForward dt gameStateT |
| 98 | + |
| 99 | + -- Physics (collisions) |
| 100 | + let gameStateC = raindropsBottom (raindropsPaddle gameStateP) |
| 101 | + |
| 102 | + -- Logic (new raindrops) |
| 103 | + let dtLastRaindrop = newTime - lastRaindrop gameStateC |
| 104 | + gameStateN = |
| 105 | + if dtLastRaindrop > dropDelay |
| 106 | + then let (newX, gen') = randomR (0, round (width - dropW) :: Int) |
| 107 | + (randomGen gameStateC) |
| 108 | + oldDrops = raindrops gameStateC |
| 109 | + in gameStateC { raindrops = (fromIntegral newX, dropMargin) : oldDrops |
| 110 | + , lastRaindrop = newTime |
| 111 | + , randomGen = gen' |
| 112 | + } |
| 113 | + else gameStateC |
| 114 | + |
| 115 | + -- Logic (game over) |
| 116 | + let gameStateO = if lives gameStateN < 0 |
| 117 | + then initialGameState (randomGen gameStateN) |
| 118 | + else gameStateN |
| 119 | + |
| 120 | + -- IO: Paint |
| 121 | + render env gameStateO |
| 122 | + |
| 123 | + -- Loop |
| 124 | + run env gameStateO |
| 125 | + |
| 126 | +-- * Physics |
| 127 | +moveForward :: Float -> GameState -> GameState |
| 128 | +moveForward dt gs = gs { raindrops = movedRaindrops } |
| 129 | + where movedRaindrops = map moveRaindrop (raindrops gs) |
| 130 | + moveRaindrop (x,y) = (x, y + 0.1 * dt * fromIntegral (level gs + 1)) |
| 131 | + |
| 132 | +-- * Collisions |
| 133 | + |
| 134 | +-- ** Collisions with paddle |
| 135 | +raindropsPaddle :: GameState -> GameState |
| 136 | +raindropsPaddle gs = gs { raindrops = remainingRaindrops |
| 137 | + , points = points gs + pts |
| 138 | + } |
| 139 | + where remainingRaindrops = filter (not.collidesWithPaddle) (raindrops gs) |
| 140 | + pts = length (raindrops gs) - length remainingRaindrops |
| 141 | + collidesWithPaddle (x,y) = (within x paddleXMin paddleXMax |
| 142 | + || within paddleXMin x (x + dropW)) |
| 143 | + && (within y paddleYMin paddleYMax |
| 144 | + || within paddleYMin y (y + dropH)) |
| 145 | + where paddleXMin = fst (paddle gs) |
| 146 | + paddleXMax = fst (paddle gs) + paddleW |
| 147 | + paddleYMin = snd (paddle gs) |
| 148 | + paddleYMax = snd (paddle gs) + paddleH |
| 149 | + within x xMin xMax = x >= xMin && x <= xMax |
| 150 | + |
| 151 | +-- ** Collisions with bottom |
| 152 | +raindropsBottom :: GameState -> GameState |
| 153 | +raindropsBottom gs = gs { raindrops = remainingRaindrops |
| 154 | + , lives = decreasedLives |
| 155 | + } |
| 156 | + where remainingRaindrops = filter (\(_,y) -> y < height) (raindrops gs) |
| 157 | + decreasedLives = lives gs - diffDrops |
| 158 | + diffDrops = length (raindrops gs) - length remainingRaindrops |
| 159 | + |
| 160 | +-- * Input sensing |
| 161 | +calculatePaddlePos :: Env -> GameState -> IO GameState |
| 162 | +calculatePaddlePos env gs = case cwiid env of |
| 163 | + Nothing -> calculatePaddlePosSDL env gs |
| 164 | + Just wm -> do (x,y) <- senseWiimote wm |
| 165 | + return (updatePaddlePos gs x) |
| 166 | + |
| 167 | +-- ** SDL Sensing |
| 168 | +calculatePaddlePosSDL :: Env -> GameState -> IO GameState |
| 169 | +calculatePaddlePosSDL env gs = do |
| 170 | + e <- pollEvent |
| 171 | + case e of |
| 172 | + NoEvent -> return gs |
| 173 | + MouseMotion x y _ _ -> calculatePaddlePosSDL env (updatePaddlePos gs (fromIntegral x)) |
| 174 | + _ -> calculatePaddlePosSDL env gs |
| 175 | + |
| 176 | +-- * Output drawing |
| 177 | +render :: Env -> GameState -> IO () |
| 178 | +render env gameState = do |
| 179 | + |
| 180 | + screen <- getVideoSurface |
| 181 | + |
| 182 | + -- Clear screen |
| 183 | + SDL.blitSurface (bgImg env) Nothing |
| 184 | + screen (Just (SDL.Rect 0 0 (round width) (round height))) |
| 185 | + |
| 186 | + -- Paint each raindrop |
| 187 | + let paintADropAt (x,y) = do |
| 188 | + SDL.blitSurface (dropImg env) Nothing |
| 189 | + screen (Just (SDL.Rect (round x) (round y) (round dropW) (round dropH))) |
| 190 | + |
| 191 | + mapM_ paintADropAt (raindrops gameState) |
| 192 | + |
| 193 | + -- Paint the paddle |
| 194 | + let (x,y) = paddle gameState |
| 195 | + SDL.blitSurface (paddleImg env) Nothing |
| 196 | + screen (Just (SDL.Rect (round x) (round y) (round paddleW) (round paddleH))) |
| 197 | + |
| 198 | + -- Paint points, lives |
| 199 | + let ttf = font env |
| 200 | + message <- TTF.renderTextSolid |
| 201 | + ttf |
| 202 | + ("Level " ++ show (level gameState) |
| 203 | + ++ " / Lives " ++ show (lives gameState)) |
| 204 | + (SDL.Color 128 128 128) |
| 205 | + let w1 = SDL.surfaceGetWidth message |
| 206 | + h1 = SDL.surfaceGetHeight message |
| 207 | + SDL.blitSurface message Nothing |
| 208 | + screen (Just (SDL.Rect 10 10 w1 h1)) |
| 209 | + |
| 210 | + -- Present |
| 211 | + SDL.flip screen |
| 212 | + |
| 213 | +-- * Game constants |
| 214 | +paddleW, paddleH :: Float |
| 215 | +paddleW = 126 |
| 216 | +paddleH = 31 |
| 217 | + |
| 218 | +paddleMargin :: Float |
| 219 | +paddleMargin = 60 |
| 220 | + |
| 221 | +width, height :: Float |
| 222 | +width = 800 |
| 223 | +height = 480 |
| 224 | + |
| 225 | +dropW, dropH :: Float |
| 226 | +dropW = 70 |
| 227 | +dropH = 70 |
| 228 | + |
| 229 | +dropMargin :: Float |
| 230 | +dropMargin = 10 |
| 231 | + |
| 232 | +dropsPerLevel :: Int |
| 233 | +dropsPerLevel = 20 |
| 234 | + |
| 235 | +dropDelay :: Float |
| 236 | +dropDelay = 500 |
| 237 | + |
| 238 | +maxLives :: Int |
| 239 | +maxLives = 10 |
| 240 | + |
| 241 | +-- * Wiimote sensing |
| 242 | + |
| 243 | +-- | Initializes the wiimote, optionally returning the sensing function. It |
| 244 | +-- returns Nothing if the Wiimote cannot be detected. Users should have a BT |
| 245 | +-- device and press 1+2 to connect to it. A message is shown on stdout. |
| 246 | +initializeWiimote :: IO (Maybe CWiidWiimote) |
| 247 | +initializeWiimote = do |
| 248 | + putStrLn "Initializing WiiMote. Please press 1+2 to connect." |
| 249 | + wm <- cwiidOpen |
| 250 | + case wm of |
| 251 | + Nothing -> return () |
| 252 | + Just wm' -> void $ cwiidSetRptMode wm' 15 -- Enable button reception, acc and IR |
| 253 | + return wm |
| 254 | + |
| 255 | +senseWiimote :: CWiidWiimote -> IO (Float, Float) |
| 256 | +senseWiimote wmdev = do |
| 257 | + irs <- cwiidGetIR wmdev |
| 258 | + |
| 259 | + -- Obtain positions of leds 1 and 2 (with a normal wii bar, those |
| 260 | + -- will be the ones we use). |
| 261 | + let led1 = irs!!0 |
| 262 | + led2 = irs!!1 |
| 263 | + |
| 264 | + -- Calculate mid point between sensor bar leds |
| 265 | + let posX = ((cwiidIRSrcPosX led1) + (cwiidIRSrcPosX led2)) `div` 2 |
| 266 | + posY = ((cwiidIRSrcPosY led1) + (cwiidIRSrcPosY led2)) `div` 2 |
| 267 | + |
| 268 | + -- Calculate proportional coordinates |
| 269 | + let propX = fromIntegral (1024 - posX) / 1024.0 |
| 270 | + propY = fromIntegral (max 0 (posY - 384)) / 384.0 |
| 271 | + |
| 272 | + -- Calculate game area coordinates |
| 273 | + let finX = width * propX |
| 274 | + finY = height * propY |
| 275 | + |
| 276 | + return (finX, finY) |
0 commit comments