diff --git a/.travis.yml b/.travis.yml index 2cdda45..82290e5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,34 +1,38 @@ +dist: bionic + # The following enables several GHC versions to be tested; often it's enough to # test only against the last release in a major GHC version. Feel free to omit # lines listings versions you don't need/want testing for. -language: c env: - - GHCVER=7.6.3 - - GHCVER=7.8.4 - - GHCVER=7.10.3 - - GHCVER=8.0.1 + - CABALVER=2.4 GHCVER=8.2.2 + - CABALVER=2.4 GHCVER=8.4.4 + - CABALVER=2.4 GHCVER=8.6.5 + - CABALVER=2.4 GHCVER=8.8.4 + - CABALVER=2.4 GHCVER=8.10.4 +# - CABALVER=head GHCVER=head # see section about GHC HEAD snapshots -# Note: the distinction between `before_install` and `install` is not important. before_install: - - unset CC + - travis_retry sudo add-apt-repository -y ppa:hvr/ghc + - travis_retry sudo apt-get update + - travis_retry sudo apt-get install --yes libsdl1.2-dev libsdl-image1.2-dev libsdl-ttf2.0-dev libsdl-mixer1.2-dev + - travis_retry sudo apt-get install --yes cabal-install-$CABALVER ghc-$GHCVER # see note about happy/alex + - export PATH=/opt/ghc/$GHCVER/bin:/opt/cabal/$CABALVER/bin:$PATH + - cabal --version + - ghc --version + - travis_retry cabal update + install: - - travis_retry sudo add-apt-repository -y ppa:hvr/ghc - - travis_retry sudo apt-get update - - travis_retry sudo apt-get install libsdl1.2-dev libsdl-image1.2-dev libsdl-ttf2.0-dev libsdl-mixer1.2-dev - - travis_retry sudo apt-get install cabal-install-1.24 ghc-$GHCVER-prof ghc-$GHCVER-dyn alex-3.1.4 happy-1.19.5 - - export PATH=$HOME/.cabal/bin:/opt/ghc/$GHCVER/bin:/opt/cabal/1.24/bin:/opt/alex/3.1.4/bin:/opt/happy/1.19.5/bin:$PATH - - travis_retry cabal update + - travis_retry cabal install -f-wiimote -f-kinect --only-dependencies --enable-tests script: - - travis_retry cabal install --run-tests -v -j1 -f-wiimote -f-kinect - -notifications: - email: true + - travis_retry cabal configure -f-wiimote -f-kinect --enable-tests && cabal build && cabal test branches: only: - master - - develop + - /^develop.*/ + - /^hotfix.*/ + - /^release.*/ deploy: provider: hackage @@ -37,4 +41,4 @@ deploy: secure: WQp6LHDPxGPlaFsFK1CGZaW5XSFX4+f3hLkglYFKTUzNNfmq0QXmqKJROLWrvwvTmipxguM0qZdsJTYIKJdIeECyD7dR2sZHMVpoaXgle9QkYyvoAs1o9wMbdjc/vYK4QpG9we9TPwHNalXJIkwKtoWA7IDxKQ56ln2sQLx6Qz2K/tDnq8bHNhe4U/7YRGc9K2TdiFkcimPY6BqPG/mRR8AI+q3P3YLg4x4GdDKXKtxwRdWuRQaidpog6H4xbdVAjEIlbCJ/Re1Ofjq1J5ESYN5NM84+b8TiMoVs7qeiqWNuid8YCGKUpuJ4JXPai63+EHP9vr0+DFxFZPEMm94f/Lo6B/2BtUB5KoC1baVItMVte9pqcjr1slf46VoeCtXmfCa3pKBQE4TZQl25pSGISHfYaiRuCZ/zo1Rd6PPOgbdaR9agHOVMugGkO55p8qWwhL7kle/Xu8oEyOiRTc/M1PAKXe5Fa1+Vz46FNtMqTlZo/Sa5ySC3QpBfhexXqbD6fO3032VRX9oNPFXuIwwjXQ0D2lGENHxxVahcqIPC4AZVDmL+9ME7k0JVDSk7zW5gQgGdEWvtCBTLahBBwbgFHoRZxlymGnMJWovcgP9cOyrjlbd/LGjd0Hp9LEaCPmMHE+yM0cgVpVUrFtTNt4O3nl14KLYCKZ+0mwkryqEF/LI= on: branch: master - condition: "$GHCVER = 8.0.1" + condition: ${TRAVIS_HASKELL_VERSION}=8.4.4 diff --git a/CHANGELOG b/CHANGELOG index 3cad21f..425822e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,18 @@ +2023-04-08 Ivan Perez : + * Version bump (0.1.6) (#108). + * Simplify travis file (#58). + * Update GHC versions, condition, publication in travis (#77). + * Update to support Yampa 0.13 (#82). + * Update pointer to hands-on file (#85). + * Add support for bearriver (#95). + * Support stopping the game with letter q (#99). + * Update installation instructions (#102). + * Bump version bounds of Yampa/bearriver (#104). + * Update dist in travis config file to Bionic (#105). + * Update GHC versions in travis config file (#106). + * Enable CI builds of branches used by our development process (#107). + * Conform to style guide (#103). + 2017-11-02 Ivan Perez : * Version bump (0.1.5.4). * Adds new levels. diff --git a/README.md b/README.md index 2ac2b12..ce2b515 100644 --- a/README.md +++ b/README.md @@ -44,9 +44,8 @@ The game is available on [hackage](https://hackage.haskell.org/package/SpaceInva ``` $ cabal update -$ cabal sandbox init $ cabal install haskanoid -$ ./.cabal-sandbox/bin/haskanoid +$ $HOME/.cabal/bin/haskanoid ``` If you want to explore the code and possibly make changes, do the following: @@ -55,9 +54,8 @@ If you want to explore the code and possibly make changes, do the following: $ cabal update $ cabal unpack haskanoid # or git clone http://github.com/ivanperez-keera/haskanoid $ cd haskanoid-* # Game resources are here -$ cabal sandbox init $ cabal install -$ ./dist/build/haskanoid/haskanoid +$ $HOME/.cabal/bin/haskanoid ``` To play it with the wiimote, you need to run the program with the special @@ -102,16 +100,23 @@ those with: ``` $ cabal unpack haskanoid ## Or git clone this-repo $ cd haskanoid-* -$ cabal sandbox init $ cabal install --only-dependencies -$ cabal configure && cabal haddock --executables --internal +$ cabal haddock --haddock-executables --haddock-internal ``` +You may also want to add the flags `-f-wiimote` and `-f-kinect` to the cabal +commands above to install without wiimote and kinect support. + # Related projects * Yampa (http://github.com/ivanperez-keera/Yampa), the Arrowized Functional Reactive Programming implementation created by Antony Courtney and Henrik Nilsson. +* Dunai (http://github.com/ivanperez-keera/dunai), an extremely powerful +reactive programming implementation capable of doing Classic and Arrowized +Functional Reactive Programming, Reactive Programming, Stream Programming, and +others. It contains a Yampa replacement that is capable of running Haskanoid. + * hcwiid (http://github.com/ivanperez-keera/hcwiid), a wrapper around the cwiid library to communicate with Wiimotes. @@ -129,7 +134,7 @@ Play](https://play.google.com/store/apps/details?id=uk.co.keera.games.magiccooki # Hands-on -In the [hands-on file](https://github.com/ivanperez-keera/haskanoid/blob/master/hands-on.md) +In the [hands-on file](https://github.com/ivanperez-keera/haskanoid/blob/develop/docs/hands-on.md) you find ideas to improve haskanoid while focussing on (game) programming related areas that you might want to dive in deeper. The areas are: functional (reactive) programming, performance, human-computer interaction and diff --git a/hands-on.md b/docs/hands-on.md similarity index 100% rename from hands-on.md rename to docs/hands-on.md diff --git a/docs/ppdp2014-tutorial-4up.pdf b/docs/slides/ppdp2014-tutorial-4up.pdf similarity index 100% rename from docs/ppdp2014-tutorial-4up.pdf rename to docs/slides/ppdp2014-tutorial-4up.pdf diff --git a/docs/ppdp2014-tutorial.pdf b/docs/slides/ppdp2014-tutorial.pdf similarity index 100% rename from docs/ppdp2014-tutorial.pdf rename to docs/slides/ppdp2014-tutorial.pdf diff --git a/haskanoid.cabal b/haskanoid.cabal index a2eb110..83a2bce 100644 --- a/haskanoid.cabal +++ b/haskanoid.cabal @@ -4,13 +4,11 @@ -- The name of the package. name: haskanoid --- The package version. See the Haskell package versioning policy (PVP) --- for standards guiding when and how versions should be incremented. --- http://www.haskell.org/haskellwiki/Package_versioning_policy +-- The package version. We follow semantic versioning. -- PVP summary: +-+------- breaking API changes --- | | +----- non-breaking API additions --- | | | +--- code changes with no API change -version: 0.1.5.4 +-- | | +----- non-breaking changes +-- | | | +version: 0.1.6 -- A short (one-line) description of the package. synopsis: A breakout game written in Yampa using SDL @@ -45,7 +43,7 @@ category: Game, Reactivity, FRP build-type: Simple -- Constraint on the version of Cabal needed to build this package. -cabal-version: >=1.8 +cabal-version: >=1.10 data-files: data/*.png data/*.wav data/*.mp3 data/*.ttf @@ -57,6 +55,10 @@ Flag kinect Description: Enable Kinect support (with freenect) Default: True +Flag bearriver + Description: Compile with bearriver instead of Yampa + Default: False + executable haskanoid -- .hs or .lhs file containing the Main module. main-is: Main.hs @@ -99,12 +101,20 @@ executable haskanoid -- Other library packages from which modules are imported. build-depends: base >= 4.6 && < 5, - transformers >= 0.3 && < 0.6, - mtl, + IfElse, MissingH, - Yampa >= 0.9.6 && < 0.12, + mtl, SDL, SDL-image, SDL-mixer, SDL-ttf, - IfElse + simple-affine-space, + transformers >= 0.3 && < 0.6 + + default-language: + Haskell2010 + + if flag(bearriver) + build-depends: bearriver >= 0.9.6 && < 0.15 + else + build-depends: Yampa >= 0.13 && < 0.15 if flag(wiimote) build-depends: hcwiid diff --git a/src/Audio.hs b/src/Audio.hs index 0e891b8..ce165ad 100644 --- a/src/Audio.hs +++ b/src/Audio.hs @@ -1,31 +1,38 @@ --- | A layer of abstraction on top of SDL audio. +-- | +-- Copyright : (c) Keera Studios, 2010-2014. +-- License : See LICENSE file. +-- Maintainer : Ivan Perez +-- +-- A layer of abstraction on top of SDL audio. -- -- It plays audio soundfx asynchronously (in a new thread), which means that -- programs must be compiled with the threaded Runtime System (ghc flag is -- -threaded). --- --- This module is 2010-2014 (c) Keera Studios, redistributed with permission. module Audio - (Music(..), - Audio(..), - initAudio, - loadAudio, - loadMusic, - playMusic, - playFile, - stopMusic, - musicPlaying) where + ( Music(..) + , Audio(..) + , initAudio + , loadAudio + , loadMusic + , playMusic + , playFile + , stopMusic + , musicPlaying + ) + where -import Control.Applicative ((<$>)) -import Control.Monad -import Control.Concurrent -import qualified Graphics.UI.SDL.Mixer.General as SDL.Mixer +-- External imports +import Control.Applicative ((<$>)) +import Control.Concurrent +import Control.Monad import qualified Graphics.UI.SDL.Mixer.Channels as SDL.Mixer.Channels -import qualified Graphics.UI.SDL.Mixer.Music as SDL.Mixer.Music -import qualified Graphics.UI.SDL.Mixer.Types as SDL.Mixer.Types -import qualified Graphics.UI.SDL.Mixer.Samples as SDL.Mixer.Samples +import qualified Graphics.UI.SDL.Mixer.General as SDL.Mixer +import qualified Graphics.UI.SDL.Mixer.Music as SDL.Mixer.Music +import qualified Graphics.UI.SDL.Mixer.Samples as SDL.Mixer.Samples +import qualified Graphics.UI.SDL.Mixer.Types as SDL.Mixer.Types data Music = Music { musicName :: String, unMusic :: SDL.Mixer.Types.Music } + data Audio = Audio { audioName :: String, unAudio :: SDL.Mixer.Types.Chunk } -- | Initialize the audio subsystem. @@ -63,6 +70,6 @@ loadAudio fp = fmap (Audio fp) <$> SDL.Mixer.Samples.tryLoadWAV fp -- This function spawns a new OS thread. Remember to compile your program -- with the threaded RTS. playFile :: Audio -> Int -> IO () -playFile wav t = void $ forkOS $ do +playFile wav t = void $ forkOS $ do _v <- SDL.Mixer.Channels.playChannel (-1) (unAudio wav) 0 threadDelay (t * 1000) diff --git a/src/Constants.hs b/src/Constants.hs index d19fadb..e430a12 100644 --- a/src/Constants.hs +++ b/src/Constants.hs @@ -1,20 +1,32 @@ +-- | +-- Copyright : (c) Ivan Perez & Henrik Nilsson, 2014. +-- License : See LICENSE file. +-- Maintainer : Ivan Perez +-- +-- Game constants. module Constants where +-- External imports import FRP.Yampa + +-- Internal imports import Physics.TwoDimensions.Dimensions width :: Double -width = 640 +width = 640 + height :: Double height = 600 -gameTop :: Double -gameTop = 100 -gameLeft :: Double -gameLeft = 0 +gameTop :: Double +gameTop = 100 + +gameLeft :: Double +gameLeft = 0 gameWidth :: Double gameWidth = width + gameHeight :: Double gameHeight = height - gameTop @@ -24,20 +36,27 @@ loadingDelay = 2 -- seconds paddleWidth, paddleHeight :: Double paddleWidth = 104 paddleHeight = 24 + paddleMargin :: Double paddleMargin = 50 + ballWidth, ballHeight :: Double -ballWidth = 10 -ballHeight = 10 +ballWidth = 10 +ballHeight = 10 + ballMargin :: Double -ballMargin = 30 +ballMargin = 30 + blockWidth, blockHeight :: Double -blockWidth = 64 -blockHeight = 32 +blockWidth = 64 +blockHeight = 32 + blockSeparation :: Double blockSeparation = 10 + maxBlockLife :: Int maxBlockLife = 3 + minBlockLife :: Int minBlockLife = 1 @@ -57,13 +76,6 @@ velTrans = 0.2 -- Max speed maxVNorm :: Double maxVNorm = 300 - --- Delays --- restartDelay :: Time --- restartDelay = 3 --- --- wonDelay :: Time --- wonDelay = 3 -- * Debugging diff --git a/src/Control/Extra/Monad.hs b/src/Control/Extra/Monad.hs index 9203983..753c898 100644 --- a/src/Control/Extra/Monad.hs +++ b/src/Control/Extra/Monad.hs @@ -1,19 +1,28 @@ +-- | +-- Copyright : (c) Ivan Perez & Henrik Nilsson, 2014. +-- License : See LICENSE file. +-- Maintainer : Ivan Perez +-- +-- Auxiliary functions related to Control.Monad. module Control.Extra.Monad where +-- External imports import Control.Monad whileLoopM :: Monad m => m a -> (a -> Bool) -> (a -> m ()) -> m () whileLoopM val cond act = r' - where r' = do v <- val - when (cond v) $ do - act v - whileLoopM val cond act + where + r' = do v <- val + when (cond v) $ do + act v + whileLoopM val cond act foldLoopM :: Monad m => a -> m b -> (b -> Bool) -> (a -> b -> m a) -> m a foldLoopM val sense cond act = r' - where r' = do s <- sense - if cond s - then do - val' <- act val s - foldLoopM val' sense cond act - else return val + where + r' = do s <- sense + if cond s + then do + val' <- act val s + foldLoopM val' sense cond act + else return val diff --git a/src/Data/Extra/List.hs b/src/Data/Extra/List.hs index 514f44b..888d863 100644 --- a/src/Data/Extra/List.hs +++ b/src/Data/Extra/List.hs @@ -1,4 +1,10 @@ +-- | +-- Copyright : (c) Ivan Perez & Henrik Nilsson, 2014. +-- License : See LICENSE file. +-- Maintainer : Ivan Perez +-- +-- Auxiliary functions related to Data.List. module Data.Extra.List where mapFilter :: (a -> b) -> (a -> Bool) -> [a] -> [b] -mapFilter f p = map f . filter p +mapFilter f p = map f . filter p diff --git a/src/Data/Extra/Num.hs b/src/Data/Extra/Num.hs index 828a9db..6650cc4 100644 --- a/src/Data/Extra/Num.hs +++ b/src/Data/Extra/Num.hs @@ -1,3 +1,9 @@ +-- | +-- Copyright : (c) Ivan Perez & Henrik Nilsson, 2014. +-- License : See LICENSE file. +-- Maintainer : Ivan Perez +-- +-- Auxiliary functions related to the 'Num' typeclass. module Data.Extra.Num where ensurePos :: (Eq a, Num a) => a -> a diff --git a/src/Data/Extra/Ord.hs b/src/Data/Extra/Ord.hs index 92e00ad..30a8f0c 100644 --- a/src/Data/Extra/Ord.hs +++ b/src/Data/Extra/Ord.hs @@ -1,5 +1,10 @@ +-- | +-- Copyright : (c) Ivan Perez & Henrik Nilsson, 2014. +-- License : See LICENSE file. +-- Maintainer : Ivan Perez +-- +-- Auxiliary functions related to the 'Ord' typeclass. module Data.Extra.Ord where -inRange :: Ord a => (a,a) -> a -> a +inRange :: Ord a => (a, a) -> a -> a inRange (mN, mX) x = min mX (max mN x) - diff --git a/src/Data/Extra/VectorSpace.hs b/src/Data/Extra/VectorSpace.hs index 7b607bc..397e3f8 100644 --- a/src/Data/Extra/VectorSpace.hs +++ b/src/Data/Extra/VectorSpace.hs @@ -1,6 +1,13 @@ +-- | +-- Copyright : (c) Ivan Perez & Henrik Nilsson, 2014. +-- License : See LICENSE file. +-- Maintainer : Ivan Perez +-- +-- Auxiliary functions related to Data.VectorSpace. module Data.Extra.VectorSpace where -import FRP.Yampa.VectorSpace +-- External imports +import Data.VectorSpace limitNorm :: (Ord s, VectorSpace v s) => v -> s -> v limitNorm v mn = if norm v > mn then mn *^ normalize v else v diff --git a/src/Display.hs b/src/Display.hs index ae30df6..5d13c0f 100644 --- a/src/Display.hs +++ b/src/Display.hs @@ -1,24 +1,32 @@ +-- | +-- Copyright : (c) Ivan Perez & Henrik Nilsson, 2014. +-- License : See LICENSE file. +-- Maintainer : Ivan Perez +-- +-- Audio and video renderer. module Display where -import Control.Applicative ((<$>)) -import Control.Monad -import Control.Monad.IfElse -import Control.Monad.Trans.Class -import Control.Monad.Trans.Maybe -import Control.Monad.IO.Class -import Data.IORef -import Data.Maybe -import Graphics.UI.SDL as SDL -import qualified Graphics.UI.SDL.TTF as TTF -import Graphics.UI.SDL.Image as Image - +-- External imports +import Control.Applicative ((<$>)) +import Control.Monad +import Control.Monad.IO.Class +import Control.Monad.IfElse +import Control.Monad.Trans.Class +import Control.Monad.Trans.Maybe +import Data.IORef +import Data.Maybe +import Graphics.UI.SDL as SDL +import Graphics.UI.SDL.Image as Image +import qualified Graphics.UI.SDL.TTF as TTF + +-- Internal imports import Audio import Constants import GameState -import Objects -import Resources hiding (audio) import Levels +import Objects import Paths_haskanoid +import Resources hiding (audio) -- * Initialization @@ -44,7 +52,7 @@ initGraphs = do -- * Rendering and Sound --- | Loads new resources, renders the game state using SDL, and adjusts music. +-- | Loads new resources, renders the game state using SDL, and adjusts music. render :: ResourceMgr -> GameState -> IO() render resourceManager shownState = do resources <- loadNewResources resourceManager shownState @@ -57,7 +65,7 @@ audio :: Resources -> GameState -> IO() audio resources shownState = do -- Start bg music if necessary playing <- musicPlaying - unless playing $ awhen (bgMusic resources) playMusic + unless playing $ awhen (bgMusic resources) playMusic -- Play object hits mapM_ (audioObject resources) $ gameObjects shownState @@ -71,7 +79,7 @@ audioObject resources object = when (objectHit object) $ -- ** Painting display :: Resources -> GameState -> IO() -display resources shownState = do +display resources shownState = do -- Obtain surface screen <- getVideoSurface @@ -86,8 +94,8 @@ display resources shownState = do SDL.blitSurface bg Nothing screen $ Just rectBg hud <- createRGBSurface [SWSurface] - (round width) (round gameTop) - 32 0xFF000000 0x00FF0000 0x0000FF00 0x000000FF + (round width) (round gameTop) + 32 0xFF000000 0x00FF0000 0x0000FF00 0x000000FF paintGeneral hud resources (gameInfo shownState) let rectHud = SDL.Rect 0 0 (round width) (round gameTop) SDL.blitSurface hud Nothing screen $ Just rectHud @@ -96,11 +104,12 @@ display resources shownState = do -- The 32 is important because we are using Word32 for the masks -- FIXME: Should I use HWSurface and possibly other flags (alpha?)? surface <- createRGBSurface [SWSurface] - (round gameWidth) (round gameHeight) - 32 0xFF000000 0x00FF0000 0x0000FF00 0x000000FF + (round gameWidth) (round gameHeight) + 32 0xFF000000 0x00FF0000 0x0000FF00 0x000000FF paintGeneralMsg surface resources (gameStatus (gameInfo shownState)) mapM_ (paintObject resources surface) $ gameObjects shownState - let rect = SDL.Rect (round gameLeft) (round gameTop) (round gameWidth) (round gameHeight) + let rect = SDL.Rect (round gameLeft) (round gameTop) + (round gameWidth) (round gameHeight) SDL.blitSurface surface Nothing screen $ Just rect -- Double buffering @@ -115,11 +124,15 @@ paintGeneral screen resources over = void $ do paintGeneralHUD screen resources over paintGeneralMsg :: Surface -> Resources -> GameStatus -> IO () -paintGeneralMsg screen resources GamePlaying = return () -paintGeneralMsg screen resources GamePaused = paintGeneralMsg' screen resources "Paused" -paintGeneralMsg screen resources (GameLoading n) = paintGeneralMsg' screen resources ("Level " ++ show n) -paintGeneralMsg screen resources GameOver = paintGeneralMsg' screen resources "GAME OVER!!!" -paintGeneralMsg screen resources GameFinished = paintGeneralMsg' screen resources "You won!!! Well done :)" +paintGeneralMsg screen resources GamePlaying = return () +paintGeneralMsg screen resources GamePaused = + paintGeneralMsg' screen resources "Paused" +paintGeneralMsg screen resources (GameLoading n) = + paintGeneralMsg' screen resources ("Level " ++ show n) +paintGeneralMsg screen resources GameOver = + paintGeneralMsg' screen resources "GAME OVER!!!" +paintGeneralMsg screen resources GameFinished = + paintGeneralMsg' screen resources "You won!!! Well done :)" paintGeneralMsg' :: Surface -> Resources -> String -> IO () paintGeneralMsg' screen resources msg = void $ do @@ -134,46 +147,63 @@ paintGeneralMsg' screen resources msg = void $ do paintGeneralHUD :: Surface -> Resources -> GameInfo -> IO () paintGeneralHUD screen resources over = void $ do let font = unFont $ resFont resources - message1 <- TTF.renderTextSolid font ("Level: " ++ show (gameLevel over)) (SDL.Color 128 128 128) + message1 <- TTF.renderTextSolid font + ("Level: " ++ show (gameLevel over)) + (SDL.Color 128 128 128) let w1 = SDL.surfaceGetWidth message1 h1 = SDL.surfaceGetHeight message1 SDL.blitSurface message1 Nothing screen $ Just (SDL.Rect 10 10 w1 h1) - message2 <- TTF.renderTextSolid font ("Points: " ++ show (gamePoints over)) (SDL.Color 128 128 128) + message2 <- TTF.renderTextSolid font + ("Points: " ++ show (gamePoints over)) + (SDL.Color 128 128 128) let w2 = SDL.surfaceGetWidth message2 h2 = SDL.surfaceGetHeight message2 - SDL.blitSurface message2 Nothing screen $ Just (SDL.Rect 10 (10 + h2 + 5) w2 h2) - message3 <- TTF.renderTextSolid font ("Lives: " ++ show (gameLives over)) (SDL.Color 128 128 128) + SDL.blitSurface message2 Nothing screen $ + Just (SDL.Rect 10 (10 + h2 + 5) w2 h2) + message3 <- TTF.renderTextSolid font + ("Lives: " ++ show (gameLives over)) + (SDL.Color 128 128 128) let rightMargin = SDL.surfaceGetWidth screen w2 = SDL.surfaceGetWidth message3 h2 = SDL.surfaceGetHeight message3 - SDL.blitSurface message3 Nothing screen $ Just (SDL.Rect (rightMargin - 10 - w2) 10 w2 h2) + SDL.blitSurface message3 Nothing screen $ + Just (SDL.Rect (rightMargin - 10 - w2) 10 w2 h2) -- | Paints a game object on a surface. paintObject :: Resources -> Surface -> Object -> IO () paintObject resources screen object = - case objectKind object of - (Paddle (w,h)) -> void $ do let bI = imgSurface $ paddleImg resources - t <- mapRGB (surfaceGetPixelFormat bI) 0 255 0 - setColorKey bI [SrcColorKey, RLEAccel] t - SDL.blitSurface bI Nothing screen $ Just (SDL.Rect x y (round w) (round h)) - (Block e (w,h)) -> void $ do let bI = imgSurface $ blockImage e - SDL.blitSurface bI Nothing screen $ Just (SDL.Rect x y (round w) (round h)) - (Ball r) -> void $ do let x' = x - round r - y' = y - round r - sz = round (2*r) - -- b <- convertSurface (imgSurface $ ballImg resources) (format) [] - let bI = imgSurface $ ballImg resources - t <- mapRGB (surfaceGetPixelFormat bI) 0 255 0 - setColorKey bI [SrcColorKey, RLEAccel] t - SDL.blitSurface bI Nothing screen $ Just (SDL.Rect x' y' sz sz) - _ -> return () - where format = surfaceGetPixelFormat screen - p = objectPos object - x = round (fst p) - y = round (snd p) - blockImage 3 = block1Img resources - blockImage 2 = block2Img resources - blockImage n = block3Img resources + case objectKind object of + (Paddle (w, h)) -> void $ do let bI = imgSurface $ paddleImg resources + t <- mapRGB + (surfaceGetPixelFormat bI) 0 255 0 + setColorKey bI [SrcColorKey, RLEAccel] t + SDL.blitSurface bI Nothing screen $ + Just (SDL.Rect x y (round w) (round h)) + (Block e (w, h)) -> void $ do let bI = imgSurface $ blockImage e + SDL.blitSurface bI Nothing screen $ + Just (SDL.Rect x y (round w) (round h)) + (Ball r) -> void $ do let x' = x - round r + y' = y - round r + sz = round (2 * r) + -- b <- convertSurface + -- (imgSurface $ ballImg resources) + -- (format) + -- [] + let bI = imgSurface $ ballImg resources + t <- mapRGB + (surfaceGetPixelFormat bI) 0 255 0 + setColorKey bI [SrcColorKey, RLEAccel] t + SDL.blitSurface bI Nothing screen $ + Just (SDL.Rect x' y' sz sz) + _ -> return () + where + format = surfaceGetPixelFormat screen + p = objectPos object + x = round (fst p) + y = round (snd p) + blockImage 3 = block1Img resources + blockImage 2 = block2Img resources + blockImage n = block3Img resources -- * Resource management @@ -197,26 +227,27 @@ data Resources = Resources , bgMusic :: Maybe Music } -data Image = Image { imgName :: String, imgSurface :: Surface } -data Font = Font { fontName :: String, unFont :: TTF.Font } +data Image = Image { imgName :: String, imgSurface :: Surface } + +data Font = Font { fontName :: String, unFont :: TTF.Font } -- | Ad-hoc resource loading --- This function is ad-hoc in two senses: first, because it --- has the paths to the files hard-coded inside. And second, --- because it loads the specific resources that are needed, --- not a general -- +-- This function is ad-hoc in two senses: first, because it has the paths to +-- the files hard-coded inside. And second, because it loads the specific +-- resources that are needed, not a general loadResources :: IO (Maybe ResourceMgr) loadResources = runMaybeT $ do -- Font initialization ttfOk <- lift TTF.init - + gameFont <- liftIO $ getDataFileName "data/lacuna.ttf" -- Load the fonts we need - font <- liftIO $ TTF.tryOpenFont gameFont 32 -- What does the 32 do? + font <- liftIO $ TTF.tryOpenFont gameFont 32 -- What does the 32 do? let myFont = fmap (Font gameFont) font - blockHit <- liftIO $ loadAudio =<< getDataFileName "data/196106_aiwha_ding-cc-by.wav" + blockHit <- + liftIO $ loadAudio =<< getDataFileName "data/196106_aiwha_ding-cc-by.wav" -- bgM <- liftIO $ loadMusic "Ckotty_-_Game_Loop_11.ogg" -- bgM <- liftIO $ loadMusic "data/level0.mp3" @@ -244,15 +275,18 @@ loadResources = runMaybeT $ do -- Return Nothing or embed in Resources res <- case (myFont, blockHit) of - (Just f, Just b) -> let - in return (Resources f b Nothing ball b1 b2 b3 paddle Nothing) - _ -> do liftIO $ putStrLn "Some resources could not be loaded" - mzero + (Just f, Just b) -> return $ + Resources f b Nothing + ball b1 b2 + b3 paddle Nothing + _ -> do liftIO $ + putStrLn + "Some resources could not be loaded" + mzero liftIO $ ResourceMgr <$> newIORef (ResourceManager GameStarted res) - loadNewResources :: ResourceMgr -> GameState -> IO Resources loadNewResources mgr state = do manager <- readIORef (unResMgr mgr) @@ -263,7 +297,7 @@ loadNewResources mgr state = do newResources <- case newState of (GameLoading _) | newState /= oldState -> updateAllResources oldResources newState - _ -> return oldResources + _ -> return oldResources let manager' = ResourceManager { lastKnownStatus = newState , resources = newResources @@ -282,15 +316,16 @@ updateAllResources res (GameLoading n) = do oldMusicFP = maybe "" musicName oldMusic newMusic <- if oldMusicFP == newMusicFP - then return oldMusic - else do -- Loading can fail, in which case we continue - -- with the old music - bgM <- loadMusic newMusicFP - if isNothing bgM - then do putStrLn $ "Could not load resource " ++ newMusicFP - return oldMusic - else do stopMusic - return bgM + then return oldMusic + else do -- Loading can fail, in which case we continue with the + -- old music + bgM <- loadMusic newMusicFP + if isNothing bgM + then do putStrLn $ + "Could not load resource " ++ newMusicFP + return oldMusic + else do stopMusic + return bgM -- Load new background let newBgFP' = _resourceFP $ levelBg $ levels !! n diff --git a/src/FRP/Extra/Yampa.hs b/src/FRP/Extra/Yampa.hs index c29d0b4..ae604f2 100644 --- a/src/FRP/Extra/Yampa.hs +++ b/src/FRP/Extra/Yampa.hs @@ -1,29 +1,35 @@ +-- | +-- Copyright : (c) Ivan Perez & Henrik Nilsson, 2014. +-- License : See LICENSE file. +-- Maintainer : Ivan Perez +-- +-- Auxiliary functions related to FRP.Yampa. module FRP.Extra.Yampa where +-- External imports import Control.Arrow import FRP.Yampa --- Auxiliary Yampa stuff - -- holdWhen behaves normally, outputting only the b, when the second value --- is false, and it holds the last known value when the value is True. -holdWhen :: b -> SF a (b,Bool) -> SF a b -holdWhen b_init sf = sf >>> holdOutput >>> hold b_init - where holdOutput = arr (\(b,discard) -> if discard then noEvent else Event b) - --- Given an occasional producer of functions --- and a source of info, apply the functions when they --- exist +-- is false, and it holds the last known value when the value is True. +holdWhen :: b -> SF a (b, Bool) -> SF a b +holdWhen bInit sf = sf >>> holdOutput >>> hold bInit + where + holdOutput = arr (\(b, discard) -> if discard then noEvent else Event b) + +-- Given an occasional producer of functions and a source of info, apply the +-- functions when they exist mergeApply :: SF a b -> SF a (Event (b -> b)) -> SF a b mergeApply sf1 sf2 = - (sf1 &&& sf2) >>> (arr (\(b,ef) -> event b ($ b) ef)) + (sf1 &&& sf2) >>> arr (\(b, ef) -> event b ($ b) ef) mergeApply' :: SF a (b, Event (b -> b)) -> SF a b -mergeApply' sf1 = sf1 >>> (arr (\(b,ef) -> event b ($ b) ef)) +mergeApply' sf1 = sf1 >>> arr (\(b, ef) -> event b ($ b) ef) rRestart :: SF a (b, Event c) -> SF a b rRestart sf = r - where r = switch sf (const r) + where + r = switch sf (const r) futureSwitch :: SF a (b, Event c) -> (c -> SF a b) -> SF a b futureSwitch sf cont = switch (sf >>> (arr id *** notYet)) cont @@ -35,4 +41,3 @@ boolToEvent :: Bool -> a -> Event a boolToEvent True = Event boolToEvent _ = \_ -> noEvent {-# INLINE boolToEvent #-} - diff --git a/src/Game.hs b/src/Game.hs index df6eafc..b10f3dd 100644 --- a/src/Game.hs +++ b/src/Game.hs @@ -1,24 +1,28 @@ {-# LANGUAGE Arrows #-} --- | This module defines the game as a big Signal Function that transforms a +-- | +-- Copyright : (c) Ivan Perez & Henrik Nilsson, 2014. +-- License : See LICENSE file. +-- Maintainer : Ivan Perez +-- +-- This module defines the game as a big Signal Function that transforms a -- Signal carrying a Input 'Controller' information into a Signal carrying -- 'GameState'. -- -- There is no randomness in the game, the only input is the user's. -- 'Controller' is an abstract representation of a basic input device with -- position information and a /fire/ button. --- +-- -- The output is defined in 'GameState', and consists of basic information --- (points, current level, etc.) and a universe of objects. +-- (points, current level, etc.) and a universe of objects. -- -- Objects are represented as Signal Functions as well ('ObjectSF'). This -- allows them to react to user input and change with time. Each object is -- responsible for itself, but it cannot affect others: objects can watch --- others, depend on others and react to them, but they cannot /send a --- message/ or eliminate other objects. However, if you would like to --- dynamically introduce new elements in the game (for instance, falling --- powerups that the player must collect before they hit the ground) then it --- might be a good idea to allow objects not only to /kill themselves/ but --- also to spawn new object. +-- others, depend on others and react to them, but they cannot /send a message/ +-- or eliminate other objects. However, if you would like to dynamically +-- introduce new elements in the game (for instance, falling powerups that the +-- player must collect before they hit the ground) then it might be a good idea +-- to allow objects not only to /kill themselves/ but also to spawn new object. -- -- This module contains three sections: -- @@ -68,9 +72,9 @@ import ObjectSF -- ('wonGame'). wholeGame :: SF Controller GameState wholeGame = switch - -- restart normal behaviour every time I'm out of lives - (canLose >>> (arr id &&& outOfLevels)) - (\_ -> wonGame) + -- restart normal behaviour every time I'm out of lives + (canLose >>> (arr id &&& outOfLevels)) + (\_ -> wonGame) -- | Detect when the last level is finished. outOfLevels :: SF GameState (Event ()) @@ -80,9 +84,9 @@ outOfLevels = arr ((>= numLevels) . gameLevel . gameInfo) >>> edge -- ('outOfLives'), in which case the game must be restarted ('restartGame'). canLose :: SF Controller GameState canLose = switch - -- retart normal behaviour every time I'm out of lives - (gameAlive >>> (arr id &&& outOfLives)) - (\_ -> restartGame) + -- retart normal behaviour every time I'm out of lives + (gameAlive >>> (arr id &&& outOfLives)) + (\_ -> restartGame) -- | Detect when the last life is lost. outOfLives :: SF GameState (Event ()) @@ -97,7 +101,7 @@ restartGame = switch -- | Produces a neutral 'GameOver' 'GameState'. gameOver :: SF a GameState gameOver = arr $ const $ - neutralGameState { gameInfo = neutralGameInfo { gameStatus = GameOver } } + neutralGameState { gameInfo = neutralGameInfo { gameStatus = GameOver } } -- | The game state is finished for 4 seconds, then the game is run again -- ('wholeGame'). @@ -108,7 +112,7 @@ wonGame = switch -- | Produces a neutral 'GameFinished' 'GameState'. gameFinished :: SF a GameState gameFinished = arr $ const $ - neutralGameState { gameInfo = neutralGameInfo { gameStatus = GameFinished } } + neutralGameState { gameInfo = neutralGameInfo { gameStatus = GameFinished } } -- | Run the game from the beginning (no points, max lives, etc.). -- @@ -126,16 +130,16 @@ gameAlive = runLevel stdLives initialLevel 0 -- continuation. runLevel :: Int -> Int -> Int -> SF Controller GameState runLevel lives level pts = loadLevel lives level pts loadingDelay - (gameWithLives lives level pts) + (gameWithLives lives level pts) -- | Unconditionally output the game in loading state ('levelLoading') for some -- time, and then ('after') switch over to the given continuation. -- -- The given arguments are the lives, the level, the points, the time to stay -- loading the game and the continuation. -loadLevel :: Int -> Int -> Int -> DTime -> SF a GameState -> SF a GameState +loadLevel :: Int -> Int -> Int -> DTime -> SF a GameState -> SF a GameState loadLevel lives level pts time next = switch - -- + -- (levelLoading lives level pts &&& after time ()) (\_ -> next) @@ -152,12 +156,11 @@ levelLoading lvs lvl pts = arr $ const $ -- | Start the game at a given level, with a given number of lives. -- --- It executes the normal gameplay until the level is completed. --- It then switches to the next level (remembering the current --- lives and points). +-- It executes the normal gameplay until the level is completed. It then +-- switches to the next level (remembering the current lives and points). -- --- Conditions like finishing the game or running out of lives are --- detected in 'wholeGame' and 'canLose', respectively. +-- Conditions like finishing the game or running out of lives are detected in +-- 'wholeGame' and 'canLose', respectively. -- gameWithLives :: Int -> Int -> Int -> SF Controller GameState gameWithLives numLives level pts = dSwitch @@ -181,9 +184,8 @@ isLevelCompleted = proc s -> do -- | Run the normal game. -- --- NOTE: The code includes a commented piece that detects --- a request to pause the game. Check out the code to learn how to --- implement pausing. +-- NOTE: The code includes a commented piece that detects a request to pause +-- the game. Check out the code to learn how to implement pausing. gamePlayOrPause :: Int -> Int -> Int -> SF Controller GameState gamePlayOrPause lives level pts = gamePlay lives level pts -- ((arr id) &&& (pause undefined (False --> isPaused) (mainLoop lives level))) @@ -210,8 +212,8 @@ gamePlay lives level pts = gamePlay' (initialObjects level) >>> composeGameState lives level pts -- | Based on the internal gameplay info, compose the main game state and --- detect when a live is lost. When that happens, restart this SF --- with one less life available. +-- detect when a live is lost. When that happens, restart this SF with one less +-- life available. -- -- NOTE: it will be some other SF's responsibility to determine if the player's -- run out of lives. @@ -224,34 +226,33 @@ composeGameState :: Int -> Int -> Int -> SF (ObjectOutputs, Event (), Int) GameState composeGameState lives level pts = futureDSwitch (composeGameState' lives level pts) - (\_ -> composeGameState (lives-1) level pts) + (\_ -> composeGameState (lives - 1) level pts) -- | Based on the internal gameplay info, compose the main game state and -- detect when a live is lost. When that happens, keep the last known game -- state. composeGameState' :: Int -> Int -> Int - -> SF (ObjectOutputs, Event (), Int) (GameState, Event GameState) -composeGameState' lives level pts = proc (oos,dead,points) -> do + -> SF (ObjectOutputs, Event (), Int) + (GameState, Event GameState) +composeGameState' lives level pts = proc (oos, dead, points) -> do -- Compose game state objects <- extractObjects -< oos let general = GameState objects - (GameInfo GamePlaying lives level (pts+points)) + (GameInfo GamePlaying lives level (pts + points)) -- Detect death let lastGeneral = dead `tag` general returnA -< (general, lastGeneral) - -- ** Game with partial state information -- | Given an initial list of objects, it runs the game, presenting the output -- from those objects at all times, notifying any time the ball hits the floor, --- and and of any additional points made. +-- and of any additional points made. -- --- This works as a game loop with a post-processing step. It uses --- a well-defined initial accumulator and a traditional feedback --- loop. +-- This works as a game loop with a post-processing step. It uses a +-- well-defined initial accumulator and a traditional feedback loop. -- -- The internal accumulator holds: -- @@ -262,82 +263,90 @@ composeGameState' lives level pts = proc (oos,dead,points) -> do -- - The last known points (added to the new ones in every loop iteration). -- gamePlay' :: ObjectSFs -> SF Controller (ObjectOutputs, Event (), Int) -gamePlay' objs = loopPre ([],[],0) $ - -- Process physical movement and detect new collisions - ((adaptInput >>> processMovement >>> (arr elemsIL &&& detectObjectCollisions)) - &&& arr (thd3.snd)) -- This last bit just carries the old points forward - - -- Adds the old point count to the newly-made points - >>> (arr fst &&& arr (\((_,cs),o) -> o + countPoints cs)) - - -- Re-arrange output, selecting (objects+dead+points, objects+collisions+points) - >>> (composeOutput &&& arr (\((x,y),z) -> (x,y,z))) - - where - - -- Detect collisions between the ball and the bottom - -- which are the only ones that matter outside gamePlay' - composeOutput = proc ((x,y),z) -> do - y' <- collisionWithBottom -< y - returnA -< (x,y',z) - - -- Just reorder the input - adaptInput :: SF (Controller, (ObjectOutputs, Collisions, Int)) ObjectInput - adaptInput = arr (\(gi,(os,cs,pts)) -> ObjectInput gi cs (map outputObject os)) - - -- Parallely apply all object functions - processMovement :: SF ObjectInput (IL ObjectOutput) - processMovement = processMovement' objs - - processMovement' :: ObjectSFs -> SF ObjectInput (IL ObjectOutput) - processMovement' objs = dpSwitchB - objs -- Signal functions - (noEvent --> arr suicidalSect) -- When necessary, remove all elements that must be removed - (\sfs' f -> processMovement' (f sfs')) -- Move along! Move along! (with new state, aka. sfs) - - suicidalSect :: (a, IL ObjectOutput) -> Event (IL ObjectSF -> IL ObjectSF) - suicidalSect (_,oos) = - -- Turn every event carrying a function that transforms the - -- object signal function list into one function that performs - -- all the efects in sequence - foldl (mergeBy (.)) noEvent es - - -- Turn every object that wants to kill itself into - -- a function that removes it from the list - where es :: [Event (IL ObjectSF -> IL ObjectSF)] - es = [ harakiri oo `tag` deleteIL k - | (k,oo) <- assocsIL oos ] - - -- From the actual objects, detect which ones collide - detectObjectCollisions :: SF (IL ObjectOutput) Collisions - detectObjectCollisions = extractObjects >>> arr detectCollisions - - -- Count-points - countPoints :: Collisions -> Int - countPoints = sum . map numPoints - where numPoints (Collision cd) - | hasBall cd = countBlocks cd - | otherwise = 0 - hasBall = any ((=="ball").fst) - countBlocks = length . filter (isPrefixOf "block" . fst) - - +gamePlay' objs = loopPre ([], [], 0) $ + -- Process physical movement and detect new collisions + ( ( adaptInput + >>> processMovement + >>> (arr elemsIL &&& detectObjectCollisions) + ) + &&& arr (thd3.snd) -- This last bit just carries the old points forward + ) + + -- Adds the old point count to the newly-made points + >>> (arr fst &&& arr (\((_, cs), o) -> o + countPoints cs)) + + -- Re-arrange output, selecting + -- (objects + dead + points, objects + collisions + points) + >>> (composeOutput &&& arr (\((x, y), z) -> (x, y, z))) + + where + + -- Detect collisions between the ball and the bottom + -- which are the only ones that matter outside gamePlay' + composeOutput = proc ((x, y), z) -> do + y' <- collisionWithBottom -< y + returnA -< (x, y', z) + + -- Just reorder the input + adaptInput :: SF (Controller, (ObjectOutputs, Collisions, Int)) ObjectInput + adaptInput = + arr (\(gi, (os, cs, pts)) -> ObjectInput gi cs (map outputObject os)) + + -- Parallely apply all object functions + processMovement :: SF ObjectInput (IL ObjectOutput) + processMovement = processMovement' objs + + processMovement' :: ObjectSFs -> SF ObjectInput (IL ObjectOutput) + processMovement' objs = dpSwitchB + objs -- Signal functions. + (noEvent --> arr suicidalSect) -- When necessary, remove all + -- elements that must be removed. + (\sfs' f -> processMovement' (f sfs')) -- Move along! Move along! + -- (with new state, aka. sfs). + + suicidalSect :: (a, IL ObjectOutput) -> Event (IL ObjectSF -> IL ObjectSF) + suicidalSect (_, oos) = + -- Turn every event carrying a function that transforms the object + -- signal function list into one function that performs all the effects + -- in sequence + foldl (mergeBy (.)) noEvent es + + -- Turn every object that wants to kill itself into a function that + -- removes it from the list + where + es :: [Event (IL ObjectSF -> IL ObjectSF)] + es = [ harakiri oo `tag` deleteIL k + | (k, oo) <- assocsIL oos ] + + -- From the actual objects, detect which ones collide + detectObjectCollisions :: SF (IL ObjectOutput) Collisions + detectObjectCollisions = extractObjects >>> arr detectCollisions + + -- Count-points + countPoints :: Collisions -> Int + countPoints = sum . map numPoints + where + numPoints (Collision cd) + | hasBall cd = countBlocks cd + | otherwise = 0 + hasBall = any ((=="ball").fst) + countBlocks = length . filter (isPrefixOf "block" . fst) -- * Game objects -- -- | Objects initially present: the walls, the ball, the paddle and the blocks. initialObjects :: Int -> ObjectSFs initialObjects level = listToIL $ - [ objSideRight - , objSideTop - , objSideLeft - , objSideBottom - , objPaddle - , objBall - ] - ++ map (\p -> objBlock p (blockWidth, blockHeight)) (blockCfgs $ levels!!level) - - + [ objSideRight + , objSideTop + , objSideLeft + , objSideBottom + , objPaddle + , objBall + ] + ++ map (\p -> objBlock p (blockWidth, blockHeight)) + (blockCfgs $ levels !! level) + -- *** Ball -- | Ball @@ -347,29 +356,27 @@ initialObjects level = listToIL $ -- bounding around, until it hits the floor ('bounceAroundDetectMiss'). -- objBall :: ObjectSF -objBall = switch followPaddleDetectLaunch $ \p -> +objBall = switch followPaddleDetectLaunch $ \p -> switch (bounceAroundDetectMiss p) $ \_ -> objBall - where - -- Yampa's edge is used to turn the continuous - -- signal produced by controllerClick into an - -- event-carrying signal, only true the instant - -- the mouse button is clicked. - followPaddleDetectLaunch = proc oi -> do - o <- followPaddle -< oi - click <- edge -< controllerClick (userInput oi) - returnA -< (o, click `tag` objectPos (outputObject o)) - - bounceAroundDetectMiss p = proc oi -> do - o <- bouncingBall p initialBallVel -< oi - miss <- collisionWithBottom -< collisions oi - returnA -< (o, miss) - --- | Fires an event when the ball *enters in* a collision with the --- bottom wall. --- --- NOTE: even if the overlap is not corrected, 'edge' makes --- the event only take place once per collision. + where + -- Yampa's edge is used to turn the continuous signal produced by + -- controllerClick into an event-carrying signal, only true the instant the + -- mouse button is clicked. + followPaddleDetectLaunch = proc oi -> do + o <- followPaddle -< oi + click <- edge -< controllerClick (userInput oi) + returnA -< (o, click `tag` objectPos (outputObject o)) + + bounceAroundDetectMiss p = proc oi -> do + o <- bouncingBall p initialBallVel -< oi + miss <- collisionWithBottom -< collisions oi + returnA -< (o, miss) + +-- | Fires an event when the ball *enters in* a collision with the bottom wall. +-- +-- NOTE: even if the overlap is not corrected, 'edge' makes the event only take +-- place once per collision. collisionWithBottom :: SF Collisions (Event ()) collisionWithBottom = inCollisionWith "ball" "bottomWall" ^>> edge @@ -377,62 +384,62 @@ collisionWithBottom = inCollisionWith "ball" "bottomWall" ^>> edge -- otherwise). To avoid reacting to collisions, this ball is non-interactive. followPaddle :: ObjectSF followPaddle = arr $ \oi -> - -- Calculate ball position, midway on top of the the paddle - -- - -- This code allows for the paddle not to exist (Maybe), although that should - -- never happen in practice. - let mbPaddlePos = objectPos <$> find isPaddle (knownObjects oi) - ballPos = maybe (outOfScreen, outOfScreen) - ((paddleWidth/2, - ballHeight) ^+^) - mbPaddlePos - in ObjectOutput (inertBallAt ballPos) noEvent - where outOfScreen = -10 - inertBallAt p = Object { objectName = "ball" - , objectKind = Ball ballWidth - , objectPos = p - , objectVel = (0, 0) - , objectAcc = (0, 0) - , objectDead = False - , objectHit = False - , canCauseCollisions = False - , collisionEnergy = 0 - , displacedOnCollision = False - } + -- Calculate ball position, midway on top of the paddle + -- + -- This code allows for the paddle not to exist (Maybe), although that + -- should never happen in practice. + let mbPaddlePos = objectPos <$> find isPaddle (knownObjects oi) + ballPos = maybe (outOfScreen, outOfScreen) + ((paddleWidth / 2, - ballHeight) ^+^) + mbPaddlePos + in ObjectOutput (inertBallAt ballPos) noEvent + where + outOfScreen = -10 + inertBallAt p = Object { objectName = "ball" + , objectKind = Ball ballWidth + , objectPos = p + , objectVel = (0, 0) + , objectAcc = (0, 0) + , objectDead = False + , objectHit = False + , canCauseCollisions = False + , collisionEnergy = 0 + , displacedOnCollision = False + } -- A bouncing ball moves freely until there is a collision, then bounces and -- goes on and on. -- --- This SF needs an initial position and velocity. Every time --- there is a bounce, it takes a snapshot of the point of --- collision and corrected velocity, and starts again. +-- This SF needs an initial position and velocity. Every time there is a +-- bounce, it takes a snapshot of the point of collision and corrected +-- velocity, and starts again. -- bouncingBall :: Pos2D -> Vel2D -> ObjectSF bouncingBall p0 v0 = - switch progressAndBounce - (uncurry bouncingBall) -- Somehow it would be clearer like this: - -- \(p', v') -> bouncingBall p' v') - where - - -- Calculate the future tentative position, and - -- bounce if necessary. - -- - -- The ballBounce needs the ball SF' input (which has knowledge of - -- collisions), so we carry it parallely to the tentative new positions, - -- and then use it to detect when it's time to bounce - - -- ========================== ============================ - -- -==--------------------->==--->==- ------------------->== - -- / == == == \ / == - -- -- == == == X == - -- \ == == == / \ == - -- -==----> freeBall' ----->==--->==--------> ballBounce -->== - -- ========================== ============================ - progressAndBounce = (arr id &&& freeBall') >>> (arr snd &&& ballBounce) - - -- Position of the ball, starting from p0 with velicity v0, since the - -- time of last switching (or being fired, whatever happened last) - -- provided that no obstacles are encountered. - freeBall' = freeBall p0 v0 + switch progressAndBounce + (uncurry bouncingBall) -- Somehow it would be clearer like this: + -- \(p', v') -> bouncingBall p' v') + where + + -- Calculate the future tentative position, and bounce if necessary. + -- + -- The ballBounce needs the ball SF' input (which has knowledge of + -- collisions), so we carry it parallelly to the tentative new positions, + -- and then use it to detect when it's time to bounce + + -- ========================== ============================ + -- -==--------------------->==--->==- ------------------->== + -- / == == == \ / == + -- -- == == == X == + -- \ == == == / \ == + -- -==----> freeBall' ----->==--->==--------> ballBounce -->== + -- ========================== ============================ + progressAndBounce = (arr id &&& freeBall') >>> (arr snd &&& ballBounce) + + -- Position of the ball, starting from p0 with velocity v0, since the + -- time of last switching (or being fired, whatever happened last) + -- provided that no obstacles are encountered. + freeBall' = freeBall p0 v0 -- | Detect if the ball must bounce and, if so, take a snapshot of the object's -- current position and velocity. @@ -440,7 +447,7 @@ bouncingBall p0 v0 = -- NOTE: To avoid infinite loops when switching, the initial input is discarded -- and never causes a bounce. This works in this game and in this particular -- case because the ball never-ever bounces immediately as fired from the --- paddle. This might not be true if a block is extremely close, if you add +-- paddle. This might not be true if a block is extremely close, if you add -- flying enemies to the game, etc. ballBounce :: SF (ObjectInput, ObjectOutput) (Event (Pos2D, Vel2D)) ballBounce = noEvent --> ballBounce' @@ -450,18 +457,18 @@ ballBounce = noEvent --> ballBounce' -- -- This does the core of the work, and does not ignore the initial input. -- --- It proceeds by detecting whether any collision affects --- the ball's velocity, and outputs a snapshot of the object --- position and the corrected velocity if necessary. +-- It proceeds by detecting whether any collision affects the ball's velocity, +-- and outputs a snapshot of the object position and the corrected velocity if +-- necessary. ballBounce' :: SF (ObjectInput, ObjectOutput) (Event (Pos2D, Vel2D)) ballBounce' = proc (ObjectInput ci cs os, o) -> do - -- HN 2014-09-07: With the present strategy, need to be able to - -- detect an event directly after - -- ev <- edgeJust -< changedVelocity "ball" cs + -- HN 2014-09-07: With the present strategy, need to be able to detect an + -- event directly after + -- ev <- edgeJust -< changedVelocity "ball" cs let ev = maybe noEvent Event (changedVelocity "ball" cs) returnA -< fmap (\v -> (objectPos (outputObject o), v)) ev --- | Position of the ball, starting from p0 with velicity v0, since the time of +-- | Position of the ball, starting from p0 with velocity v0, since the time of -- last switching (that is, collision, or the beginning of time --being fired -- from the paddle-- if never switched before), provided that no obstacles are -- encountered. @@ -475,9 +482,8 @@ freeBall p0 v0 = proc (ObjectInput ci cs os) -> do -- Cap speed let v = limitNorm v0 maxVNorm - -- Any free moving object behaves like this (but with - -- acceleration. This should be in some FRP.NewtonianPhysics - -- module) + -- Any free moving object behaves like this (but with acceleration. This + -- should be in some FRP.NewtonianPhysics module) p <- (p0 ^+^) ^<< integral -< v let obj = Object { objectName = name @@ -491,7 +497,7 @@ freeBall p0 v0 = proc (ObjectInput ci cs os) -> do , collisionEnergy = 1 , displacedOnCollision = True } - + returnA -< livingObject obj -- *** Player paddle @@ -508,30 +514,33 @@ objPaddle = proc (ObjectInput ci cs os) -> do let name = "paddle" let isHit = inCollision name cs - -- Try to get to the mouse position, but with a capped - -- velocity. + -- Try to get to the mouse position, but with a capped velocity. - rec - -- let v = limitNorm (20.0 *^ (refPosPaddle ci ^-^ p)) maxVNorm - -- let p = refPosPaddle ci -- (initPosPaddle ^+^) ^<< integral -< v - let v = 100.00 *^ (refPosPaddle ci ^-^ p) - p <- (initPosPaddle ^+^) ^<< integral -< v - -- let p = refPosPaddle ci + -- rec + -- -- let v = limitNorm (20.0 *^ (refPosPaddle ci ^-^ p)) maxVNorm + -- -- let p = refPosPaddle ci -- (initPosPaddle ^+^) ^<< integral -< v + -- let v = 100.00 *^ (refPosPaddle ci ^-^ p) + -- + -- -- The initial position of the paddle, horizontally centered. + -- initPosPaddle = ((gameWidth - paddleWidth)/2, yPosPaddle) + -- + -- p <- (initPosPaddle ^+^) ^<< integral -< v + let p = refPosPaddle ci + v <- derivative -< p - -- Use this code if you want instantaneous movement, - -- particularly cool with the Wiimote, but remember to cap - -- the balls velocity or you will get incredibly high - -- velocities when the paddle hits the ball. + -- Use this code if you want instantaneous movement, particularly cool with + -- the Wiimote, but remember to cap the balls velocity or you will get + -- incredibly high velocities when the paddle hits the ball. -- -- let p = refPosPaddle ci -- v <- derivative -< p returnA -< livingObject $ Object{ objectName = name - , objectKind = Paddle (paddleWidth,paddleHeight) + , objectKind = Paddle (paddleWidth, paddleHeight) , objectPos = p - , objectVel = (0,0) - , objectAcc = (0,0) + , objectVel = (0, 0) + , objectAcc = (0, 0) , objectDead = False , objectHit = isHit , canCauseCollisions = True @@ -539,17 +548,13 @@ objPaddle = proc (ObjectInput ci cs os) -> do , displacedOnCollision = False } --- | Follow the controller's horizontal position, keeping a constant --- vertical position. +-- | Follow the controller's horizontal position, keeping a constant vertical +-- position. refPosPaddle :: Controller -> Pos2D refPosPaddle c = (x', yPosPaddle) - where - (x, _) = controllerPos c - x' = inRange (0, gameWidth - paddleWidth) (x - (paddleWidth/2)) - --- | The initial position of the paddle, horizontally centered. -initPosPaddle :: Pos2D -initPosPaddle = ((gameWidth - paddleWidth)/2, yPosPaddle) + where + (x, _) = controllerPos c + x' = inRange (0, gameWidth - paddleWidth) (x - (paddleWidth / 2)) -- | The paddle's vertical position, at a reasonable distance from the bottom. yPosPaddle :: Double @@ -557,28 +562,28 @@ yPosPaddle = gameHeight - paddleMargin -- *** Blocks --- | Block SF generator. It uses the blocks's size, position and a number of +-- | Block SF generator. It uses the blocks' size, position and a number of -- lives that the block has. The block's position is used for it's unique ID, --- which means that two simulatenously existing blocks should never have the +-- which means that two simultaneously existing blocks should never have the -- same position. This is ok in this case because they are static, but would not -- work if they could move and be created dynamically. objBlock :: (Pos2D, Int) -> Size2D -> ObjectSF -objBlock ((x,y), initlives) (w,h) = proc (ObjectInput ci cs os) -> do +objBlock ((x, y), initlives) (w, h) = proc (ObjectInput ci cs os) -> do -- Detect collisions - let name = "blockat" ++ show (x,y) + let name = "blockat" ++ show (x, y) isHit = inCollision name cs hit <- edge -< isHit -- Must be hit initlives times to disappear -- - -- If you want them to "recover" or self-heal with time, - -- use the following code in place of lives. + -- If you want them to "recover" or self-heal with time, use the following + -- code in place of lives. -- -- recover <- delayEvent 5.0 -< hit - -- lives <- accumHoldBy (+) 3 -< (hit `tag` (-1) `lMerge` recover `tag` 1) - lives <- accumHoldBy (+) initlives -< (hit `tag` (-1)) - -- + -- lives <- accumHoldBy (+) 3 -< (hit `tag` (-1) `lMerge` recover `tag` 1) + lives <- accumHoldBy (+) initlives -< (hit `tag` (-1)) + -- -- let lives = 3 -- Always perfect -- Dead if out of lives. @@ -586,18 +591,18 @@ objBlock ((x,y), initlives) (w,h) = proc (ObjectInput ci cs os) -> do dead <- edge -< isDead -- let isDead = False -- immortal blocks - returnA -< ObjectOutput - Object{ objectName = name - , objectKind = Block lives (w, h) - , objectPos = (x,y) - , objectVel = (0,0) - , objectAcc = (0,0) - , objectDead = isDead - , objectHit = isHit - , canCauseCollisions = False - , collisionEnergy = 0 - , displacedOnCollision = False - } + returnA -< ObjectOutput + Object{ objectName = name + , objectKind = Block lives (w, h) + , objectPos = (x, y) + , objectVel = (0, 0) + , objectAcc = (0, 0) + , objectDead = isDead + , objectHit = isHit + , canCauseCollisions = False + , collisionEnergy = 0 + , displacedOnCollision = False + } dead -- *** Walls @@ -608,40 +613,40 @@ objBlock ((x,y), initlives) (w,h) = proc (ObjectInput ci cs os) -> do -- The function that turns walls into 'Shape's for collision detection -- determines how big they really are. In particular, this has implications in -- ball-through-paper effects (ball going through objects, potentially never --- coming back), which can be seen if the FPS suddently drops due to CPU load +-- coming back), which can be seen if the FPS suddenly drops due to CPU load -- (for instance, if a really major Garbage Collection kicks in. One potential -- optimisation is to trigger these with every SF iteration or every rendering, --- to decrease the workload and thus the likelyhood of BTP effects. -objSideRight :: ObjectSF -objSideRight = objWall "rightWall" RightSide (gameWidth, 0) +-- to decrease the workload and thus the likelihood of BTP effects. +objSideRight :: ObjectSF +objSideRight = objWall "rightWall" RightSide (gameWidth, 0) -- | See 'objSideRight'. -objSideLeft :: ObjectSF -objSideLeft = objWall "leftWall" LeftSide (0, 0) +objSideLeft :: ObjectSF +objSideLeft = objWall "leftWall" LeftSide (0, 0) -- | See 'objSideRight'. -objSideTop :: ObjectSF -objSideTop = objWall "topWall" TopSide (0, 0) +objSideTop :: ObjectSF +objSideTop = objWall "topWall" TopSide (0, 0) -- | See 'objSideRight'. objSideBottom :: ObjectSF -objSideBottom = objWall "bottomWall" BottomSide (0, gameHeight) +objSideBottom = objWall "bottomWall" BottomSide (0, gameHeight) -- | Generic wall builder, given a name, a side and its base -- position. objWall :: ObjectName -> Side -> Pos2D -> ObjectSF objWall name side pos = proc (ObjectInput ci cs os) -> do - let isHit = inCollision name cs - returnA -< ObjectOutput - Object { objectName = name - , objectKind = Side side - , objectPos = pos - , objectVel = (0,0) - , objectAcc = (0,0) - , objectDead = False - , objectHit = isHit - , canCauseCollisions = False - , collisionEnergy = 0 - , displacedOnCollision = False - } - noEvent + let isHit = inCollision name cs + returnA -< ObjectOutput + Object { objectName = name + , objectKind = Side side + , objectPos = pos + , objectVel = (0, 0) + , objectAcc = (0, 0) + , objectDead = False + , objectHit = isHit + , canCauseCollisions = False + , collisionEnergy = 0 + , displacedOnCollision = False + } + noEvent diff --git a/src/GameCollisions.hs b/src/GameCollisions.hs index e0df5c0..5e3a67c 100644 --- a/src/GameCollisions.hs +++ b/src/GameCollisions.hs @@ -1,4 +1,9 @@ --- | A very rudimentary collision system. +-- | +-- Copyright : (c) Ivan Perez & Henrik Nilsson, 2014. +-- License : See LICENSE file. +-- Maintainer : Ivan Perez +-- +-- A very rudimentary collision system. -- -- It compares every pair of objects, trying to determine if there is a -- collision between the two of them. @@ -10,38 +15,41 @@ -- module GameCollisions where +-- External imports import Data.List import Data.Maybe -import Objects + +-- Internal imports import Data.IdentityList +import Objects import Physics.TwoDimensions.Dimensions -- | Given a list of objects, it detects all the collisions between them. -- --- Note: this is a simple n*m-complex algorithm, with n the --- number of objects and m the number of moving objects (right now, --- only 2). +-- Note: this is a simple n*m-complex algorithm, with n the number of objects +-- and m the number of moving objects (right now, only 2). -- detectCollisions :: IL Object -> Collisions detectCollisions = detectCollisionsH - where detectCollisionsH objsT = flattened - where -- Eliminate empty collision sets - -- TODO: why is this really necessary? - flattened = filter (\(Collision n) -> not (null n)) collisions + where + detectCollisionsH objsT = flattened + where + -- Eliminate empty collision sets + -- TODO: why is this really necessary? + flattened = filter (\(Collision n) -> not (null n)) collisions - -- Detect collisions between moving objects and any other objects - collisions = detectCollisions' objsT moving + -- Detect collisions between moving objects and any other objects + collisions = detectCollisions' objsT moving - -- Partition the object space between moving and static objects - (moving, _static) = partition (canCauseCollisions.snd) $ assocsIL objsT + -- Partition the object space between moving and static objects + (moving, _static) = partition (canCauseCollisions.snd) $ assocsIL objsT --- | Detect collisions between each moving object and --- every other object. +-- | Detect collisions between each moving object and every other object. detectCollisions' :: IL Object -> [(ILKey, Object)] -> [Collision] detectCollisions' objsT ms = concatMap (detectCollisions'' objsT) ms -- | Detect collisions between one specific moving object and every existing --- object. Each collision is idependent of the rest (which is not necessarily +-- object. Each collision is independent of the rest (which is not necessarily -- what should happen, but since the transformed velocities are eventually -- added, there isn't much difference in the end). detectCollisions'' :: IL Object -> (ILKey, Object) -> [Collision] @@ -52,29 +60,8 @@ detectCollisions'' objsT m = concatMap (detectCollisions''' m) (assocsIL objsT) -- determine whether the two objects do collide. detectCollisions''' :: (ILKey, Object) -> (ILKey, Object) -> [Collision] detectCollisions''' m o - | fst m == fst o = [] -- Same object -> no collision - | otherwise = maybeToList (detectCollision (snd m) (snd o)) - --- updateObjPos :: SF (ILKey, Object) (ILKey, Object) --- updateObjPos = proc (i,o) -> do --- -- Since we are saving the position to avoid having to keep the last known --- -- position in memory every time and integrate over a range every time --- -- (would that really happen???) we use an integral over an interval. --- -- I really wonder if this integration thing in Yampa works the way it is --- -- expected to work. Does it work well for non-linear equations? --- -- --- -- Integral only for dt interval --- actualVel <- iterFrom (\_ (v1,v2) dt _ -> (v1 * dt, v2 * dt)) (0,0) -< objectVel o --- --- -- Update position --- let newPos = objectPos o ^+^ actualVel --- o' = o { objectPos = newPos } --- returnA -< (i,o') - --- killBall :: ObjectOutput -> ObjectOutput --- killBall oo = oo { outputObject = o' } --- where o = outputObject oo --- o' = o { objectDead = True} + | fst m == fst o = [] -- Same object -> no collision + | otherwise = maybeToList (detectCollision (snd m) (snd o)) -- | Return the new velocity as changed by the collection of collisions. -- @@ -83,34 +70,33 @@ detectCollisions''' m o -- The assumption is that collision detection happens globally and that the -- changed velocity is figured out for each object involved in a collision -- based on the properties of all objects involved in any specific interaction. --- That may not be how it works now, but the interface means it could work --- that way. Even more physical might be to figure out the impulsive force --- acting on each object. +-- That may not be how it works now, but the interface means it could work that +-- way. Even more physical might be to figure out the impulsive force acting on +-- each object. -- -- However, the whole collision infrastructure should be revisited. -- -- - Statefulness ("edge") might make it more robust. -- --- - Think through how collision events are going to be communicated --- to the objects themselves. Maybe an input event is the natural --- thing to do. Except then we have to be careful to avoid switching --- again immediately after one switch. +-- - Think through how collision events are going to be communicated to the +-- objects themselves. Maybe an input event is the natural thing to do. +-- Except then we have to be careful to avoid switching again immediately +-- after one switch. -- --- - Should try to avoid n^2 checks. Maybe some kind of quad-trees? --- Maybe spawning a stateful collision detector when two objects are --- getting close? Cf. the old tail-gating approach. --- - Maybe a collision should also carry the identity of the object --- one collieded with to facilitate impl. of "inCollisionWith". +-- - Should try to avoid n^2 checks. Maybe some kind of quad-trees? Maybe +-- spawning a stateful collision detector when two objects are getting close? +-- Cf. the old tail-gating approach. +-- - Maybe a collision should also carry the identity of the object one +-- collided with to facilitate impl. of "inCollisionWith". -- changedVelocity :: ObjectName -> Collisions -> Maybe Vel2D -changedVelocity name cs = - case concatMap (filter ((== name) . fst) . collisionData) cs of - [] -> Nothing - (_, v') : _ -> Just v' +changedVelocity name cs = + case concatMap (filter ((== name) . fst) . collisionData) cs of + [] -> Nothing + (_, v') : _ -> Just v' - -- IP: It should be something like the following, but that doesn't - -- work: - -- vs -> Just (foldl (^+^) (0,0) (map snd vs)) + -- IP: It should be something like the following, but that doesn't work: + -- vs -> Just (foldl (^+^) (0, 0) (map snd vs)) -- | True if the velocity of the object has been changed by any collision. inCollision :: ObjectName -> Collisions -> Bool @@ -119,7 +105,7 @@ inCollision name cs = isJust (changedVelocity name cs) -- | True if the two objects are colliding with one another. inCollisionWith :: ObjectName -> ObjectName -> Collisions -> Bool inCollisionWith nm1 nm2 cs = any (both nm1 nm2) cs - where - both nm1 nm2 (Collision nmvs) = - any ((== nm1) . fst) nmvs - && any ((== nm2) . fst) nmvs + where + both nm1 nm2 (Collision nmvs) = + any ((== nm1) . fst) nmvs + && any ((== nm2) . fst) nmvs diff --git a/src/GameState.hs b/src/GameState.hs index a74ff2e..5d6bb24 100644 --- a/src/GameState.hs +++ b/src/GameState.hs @@ -1,15 +1,17 @@ --- | The state of the game during execution. It has two --- parts: general info (level, points, etc.) and --- the actual gameplay info (objects). +-- | +-- Copyright : (c) Ivan Perez & Henrik Nilsson, 2014. +-- License : See LICENSE file. +-- Maintainer : Ivan Perez -- --- Because the game is always in some running state --- (there are no menus, etc.) we assume that there's --- always some gameplay info, even though it can be +-- The state of the game during execution. It has two parts: general info +-- (level, points, etc.) and the actual gameplay info (objects). +-- +-- Because the game is always in some running state (there are no menus, etc.) +-- we assume that there's always some gameplay info, even though it can be -- empty. module GameState where --- import FRP.Yampa as Yampa - +-- Internal imports import Objects -- | The running state is given by a bunch of 'Objects' and the current general @@ -18,7 +20,7 @@ import Objects -- -- Different parts of the game deal with these data structures. It is -- therefore convenient to group them in subtrees, even if there's no --- substantial difference betweem them. +-- substantial difference between them. data GameState = GameState { gameObjects :: Objects , gameInfo :: GameInfo @@ -31,9 +33,9 @@ neutralGameState = GameState , gameInfo = neutralGameInfo } --- | The gameinfo tells us the current game state (running, paused, etc.) --- and general information, in this case, the number of lives, the level --- and the points. +-- | The GameInfo tells us the current game state (running, paused, etc.) and +-- general information, in this case, the number of lives, the level and the +-- points. -- -- Since this info is then presented together to the users in a top panel, it -- is convenient to give this product of values a proper name. @@ -54,13 +56,13 @@ neutralGameInfo = GameInfo } -- | Possible actual game statuses. The game is always in one of these. --- Interaction and presentation depend on this. Yampa switches are --- used to jump from one to another, and the display module --- changes presentation depending on the status. +-- Interaction and presentation depend on this. Yampa switches are used to jump +-- from one to another, and the display module changes presentation depending +-- on the status. data GameStatus = GamePlaying | GamePaused | GameLoading Int | GameOver | GameFinished | GameStarted - deriving Eq + deriving Eq diff --git a/src/Graphics/UI/Extra/FPS.hs b/src/Graphics/UI/Extra/FPS.hs index 883713d..3773a1e 100644 --- a/src/Graphics/UI/Extra/FPS.hs +++ b/src/Graphics/UI/Extra/FPS.hs @@ -1,5 +1,12 @@ +-- | +-- Copyright : (c) Ivan Perez & Henrik Nilsson, 2014. +-- License : See LICENSE file. +-- Maintainer : Ivan Perez +-- +-- Auxiliary functions to calculate frames-per-second. module Graphics.UI.Extra.FPS where +-- External imports import Control.Monad import Data.IORef @@ -24,14 +31,15 @@ stepFPSCounter (clock, fpsRef, every) = do (lastTime, left) <- readIORef fpsRef let left' = left - 1 if left' < 0 - then do newTime <- clock - let - msf = fromIntegral (newTime - lastTime) / (fromIntegral every :: Double) - fps = 1000 / msf - do -- when (msf > 0) $ - putStrLn $ "Performance report :: Time per frame: " ++ show msf - ++ "ms, FPS: " ++ show fps - ++ ", Total running time: " ++ show newTime - writeIORef fpsRef (newTime, every) - else do writeIORef fpsRef (lastTime, left') + then do newTime <- clock + let + msf = fromIntegral (newTime - lastTime) + / (fromIntegral every :: Double) + fps = 1000 / msf + do -- when (msf > 0) $ + putStrLn $ "Performance report :: Time per frame: " ++ show msf + ++ "ms, FPS: " ++ show fps + ++ ", Total running time: " ++ show newTime + writeIORef fpsRef (newTime, every) + else do writeIORef fpsRef (lastTime, left') diff --git a/src/Graphics/UI/Extra/SDL.hs b/src/Graphics/UI/Extra/SDL.hs index e3e8c1d..022044f 100644 --- a/src/Graphics/UI/Extra/SDL.hs +++ b/src/Graphics/UI/Extra/SDL.hs @@ -1,5 +1,12 @@ +-- | +-- Copyright : (c) Ivan Perez & Henrik Nilsson, 2014. +-- License : See LICENSE file. +-- Maintainer : Ivan Perez +-- +-- Auxiliary functions related to Graphics.UI.SDL. module Graphics.UI.Extra.SDL where +-- External imports import Data.IORef import Graphics.UI.SDL as SDL diff --git a/src/Input.hs b/src/Input.hs index 8611a30..9b02595 100644 --- a/src/Input.hs +++ b/src/Input.hs @@ -1,9 +1,13 @@ {-# LANGUAGE ExistentialQuantification #-} {-# LANGUAGE CPP #-} --- | Defines an abstraction for the game controller and the functions to read --- it. +-- | +-- Copyright : (c) Ivan Perez & Henrik Nilsson, 2014. +-- License : See LICENSE file. +-- Maintainer : Ivan Perez -- --- Lower-level devices replicate the higher-level API, and should accomodate to +-- Defines an abstraction for the game controller and the functions to read it. +-- +-- Lower-level devices replicate the higher-level API, and should accommodate to -- it. Each device should: -- -- - Upon initialisation, return any necessary information to poll it again. @@ -18,7 +22,7 @@ -- updated version. -- -- Limitations: --- +-- -- - Device failures are not handled. -- -- - Falling back to the next available device when there's a problem. @@ -31,34 +35,32 @@ module Input where -- External imports +import Control.Monad import Data.IORef import Graphics.UI.SDL as SDL -import Control.Monad -- External imports (Wiimote) #ifdef wiimote -import Control.Monad(void) +import Control.Monad (void) import Control.Monad.IfElse (awhen) -import Data.Maybe (fromMaybe) +import Data.Maybe (fromMaybe) import System.CWiid #endif -- External imports (Kinect) #ifdef kinect -import Control.Concurrent -import Data.Maybe (fromJust) -import Data.Vector.Storable (Vector,(!)) -import Data.Word -import Freenect +import Control.Concurrent +import Data.Vector.Storable (Vector, (!)) import qualified Data.Vector.Storable as V +import Data.Word +import Freenect #endif -- Internal imports +import Constants import Control.Extra.Monad import Graphics.UI.Extra.SDL -import Constants - -- * Game controller -- | Controller info at any given point. @@ -66,54 +68,53 @@ data Controller = Controller { controllerPos :: (Double, Double) , controllerClick :: Bool , controllerPause :: Bool + , controllerQuit :: Bool } --- | Controller info at any given point, plus a pointer --- to poll the main device again. This is safe, --- since there is only one writer at a time (the device itself). +-- | Controller info at any given point, plus a pointer to poll the main device +-- again. This is safe, since there is only one writer at a time (the device +-- itself). newtype ControllerRef = ControllerRef (IORef Controller, Controller -> IO Controller) -- * General API --- | Initialize the available input devices. This operation --- returns a reference to a controller, which enables --- getting its state as many times as necessary. It does --- not provide any information about its nature, abilities, etc. +-- | Initialize the available input devices. This operation returns a reference +-- to a controller, which enables getting its state as many times as necessary. +-- It does not provide any information about its nature, abilities, etc. initializeInputDevices :: IO ControllerRef initializeInputDevices = do - let baseDev = sdlGetController + let baseDev = sdlGetController -- Fall back to mouse/kb is no kinect is present #ifdef kinect - print "Kinecting" - dev <- do kn <- kinectController - case kn of - Nothing -> return baseDev - Just kn' -> return kn' + print "Kinecting" + dev <- do kn <- kinectController + case kn of + Nothing -> return baseDev + Just kn' -> return kn' #else - let dev = baseDev + let dev = baseDev #endif -- Fall back to kinect or mouse/kb is no wiimote is present #ifdef wiimote - dev' <- do wm <- wiimoteDev - return $ fromMaybe dev wm + dev' <- do wm <- wiimoteDev + return $ fromMaybe dev wm #else - let dev' = dev + let dev' = dev #endif - nr <- newIORef defaultInfo - return $ ControllerRef (nr, dev') - where defaultInfo = Controller (0,0) False False - --- | Sense from the controller, providing its current --- state. This should return a new Controller state --- if available, or the last one there was. --- --- It is assumed that the sensing function is always --- callable, and that it knows how to update the --- Controller info if necessary. + nr <- newIORef defaultInfo + return $ ControllerRef (nr, dev') + where + defaultInfo = Controller (0, 0) False False False + +-- | Sense from the controller, providing its current state. This should return +-- a new Controller state if available, or the last one there was. +-- +-- It is assumed that the sensing function is always callable, and that it +-- knows how to update the Controller info if necessary. senseInput :: ControllerRef -> IO Controller senseInput (ControllerRef (cref, sensor)) = do cinfo <- readIORef cref @@ -126,8 +127,8 @@ type ControllerDev = IO (Maybe (Controller -> IO Controller)) -- * WiiMote API (mid-level) #ifdef wiimote --- | The wiimote controller as defined using this --- abstract interface. See 'initializeWiimote'. +-- | The wiimote controller as defined using this abstract interface. See +-- 'initializeWiimote'. wiimoteDev :: ControllerDev wiimoteDev = initializeWiimote @@ -140,19 +141,19 @@ initializeWiimote :: ControllerDev initializeWiimote = do putStrLn "Initializing WiiMote. Please press 1+2 to connect." wm <- cwiidOpen - awhen wm (void . (`cwiidSetRptMode` 15)) -- Enable button reception, acc and IR + awhen wm (void . (`cwiidSetRptMode` 15)) -- Enable button reception, acc, IR case wm of - Nothing -> return Nothing - Just wm' -> return $ Just $ senseWiimote wm' + Nothing -> return Nothing + Just wm' -> return $ Just $ senseWiimote wm' -- ** Sensing -- | Sense the Wiimote and update the controller. -- --- This operation uses the IR for the controller's position, --- and the main (A) button for the click. +-- This operation uses the IR for the controller's position, and the main (A) +-- button for the click. -- --- TODO: Allow configuring the button and using other motion mechamisms +-- TODO: Allow configuring the button and using other motion mechanisms -- (accelerometers). -- -- TODO: This should be split in two operations. One that presents a nice @@ -164,8 +165,8 @@ senseWiimote wmdev controller = do -- Obtain positions of leds 1 and 2 (with a normal wii bar, those -- will be the ones we use). - let led1 = irs!!0 - led2 = irs!!1 + let led1 = irs !! 0 + led2 = irs !! 1 -- Calculate mid point between sensor bar leds let posX = ((cwiidIRSrcPosX led1) + (cwiidIRSrcPosX led2)) `div` 2 @@ -176,15 +177,17 @@ senseWiimote wmdev controller = do propY = fromIntegral (max 0 (posY - 384)) / 384.0 -- Calculate game area coordinates - let finX = width * propX - finY = height * propY + let finX = width * propX + finY = height * propY -- Direction (old system based on buttons) -- let isLeft = cwiidIsBtnPushed flags cwiidBtnLeft - -- isRight = cwiidIsBtnPushed flags cwiidBtnRight - -- (x,y) = controllerPos controller - -- x' = if isLeft then x - wiiXDiff else if isRight then x + wiiXDiff else x - -- x'' = inRange (0, gameWidth) x' + -- isRight = cwiidIsBtnPushed flags cwiidBtnRight + -- (x, y) = controllerPos controller + -- x' | isLeft = x - wiiXDiff + -- | isRight = x + wiiXDiff + -- | otherwise = x + -- x'' = inRange (0, gameWidth) x' -- pos' = (x'', y) -- wiiXDiff :: Float -- wiiXDiff = 6 @@ -208,10 +211,9 @@ sdlMouseKB = return (Just sdlGetController) -- ** Sensing --- | Sense the SDL keyboard and mouse and update --- the controller. It only senses the mouse position, --- the primary mouse button, and the p key to pause --- the game. +-- | Sense the SDL keyboard and mouse and update the controller. It only senses +-- the mouse position, the primary mouse button, and the p key to pause the +-- game. -- -- We need a non-blocking controller-polling function. -- TODO: Check http://gameprogrammer.com/fastevents/fastevents1.html @@ -223,14 +225,21 @@ sdlGetController info = handleEvent :: Controller -> SDL.Event -> Controller handleEvent c e = case e of - MouseMotion x y _ _ -> c { controllerPos = (fromIntegral x, fromIntegral y)} - MouseButtonDown _ _ ButtonLeft -> c { controllerClick = True } - MouseButtonUp _ _ ButtonLeft -> c { controllerClick = False} - KeyUp Keysym { symKey = SDLK_p } -> c { controllerPause = not (controllerPause c) } - KeyDown Keysym { symKey = SDLK_SPACE } -> c { controllerClick = True } - KeyUp Keysym { symKey = SDLK_SPACE } -> c { controllerClick = False } - _ -> c - + MouseMotion x y _ _ -> + c { controllerPos = (fromIntegral x, fromIntegral y)} + MouseButtonDown _ _ ButtonLeft -> + c { controllerClick = True } + MouseButtonUp _ _ ButtonLeft -> + c { controllerClick = False} + KeyUp Keysym { symKey = SDLK_p } -> + c { controllerPause = not (controllerPause c) } + KeyDown Keysym { symKey = SDLK_SPACE } -> + c { controllerClick = True } + KeyUp Keysym { symKey = SDLK_SPACE } -> + c { controllerClick = False } + KeyDown Keysym { symKey = SDLK_q } -> + c { controllerQuit = True } + _ -> c -- Kinect @@ -253,6 +262,7 @@ kinectWidth = 640 kinectHeight = 480 type KinectPosRef = IORef KinectPos + type KinectPos = Maybe (Double, Double) initializeKinect :: (Double, Double) -> IO KinectPosRef @@ -263,54 +273,61 @@ initializeKinect screenSize = do getDepthThread :: (Double, Double) -> KinectPosRef -> IO ThreadId getDepthThread screenSize lastPos = forkIO $ do - withContext $ \context -> do - setLogLevel LogFatal context - selectSubdevices context devices - withDevice context index $ \device -> do - setDepthMode device Medium ElevenBit - setDepthCallback device $ \payload _timestamp -> do - maybe (print ".") -- Too far or too close - (updatePos lastPos) - (calculateMousePos screenSize payload) - return () - startDepth device - forever $ processEvents context - - where devices = [Camera] - index = 0 :: Integer + withContext $ \context -> do + setLogLevel LogFatal context + selectSubdevices context devices + withDevice context index $ \device -> do + setDepthMode device Medium ElevenBit + setDepthCallback device $ \payload _timestamp -> do + maybe (print ".") -- Too far or too close + (updatePos lastPos) + (calculateMousePos screenSize payload) + return () + startDepth device + forever $ processEvents context + + where + devices = [Camera] + index = 0 :: Integer updatePos :: IORef (Maybe (Double, Double)) -> (Double, Double) -> IO () -updatePos lastPosRef newPos@(nx,ny) = do +updatePos lastPosRef newPos@(nx, ny) = do lastPosM <- readIORef lastPosRef let (mx, my) = case lastPosM of - Nothing -> newPos - (Just (lx,ly)) -> (adjust 50 lx nx, adjust 50 ly ny) + Nothing -> newPos + (Just (lx, ly)) -> (adjust 50 lx nx, adjust 50 ly ny) writeIORef lastPosRef (Just (mx, my)) mx `seq` my `seq` return () -calculateMousePos :: (Double, Double) -> Vector Word16 -> Maybe (Double, Double) +calculateMousePos :: (Double, Double) + -> Vector Word16 + -> Maybe (Double, Double) calculateMousePos (width, height) payload = - fmap g (findFirst payload) - where g (px,py) = (mousex, mousey) - where - pointerx = fromIntegral (640 - px) - pointery = fromIntegral py - mousex = pointerx -- pointerx * adjx - mousey = pointery -- pointery * adjy - adjx = width / 630.0 - adjy = height / 470.0 + fmap g (findFirst payload) + where + g (px, py) = (mousex, mousey) + where + pointerx = fromIntegral (640 - px) + pointery = fromIntegral py + mousex = pointerx -- pointerx * adjx + mousey = pointery -- pointery * adjy + adjx = width / 630.0 + adjy = height / 470.0 mat :: Vector Float -mat = V.generate 2048 (\i -> let v :: Float - v = ((fromIntegral i/2048.0)^3)*6.0 in v * 6.0 * 256.0) +mat = V.generate 2048 $ \i -> + let v :: Float + v = ((fromIntegral i / 2048.0)^3) * 6.0 + in v * 6.0 * 256.0 findFirst :: Vector Word16 -> Maybe (Int, Int) findFirst vs = fmap (\v -> (v `mod` 640, v `div` 640)) i - where i = V.findIndex (\x -> mat!(fromIntegral x) < 512) vs + where + i = V.findIndex (\x -> mat ! (fromIntegral x) < 512) vs processPayload :: Vector Word16 -> [(Float, Int, Int)] -processPayload ps = [(pval, tx, ty) | i <- [0..640*480-1] - , let pval = mat!(fromIntegral (ps!i)) +processPayload ps = [(pval, tx, ty) | i <- [0 .. 640 * 480 - 1] + , let pval = mat ! (fromIntegral (ps ! i)) , pval < 300 , let ty = i `div` 640 tx = i `mod` 640 @@ -319,8 +336,9 @@ processPayload ps = [(pval, tx, ty) | i <- [0..640*480-1] -- Drop the fst elem, calculate the avg of snd and trd over the whole list avg :: [(Float, Int, Int)] -> (Int, Int) avg ls = (sumx `div` l, sumy `div` l) - where l = length ls - (sumx, sumy) = foldr (\(_,x,y) (rx,ry) -> (x+rx,y+ry)) (0,0) ls + where + l = length ls + (sumx, sumy) = foldr (\(_, x, y) (rx, ry) -> (x + rx, y + ry)) (0, 0) ls -- Update a value, with a max cap adjust :: (Num a, Ord a) => a -> a -> a -> a @@ -328,6 +346,4 @@ adjust maxD old new | abs (old - new) < maxD = new | old < new = old + maxD | otherwise = old - maxD - #endif - diff --git a/src/Levels.hs b/src/Levels.hs index c46dc74..0f6f314 100644 --- a/src/Levels.hs +++ b/src/Levels.hs @@ -1,4 +1,9 @@ --- | Level definition +-- | +-- Copyright : (c) Ivan Perez & Henrik Nilsson, 2014. +-- License : See LICENSE file. +-- Maintainer : Ivan Perez +-- +-- Level definition -- -- This module includes the definition of the levels. -- @@ -11,23 +16,24 @@ -- Together they form 'levels'. module Levels where -import Control.Arrow ((***), first) -import Data.List (nub) -import Physics.TwoDimensions.Dimensions +-- External imports +import Control.Arrow (first, (***)) +import Data.List (nub) +-- Internal imports import Constants +import Physics.TwoDimensions.Dimensions import Resources -- * Levels -- ** Level specification data LevelSpec = LevelSpec - { blockCfgs :: [(Pos2D, Int)] -- ^ Block positions and block lives - , levelBg :: ImageResource -- ^ Background image - , levelMusic :: MusicResource -- ^ Background music - } + { blockCfgs :: [(Pos2D, Int)] -- ^ Block positions and block lives + , levelBg :: ImageResource -- ^ Background image + , levelMusic :: MusicResource -- ^ Background music + } --- | Number of levels. Change this in the code to finish --- in a different level. +-- | Number of levels. Change this in the code to finish in a different level. numLevels :: Int numLevels = length levels @@ -142,7 +148,7 @@ levels = [ -- Level 0 } ] --- | Level block specification (positions,lives of block) +-- | Level block specification (positions, lives of block) -- Level 0 -- %%%%%%%% @@ -153,48 +159,51 @@ levels = [ -- Level 0 blockDescS :: Int -> [(Pos2D, Int)] blockDescS 0 = map (first adjustPos) allBlocks - where allBlocks :: (Enum a, Num a) => [((a,a), Int)] - allBlocks = [((x,y), maxBlockLife) | x <- [0..blockColumns - 1] - , y <- [0..blockRows - 1] - ] - - blockRows :: Num a => a - blockRows = 4 + where + allBlocks :: (Enum a, Num a) => [((a, a), Int)] + allBlocks = [ ((x, y), maxBlockLife) + | x <- [0..blockColumns - 1] + , y <- [0..blockRows - 1] + ] + blockRows :: Num a => a + blockRows = 4 -- Level 1 -- %%%%%%%% -- % XXXX --- % XXXXX +-- % XXXXX -- % XXXXXX -- % XXXXXX -- % XXXXX -- % XXXX -- blockDescS 1 = map (first adjustPos) allBlocks - where - allBlocks :: (Enum a, Num a, Eq a, Ord a) => [((a,a), Int)] - allBlocks = [((x,y), maxBlockLife) | x <- [0..blockColumns - 1] - , y <- [0..blockRows - 1] - , (x + y > 2) && (x + y < 10) - ] - - blockRows :: Num a => a - blockRows = 6 + where + allBlocks :: (Enum a, Num a, Eq a, Ord a) => [((a, a), Int)] + allBlocks = [ ((x, y), maxBlockLife) + | x <- [0..blockColumns - 1] + , y <- [0..blockRows - 1] + , (x + y > 2) && (x + y < 10) + ] + + blockRows :: Num a => a + blockRows = 6 -- Level 2 blockDescS 2 = map (first adjustPos) allBlocks - where - allBlocks :: (Enum a, Num a, Eq a) => [((a,a), Int)] - allBlocks = [((x,y), maxBlockLife) | x <- [0..blockColumns - 1] - , y <- [0..blockRows - 1] - , x /= y && (blockColumns - 1 - x) /= y - ] + where + allBlocks :: (Enum a, Num a, Eq a) => [((a, a), Int)] + allBlocks = [ ((x, y), maxBlockLife) + | x <- [0..blockColumns - 1] + , y <- [0..blockRows - 1] + , x /= y && (blockColumns - 1 - x) /= y + ] + + blockRows :: Num a => a + blockRows = 4 - blockRows :: Num a => a - blockRows = 4 - -- Level 3 -- %%%%%%%% @@ -204,16 +213,17 @@ blockDescS 2 = map (first adjustPos) allBlocks -- % X X X X blockDescS 3 = map (first adjustPos) allBlocks - where allBlocks :: (Enum a, Num a, Eq a, Integral a) => [((a,a), Int)] - allBlocks = [((x,y), maxBlockLife) | x <- [0..blockColumns - 1] - , y <- [0..blockRows - 1] - , ((even x) && (odd y) || - (odd x) && (even y)) - ] + where - blockRows :: Num a => a - blockRows = 4 + allBlocks :: (Enum a, Num a, Eq a, Integral a) => [((a, a), Int)] + allBlocks = [ ((x, y), maxBlockLife) + | x <- [0..blockColumns - 1] + , y <- [0..blockRows - 1] + , even x && odd y || odd x && even y + ] + blockRows :: Num a => a + blockRows = 4 -- Level 4 -- %%%%%%%% @@ -226,16 +236,24 @@ blockDescS 3 = map (first adjustPos) allBlocks -- % XXXXXXXX blockDescS 4 = map (first adjustPos) allBlocks - where allBlocks :: (Enum a, Num a, Eq a, Integral a) => [((a,a), Int)] - allBlocks = [((x,y), maxBlockLife) | x <- [0..blockColumns - 1] - , y <- [0,blockRows - 1]] - ++ [((x,y), maxBlockLife) | x <- [0..blockColumns - 1] - , y <- [2], odd x] - ++ [((x,y), maxBlockLife) | x <- [0..blockColumns - 1] - , y <- [4], even x] - - blockRows :: Num a => a - blockRows = 7 + where + + allBlocks :: (Enum a, Num a, Eq a, Integral a) => [((a, a), Int)] + allBlocks = [ ((x, y), maxBlockLife) + | x <- [0..blockColumns - 1] + , y <- [0, blockRows - 1] + ] + ++ [ ((x, y), maxBlockLife) + | x <- [0..blockColumns - 1] + , y <- [2], odd x + ] + ++ [ ((x, y), maxBlockLife) + | x <- [0..blockColumns - 1] + , y <- [4], even x + ] + + blockRows :: Num a => a + blockRows = 7 -- Level 5 -- %%%%%%%% @@ -245,12 +263,14 @@ blockDescS 4 = map (first adjustPos) allBlocks -- % XXXXXXX blockDescS 5 = map (first adjustPos) allBlocks - where allBlocks :: (Enum a, Num a, Eq a, Integral a) => [((a,a), Int)] - allBlocks = nub $ - [((3,0), maxBlockLife),((blockColumns - 4,1), maxBlockLife)] - ++ [((2,1), maxBlockLife),((blockColumns - 3,1), maxBlockLife)] - ++ [((1,2), maxBlockLife),((blockColumns - 2,2), maxBlockLife)] - ++ [((x,y), maxBlockLife) | x <- [0..blockColumns - 1], y <- [3]] + where + + allBlocks :: (Enum a, Num a, Eq a, Integral a) => [((a, a), Int)] + allBlocks = nub $ + [((3, 0), maxBlockLife), ((blockColumns - 4, 1), maxBlockLife)] + ++ [((2, 1), maxBlockLife), ((blockColumns - 3, 1), maxBlockLife)] + ++ [((1, 2), maxBlockLife), ((blockColumns - 2, 2), maxBlockLife)] + ++ [((x, y), maxBlockLife) | x <- [0..blockColumns - 1], y <- [3]] -- Level 6 -- %%%%%%%% @@ -261,42 +281,55 @@ blockDescS 5 = map (first adjustPos) allBlocks -- % XXXXXX blockDescS 6 = map (first adjustPos) allBlocks - where allBlocks :: (Enum a, Num a, Eq a, Integral a) => [((a,a), Int)] - allBlocks = [((x,y), maxBlockLife) | x <- [1..blockColumns - 2] - , y <- [0, blockRows - 1]] - ++ [((x,y), maxBlockLife) | x <- [0, blockColumns - 1] - , y <- [1..blockRows - 2]] + where - blockRows :: Num a => a - blockRows = 5 + allBlocks :: (Enum a, Num a, Eq a, Integral a) => [((a, a), Int)] + allBlocks = [ ((x, y), maxBlockLife) + | x <- [1..blockColumns - 2] + , y <- [0, blockRows - 1] + ] + ++ [ ((x, y), maxBlockLife) + | x <- [0, blockColumns - 1] + , y <- [1..blockRows - 2] + ] + blockRows :: Num a => a + blockRows = 5 -- Level 7 -- %%%%%%%% -- % XXXXXX -- % X X --- % XX --- % XX +-- % XX +-- % XX -- % X X -- % XXXXXX blockDescS 7 = map (first adjustPos) allBlocks - where allBlocks :: (Enum a, Num a, Eq a, Integral a) => [((a,a), Int)] - allBlocks = [((x,y), maxBlockLife) | x <- [1..blockColumns - 2] - , y <- [0, blockRows - 1]] - ++ [((x,y), maxBlockLife) | x <- [0, blockColumns - 1] - , y <- [1, blockRows - 2]] - ++ [((x,y), maxBlockLife) | x <- [3,4] - , y <- [2..4]] - - blockRows :: Num a => a - blockRows = 7 + where + + allBlocks :: (Enum a, Num a, Eq a, Integral a) => [((a, a), Int)] + allBlocks = [ ((x, y), maxBlockLife) + | x <- [1..blockColumns - 2] + , y <- [0, blockRows - 1] + ] + ++ [ ((x, y), maxBlockLife) + | x <- [0, blockColumns - 1] + , y <- [1, blockRows - 2] + ] + ++ [ ((x, y), maxBlockLife) + | x <- [3, 4] + , y <- [2..4] + ] + + blockRows :: Num a => a + blockRows = 7 -- Level 8 -- %%%%%%%% -- % XX XXXXX -- % XX XXXXX --- % +-- % -- % XX XXXXX -- % XX XXXXX -- % XX XXXXX @@ -306,14 +339,18 @@ blockDescS 7 = map (first adjustPos) allBlocks -- % XX XXXXX blockDescS 8 = map (first adjustPos) allBlocks - where allBlocks :: (Enum a, Num a, Eq a, Integral a) => [((a,a), Int)] - allBlocks = [((x,y), maxBlockLife) | x <- [0..blockColumns - 1] - , y <- [0..blockRows - 1] - , x /= 2, y /= 2 - ] + where + + allBlocks :: (Enum a, Num a, Eq a, Integral a) => [((a, a), Int)] + allBlocks = [ ((x, y), maxBlockLife) + | x <- [0..blockColumns - 1] + , y <- [0..blockRows - 1] + , x /= 2 + , y /= 2 + ] - blockRows :: Num a => a - blockRows = 9 + blockRows :: Num a => a + blockRows = 9 -- Level 9 -- %%%%%%%% @@ -326,70 +363,80 @@ blockDescS 8 = map (first adjustPos) allBlocks -- % X blockDescS 9 = map (first ((adjustHPos *** adjustVPos) . fI2)) allBlocks - where allBlocks :: (Enum a, Num a, Eq a, Integral a) => [((a,a), Int)] - allBlocks = [((x,y), maxBlockLife) | x <- [3], y <- [0..6]] - ++ [((x,y), maxBlockLife) | x <- [0..6], y <- [3]] - ++ [((x,y), maxBlockLife) | x <- [2,4], y <- [1,5]] - ++ [((x,y), maxBlockLife) | x <- [1,5], y <- [2,4]] - - adjustHPos :: Double -> Double - adjustHPos = (leftMargin +) . ((blockWidth + blockSeparation) *) + where - leftMargin :: Num a => a - leftMargin = round' ((gameWidth - (blockWidth + blockSeparation) * 7)/2) - where round' = fromIntegral . floor + allBlocks :: (Enum a, Num a, Eq a, Integral a) => [((a, a), Int)] + allBlocks = [ ((x, y), maxBlockLife) | x <- [3], y <- [0..6] ] + ++ [ ((x, y), maxBlockLife) | x <- [0..6], y <- [3] ] + ++ [ ((x, y), maxBlockLife) | x <- [2, 4], y <- [1, 5] ] + ++ [ ((x, y), maxBlockLife) | x <- [1, 5], y <- [2, 4] ] + adjustHPos :: Double -> Double + adjustHPos = (leftMargin +) . ((blockWidth + blockSeparation) *) + + leftMargin :: Num a => a + leftMargin = + round' ((gameWidth - (blockWidth + blockSeparation) * 7) / 2) + where + round' = fromIntegral . floor -- Level 10 -- %%%%%%%% -- % X X X --- % X X X --- % X X X --- % X X X +-- % X X X +-- % X X X +-- % X X X -- % XXXXXXX --- % X X X --- % X X X -- % X X X --- % X X X +-- % X X X +-- % X X X +-- % X X X blockDescS 10 = map (first adjustPos) allBlocks - where allBlocks :: (Enum a, Num a, Eq a, Integral a) => [((a,a), Int)] - allBlocks = [((x,y), maxBlockLife) | x <- [0..blockColumns - 1] - , y <- [0..blockRows - 1], odd x] - ++ [((x,y), maxBlockLife) | x <- [0..blockColumns - 1] - , y <- [midRow], even x] - - blockRows :: Num a => a - blockRows = 9 + where - midRow :: Integral a => a - midRow = blockRows `div` 2 + allBlocks :: (Enum a, Num a, Eq a, Integral a) => [((a, a), Int)] + allBlocks = [ ((x, y), maxBlockLife) + | x <- [0..blockColumns - 1] + , y <- [0..blockRows - 1] + , odd x + ] + ++ [ ((x, y), maxBlockLife) + | x <- [0..blockColumns - 1] + , y <- [midRow] + , even x + ] + blockRows :: Num a => a + blockRows = 9 + midRow :: Integral a => a + midRow = blockRows `div` 2 -- Level 11 -- %%%%%%%% --- % XX --- % XXXX --- % XXXXXX +-- % XX +-- % XXXX +-- % XXXXXX -- % XXXXXXXX --- % XXXXXX --- % XXXX --- % XX +-- % XXXXXX +-- % XXXX +-- % XX blockDescS 11 = map (first adjustPos) allBlocks - where allBlocks :: (Enum a, Num a, Eq a, Integral a) => [((a,a), Int)] - allBlocks = [((x,y), maxBlockLife) | y <- [0..blockRows-1] - , x <- [0..(blockColumns-1) - - (2 * abs (y - midRow))] - ] - + where + + allBlocks :: (Enum a, Num a, Eq a, Integral a) => [((a, a), Int)] + allBlocks = [ ((x, y), maxBlockLife) + | y <- [0 .. blockRows - 1] + , x <- [0 .. (blockColumns - 1) - (2 * abs (y - midRow))] + ] - blockRows :: Num a => a - blockRows = 7 + blockRows :: Num a => a + blockRows = 7 - midRow :: Integral a => a - midRow = blockRows `div` 2 + midRow :: Integral a => a + midRow = blockRows `div` 2 -- Level 12 -- %%%%%%%% @@ -402,19 +449,25 @@ blockDescS 11 = map (first adjustPos) allBlocks blockDescS 12 = map (first adjustPos) allBlocks - where allBlocks :: (Enum a, Num a, Eq a, Integral a) => [((a,a), Int)] - allBlocks = [((x,y), maxBlockLife) | x <- [0..blockColumns - 1] - , y <- [0]] - ++ [((x,y), maxBlockLife) | x <- [2, blockColumns - 3] - , y <- [1, 2]] - ++ [ ((1,4), maxBlockLife) - , ((blockColumns - 2,4), maxBlockLife) - , ((0,5), maxBlockLife) - , ((blockColumns - 1,5), maxBlockLife) - ] - - blockRows :: Num a => a - blockRows = 9 + where + + allBlocks :: (Enum a, Num a, Eq a, Integral a) => [((a, a), Int)] + allBlocks = [ ((x, y), maxBlockLife) + | x <- [0..blockColumns - 1] + , y <- [0] + ] + ++ [ ((x, y), maxBlockLife) + | x <- [2, blockColumns - 3] + , y <- [1, 2] + ] + ++ [ ((1, 4), maxBlockLife) + , ((blockColumns - 2, 4), maxBlockLife) + , ((0, 5), maxBlockLife) + , ((blockColumns - 1, 5), maxBlockLife) + ] + + blockRows :: Num a => a + blockRows = 9 -- Level 13 -- X == maxBlockLife @@ -426,16 +479,20 @@ blockDescS 12 = map (first adjustPos) allBlocks -- % OXOXOXOX blockDescS 13 = map (first adjustPos) allBlocks - where allBlocks :: (Enum a, Num a, Eq a, Integral a) => [((a,a), Int)] - allBlocks = [((x,y), blockLife) | x <- [0..blockColumns - 1] - , y <- [0..blockRows - 1] - , let blockLife = if even (x + y) - then maxBlockLife - else maxBlockLife - 1 - ] - - blockRows :: Num a => a - blockRows = 4 + where + + allBlocks :: (Enum a, Num a, Eq a, Integral a) => [((a, a), Int)] + allBlocks = [ ((x, y), blockLife) + | x <- [0..blockColumns - 1] + , y <- [0..blockRows - 1] + , let blockLife = + if even (x + y) + then maxBlockLife + else maxBlockLife - 1 + ] + + blockRows :: Num a => a + blockRows = 4 -- Level 14 -- X == maxBlockLife @@ -449,29 +506,35 @@ blockDescS 13 = map (first adjustPos) allBlocks -- % YYYYYYYY blockDescS 14 = map (first adjustPos) allBlocks - where allBlocks :: (Enum a, Num a, Eq a, Integral a) => [((a,a), Int)] - allBlocks = [((x,y), minBlockLife) | x <- [0..blockColumns - 1] - , y <- [0..blockRows - 1] - , (x == 0) - || (y == 0) - || (x == blockColumns - 1) - || (y == blockRows - 1)] - ++ [((x,y), maxBlockLife - 1) | x <- [1..blockColumns - 2] - , y <- [1..blockRows - 2] - , (x == 1) - || (y == 1) - || (x == blockColumns - 2) - || (y == blockRows - 2)] - ++ [((x,y), maxBlockLife) | x <- [2..blockColumns - 3] - , y <- [2..blockRows - 3] - , (x == 2) - || (y == 2) - || (x == blockColumns - 3) - || (y == blockRows - 3)] - - blockRows :: Num a => a - blockRows = 5 - + where + + allBlocks :: (Enum a, Num a, Eq a, Integral a) => [((a, a), Int)] + allBlocks = [ ((x, y), minBlockLife) + | x <- [0..blockColumns - 1] + , y <- [0..blockRows - 1] + , (x == 0) + || (y == 0) + || (x == blockColumns - 1) + || (y == blockRows - 1)] + + ++ [ ((x, y), maxBlockLife - 1) + | x <- [1..blockColumns - 2] + , y <- [1..blockRows - 2] + , (x == 1) + || (y == 1) + || (x == blockColumns - 2) + || (y == blockRows - 2)] + + ++ [ ((x, y), maxBlockLife) + | x <- [2..blockColumns - 3] + , y <- [2..blockRows - 3] + , (x == 2) + || (y == 2) + || (x == blockColumns - 3) + || (y == blockRows - 3)] + + blockRows :: Num a => a + blockRows = 5 -- Level 15 -- maxBlockLife == X @@ -479,32 +542,40 @@ blockDescS 14 = map (first adjustPos) allBlocks -- blockLife == T -- %%%%%%%% -- % X X X --- % X X X --- % X X X --- % X X X +-- % X X X +-- % X X X +-- % X X X -- % OXOXOXO --- % T T T --- % T T T -- % T T T --- % T T T +-- % T T T +-- % T T T +-- % T T T blockDescS 15 = map (first adjustPos) allBlocks - where allBlocks :: (Enum a, Num a, Eq a, Integral a) => [((a,a), Int)] - allBlocks = [((x,y), maxBlockLife) | x <- [0..blockColumns - 1] - , y <- [0..midRow], odd x] - ++ [((x,y), minBlockLife) | x <- [0..blockColumns - 1] - , y <- [midRow], even x] - ++ [((x,y), maxBlockLife - 1) | x <- [0..blockColumns - 1] - , y <- [midRow - + 1..blockColumns - 1] - , even x] - - blockRows :: Num a => a - blockRows = 9 - - midRow :: Integral a => a - midRow = blockRows `div` 2 - + where + + allBlocks :: (Enum a, Num a, Eq a, Integral a) => [((a, a), Int)] + allBlocks = [ ((x, y), maxBlockLife) + | x <- [0..blockColumns - 1] + , y <- [0..midRow] + , odd x + ] + ++ [ ((x, y), minBlockLife) + | x <- [0..blockColumns - 1] + , y <- [midRow] + , even x + ] + ++ [ ((x, y), maxBlockLife - 1) + | x <- [0..blockColumns - 1] + , y <- [midRow + 1..blockColumns - 1] + , even x + ] + + blockRows :: Num a => a + blockRows = 9 + + midRow :: Integral a => a + midRow = blockRows `div` 2 -- Level 16 -- maxBlockLife == X @@ -513,37 +584,44 @@ blockDescS 15 = map (first adjustPos) allBlocks -- %%%%%%%% -- % XXXOOXXX -- % XXXOOXXX --- % +-- % -- % T T T T -- % T T T T --- % +-- % -- % XXXOOXXX -- % XXXOOXXX blockDescS 16 = map (first adjustPos) allBlocks - where allBlocks :: (Enum a, Num a, Eq a, Integral a) => [((a,a), Int)] - allBlocks = [((x,y), maxBlockLife - 1) | x <- [0..blockColumns - 1] - , y <- [midColumn - 1, midColumn] - , (even x && y == midColumn) - || (odd x && y == midColumn -1)] - ++ [((x,y), minBlockLife) | x <- [midColumn - 1, midColumn] - , y <- [0, 1, - blockRows - 2, blockRows - 1]] - ++ [((x,y), maxBlockLife) | x <- [0..blockColumns - 1] - , y <- [0, 1, - blockRows - 2, blockRows - 1] - , x /= midColumn - 1, x /= midColumn] + where + allBlocks :: (Enum a, Num a, Eq a, Integral a) => [((a, a), Int)] + allBlocks = [ ((x, y), maxBlockLife - 1) + | x <- [0..blockColumns - 1] + , y <- [midColumn - 1, midColumn] + , (even x && y == midColumn) || (odd x && y == midColumn -1) + ] - blockRows :: Num a => a - blockRows = 8 + ++ [ ((x, y), minBlockLife) + | x <- [midColumn - 1, midColumn] + , y <- [0, 1, blockRows - 2, blockRows - 1] + ] - midRow :: Integral a => a - midRow = blockRows `div` 2 + ++ [ ((x, y), maxBlockLife) + | x <- [0..blockColumns - 1] + , y <- [0, 1, blockRows - 2, blockRows - 1] + , x /= midColumn - 1 + , x /= midColumn + ] - midColumn :: Integral a => a - midColumn = blockColumns `div` 2 + blockRows :: Num a => a + blockRows = 8 + + midRow :: Integral a => a + midRow = blockRows `div` 2 + + midColumn :: Integral a => a + midColumn = blockColumns `div` 2 -- Level 17 -- maxBlockLife == X @@ -557,34 +635,42 @@ blockDescS 16 = map (first adjustPos) allBlocks -- % T T T T -- % O O O O -- % O O O O --- % +-- % -- % XXX XXXX blockDescS 17 = map (first adjustPos) allBlocks - where allBlocks :: (Enum a, Num a, Eq a, Integral a) => [((a,a), Int)] - allBlocks = [((x,y), maxBlockLife - 1) | x <- [0..blockColumns - 1] - , y <- [3, 4] - , odd x] - ++ [((x,y), minBlockLife) | x <- [0..blockColumns - 1] - , y <- [1, 2, - blockRows - 4, blockRows - 3] - , even x ] - ++ [((x,y), maxBlockLife) | x <- [0..blockColumns - 1] - , y <- [0, blockRows - 1] - , y == 0 || x /= midColumn] - blockRows :: Num a => a - blockRows = 9 + where + + allBlocks :: (Enum a, Num a, Eq a, Integral a) => [((a, a), Int)] + allBlocks = [ ((x, y), maxBlockLife - 1) + | x <- [0..blockColumns - 1] + , y <- [3, 4] + , odd x + ] + + ++ [((x, y), minBlockLife) + | x <- [0..blockColumns - 1] + , y <- [1, 2, blockRows - 4, blockRows - 3] + , even x + ] - midColumn :: Integral a => a - midColumn = blockColumns `div` 2 + ++ [ ((x, y), maxBlockLife) + | x <- [0..blockColumns - 1] + , y <- [0, blockRows - 1] + , y == 0 || x /= midColumn + ] + blockRows :: Num a => a + blockRows = 9 + midColumn :: Integral a => a + midColumn = blockColumns `div` 2 blockDescS _ = error "No more levels" -- Dynamic positioning/level size -adjustPos :: Integral a => (a,a) -> (Double, Double) +adjustPos :: Integral a => (a, a) -> (Double, Double) adjustPos = ((adjustHPos *** adjustVPos) . fI2) adjustVPos :: Double -> Double @@ -599,7 +685,8 @@ blockColumns = 1 + round' ( (gameWidth - blockWidth - 2 * leftMargin) / (blockWidth + blockSeparation) ) - where round' = fromIntegral . floor + where + round' = fromIntegral . floor -- * Constants @@ -611,5 +698,5 @@ leftMargin = 25 -- * Auxiliary functions -fI2 :: Integral a => (a,a) -> (Double, Double) -fI2 (x,y) = (fromIntegral x, fromIntegral y) +fI2 :: Integral a => (a, a) -> (Double, Double) +fI2 (x, y) = (fromIntegral x, fromIntegral y) diff --git a/src/Main.hs b/src/Main.hs index 7353267..cb7782a 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -1,11 +1,13 @@ -import Control.Applicative ((<$>)) +-- External imports +import Control.Applicative ((<$>)) import Control.Monad.IfElse -import FRP.Yampa as Yampa +import FRP.Yampa as Yampa -import Game +-- Internal imports import Display -import Input +import Game import Graphics.UI.Extra.SDL +import Input -- TODO: Use MaybeT or ErrorT to report errors main :: IO () @@ -25,6 +27,5 @@ main = do mInput <- senseInput controllerRef return (dtSecs, Just mInput) ) - (\_ e -> render res' e >> return False) - wholeGame - + (\_ (e, c) -> render res' e >> return (controllerQuit c)) + (wholeGame &&& arr id) diff --git a/src/ObjectSF.hs b/src/ObjectSF.hs index 2c0a205..b730fcc 100644 --- a/src/ObjectSF.hs +++ b/src/ObjectSF.hs @@ -1,21 +1,27 @@ --- | Objects as signal functions. +-- | +-- Copyright : (c) Ivan Perez & Henrik Nilsson, 2014. +-- License : See LICENSE file. +-- Maintainer : Ivan Perez -- --- Live objects in the game take user input and the game universe --- and define their state in terms of that. They can remember what --- happened (see Yampa's Arrow combinators, which hide continuations), --- change their behaviour (see switches in Yampa). +-- Objects as signal functions. +-- +-- Live objects in the game take user input and the game universe and define +-- their state in terms of that. They can remember what happened (see Yampa's +-- Arrow combinators, which hide continuations), change their behaviour (see +-- switches in Yampa). -- -- They cannot affect other objects, but they can kill themselves (see --- 'harakiri'). Should you need to spawn new game elements upon --- events, you might want to change 'harakiri' to something more --- general. +-- 'harakiri'). Should you need to spawn new game elements upon events, you +-- might want to change 'harakiri' to something more general. module ObjectSF where +-- External imports import FRP.Yampa -import Objects -import Input +-- Internal imports import Data.IdentityList +import Input +import Objects -- | Objects are defined as transformations that take 'ObjectInput' signals and -- return 'ObjectOutput' signals. @@ -26,13 +32,13 @@ type ObjectSF = SF ObjectInput ObjectOutput -- ('collisions'), and the presence of any pre-existing objects -- ('knownObjects'). -- --- The reason for depending on 'Collisions' is that objects may ``die'' --- when hit. +-- The reason for depending on 'Collisions' is that objects may ``die'' when +-- hit. -- --- The reason for depending on 'Objects' is that objects may choose to --- follow other objects. +-- The reason for depending on 'Objects' is that objects may choose to follow +-- other objects. -- --- TODO: Would it be possible to depend on the specific object sfs internally +-- TODO: Would it be possible to depend on the specific object SFs internally -- and remove the explicit 'knownObjects'? I guess so, so long as it's possible -- to always provide the same input to those SFs that they will have in the -- game: because they are different instances, we need the exact same input to @@ -43,13 +49,12 @@ data ObjectInput = ObjectInput , knownObjects :: Objects } --- | What we can see about each live object at each time. It's a --- snapshot of the object. +-- | What we can see about each live object at each time. It's a snapshot of +-- the object. data ObjectOutput = ObjectOutput { outputObject :: Object -- ^ The object's state (position, shape, etc.). , harakiri :: Event () -- ^ Whether the object has died (killed itself). - } - + } -- | Handy function to create an object that is currently alive. livingObject :: Object -> ObjectOutput @@ -64,4 +69,3 @@ extractObjects = arr (fmap outputObject) -- | A list of object outputs type ObjectOutputs = [ObjectOutput] - diff --git a/src/Objects.hs b/src/Objects.hs index e100934..e605e7f 100644 --- a/src/Objects.hs +++ b/src/Objects.hs @@ -1,13 +1,21 @@ --- | Game objects and collisions. +-- | +-- Copyright : (c) Ivan Perez & Henrik Nilsson, 2014. +-- License : See LICENSE file. +-- Maintainer : Ivan Perez +-- +-- Game objects and collisions. module Objects where -import FRP.Yampa.VectorSpace +-- External imports +import Data.VectorSpace +-- Internal imports (general purpose) import Data.Extra.Num -import Physics.TwoDimensions.Dimensions import Physics.TwoDimensions.Collisions +import Physics.TwoDimensions.Dimensions import Physics.TwoDimensions.Physics +-- Internal imports import Constants -- * Objects @@ -26,9 +34,11 @@ data Object = Object { objectName :: ObjectName , objectHit :: Bool , canCauseCollisions :: Bool , collisionEnergy :: Double - , displacedOnCollision :: Bool -- Theoretically, setting cE == 0 should suffice + , displacedOnCollision :: Bool -- Theoretically, + -- setting cE == 0 + -- should suffice. } - deriving (Show) + deriving (Show) type Objects = [Object] @@ -36,11 +46,11 @@ type Objects = [Object] -- -- TODO: Use a GADT to separate these properties in two types and guarantee a -- proper correspondence in 'Object'. -data ObjectKind = Ball Double -- radius? - | Paddle Size2D - | Block Energy Size2D - | Side Side - deriving (Show,Eq) +data ObjectKind = Ball Double -- radius? + | Paddle Size2D + | Block Energy Size2D + | Side Side + deriving (Show, Eq) type Energy = Int @@ -50,7 +60,7 @@ isBall _ = False isBlock :: ObjectKind -> Bool isBlock Block {} = True -isBlock _ = False +isBlock _ = False isPaddle :: Object -> Bool isPaddle o = case objectKind o of @@ -59,30 +69,33 @@ isPaddle o = case objectKind o of objShape :: Object -> Shape objShape obj = case objectKind obj of - (Ball r) -> Rectangle (p ^-^ (r,r)) (2*r, 2*r) - (Paddle s) -> Rectangle p s - (Block _ s) -> Rectangle p s - (Side s) -> sideToShape p s - where p = objectPos obj - width' = gameWidth - height' = gameHeight - d = collisionErrorMargin - sideToShape p TopSide = Rectangle (p ^-^ (d, d)) (width' + 2*d, d) - sideToShape p LeftSide = Rectangle (p ^-^ (d, d)) (d, height' + 2*d) - sideToShape p RightSide = Rectangle (p ^-^ (0, d)) (d, height' + 2*d) - sideToShape p BottomSide = Rectangle (p ^-^ (d, 0)) (width' + 2*d, d) + (Ball r) -> Rectangle (p ^-^ (r, r)) (2 * r, 2 * r) + (Paddle s) -> Rectangle p s + (Block _ s) -> Rectangle p s + (Side s) -> sideToShape p s + + where + + p = objectPos obj + width' = gameWidth + height' = gameHeight + d = collisionErrorMargin + sideToShape p TopSide = Rectangle (p ^-^ (d, d)) (width' + 2 * d, d) + sideToShape p LeftSide = Rectangle (p ^-^ (d, d)) (d, height' + 2 * d) + sideToShape p RightSide = Rectangle (p ^-^ (0, d)) (d, height' + 2 * d) + sideToShape p BottomSide = Rectangle (p ^-^ (d, 0)) (width' + 2 * d, d) -- * Collisions type Collisions = [Collision] -- | A collision is a list of objects that collided, plus their velocities as -- modified by the collision. --- +-- -- Take into account that the same object could take part in several --- simultaneous collitions, so these velocities should be added (per object). +-- simultaneous collisions, so these velocities should be added (per object). data Collision = Collision - { collisionData :: [(ObjectName, Vel2D)] } -- ObjectId x Velocity - deriving Show + { collisionData :: [(ObjectName, Vel2D)] } -- ObjectId x Velocity + deriving Show -- | Detects a collision between one object and another regardless of -- everything else @@ -101,13 +114,25 @@ collisionSide obj1 obj2 = shapeCollisionSide (objShape obj1) (objShape obj2) collisionResponseObj :: Object -> Object -> Collision collisionResponseObj o1 o2 = - Collision $ - map objectToCollision [(o1, side, o2), (o2, side', o1)] - where side = collisionSide o1 o2 - side' = oppositeSide side - objectReacts o = collisionEnergy o > 0 || displacedOnCollision o - objectToCollision (o,s,o') = (objectName o, correctVel (objectVel o ^+^ (velTrans *^ objectVel o')) (collisionEnergy o) s) - correctVel (vx,vy) e TopSide = (vx, ensurePos (vy * (-e))) - correctVel (vx,vy) e BottomSide = (vx, ensureNeg (vy * (-e))) - correctVel (vx,vy) e LeftSide = (ensureNeg (vx * (-e)),vy) - correctVel (vx,vy) e RightSide = (ensurePos (vx * (-e)),vy) + Collision $ + map objectToCollision [(o1, side, o2), (o2, side', o1)] + + where + + side = collisionSide o1 o2 + side' = oppositeSide side + + objectReacts o = collisionEnergy o > 0 || displacedOnCollision o + + objectToCollision (o, s, o') = + ( objectName o + , correctVel + (objectVel o ^+^ (velTrans *^ objectVel o')) + (collisionEnergy o) + s + ) + + correctVel (vx, vy) e TopSide = (vx, ensurePos (vy * (-e))) + correctVel (vx, vy) e BottomSide = (vx, ensureNeg (vy * (-e))) + correctVel (vx, vy) e LeftSide = (ensureNeg (vx * (-e)), vy) + correctVel (vx, vy) e RightSide = (ensurePos (vx * (-e)), vy) diff --git a/src/Physics/TwoDimensions/Collisions.hs b/src/Physics/TwoDimensions/Collisions.hs index c33b061..c741094 100644 --- a/src/Physics/TwoDimensions/Collisions.hs +++ b/src/Physics/TwoDimensions/Collisions.hs @@ -1,39 +1,46 @@ --- | A trivial collision subsystem. +-- | +-- Copyright : (c) Ivan Perez & Henrik Nilsson, 2014. +-- License : See LICENSE file. +-- Maintainer : Ivan Perez -- --- Based on the physics module, it determines the side of collision --- between shapes. +-- A trivial collision subsystem. +-- +-- Based on the physics module, it determines the side of collision between +-- shapes. module Physics.TwoDimensions.Collisions where -import FRP.Yampa.VectorSpace as Yampa +-- External imports +import Data.VectorSpace + +-- Internal imports import Physics.TwoDimensions.Physics -- * Collision sides -- | Collision side of a rectangle data Side = TopSide | BottomSide | LeftSide | RightSide - deriving (Eq,Show) + deriving (Eq, Show) -- | Opposite side during a collision. -- --- If A collides with B, the collision sides on --- A and B are always opposite. +-- If A collides with B, the collision sides on A and B are always opposite. oppositeSide :: Side -> Side oppositeSide TopSide = BottomSide oppositeSide BottomSide = TopSide oppositeSide LeftSide = RightSide oppositeSide RightSide = LeftSide --- | Calculates the collision side of a shape --- that collides against another. +-- | Calculates the collision side of a shape that collides against another. -- -- PRE: the shapes do collide. Use 'overlapShape' to check. shapeCollisionSide :: Shape -> Shape -> Side shapeCollisionSide (Rectangle p1 s1) (Rectangle p2 s2) - | wy > hx && wy > -hx = TopSide - | wy > hx = LeftSide - | wy > -hx = RightSide - | otherwise = BottomSide - where (dx,dy) = (p1 ^+^ (0.5 *^ s1)) ^-^ (p2 ^+^ (0.5 *^ s2)) -- p1 ^-^ p2 - (w,h) = 0.5 *^ (s1 ^+^ s2) - wy = w * dy - hx = h * dx + | wy > hx && wy > -hx = TopSide + | wy > hx = LeftSide + | wy > -hx = RightSide + | otherwise = BottomSide + where + (dx, dy) = (p1 ^+^ (0.5 *^ s1)) ^-^ (p2 ^+^ (0.5 *^ s2)) -- p1 ^-^ p2 + (w, h) = 0.5 *^ (s1 ^+^ s2) + wy = w * dy + hx = h * dx diff --git a/src/Physics/TwoDimensions/Dimensions.hs b/src/Physics/TwoDimensions/Dimensions.hs index d4f985a..867f680 100644 --- a/src/Physics/TwoDimensions/Dimensions.hs +++ b/src/Physics/TwoDimensions/Dimensions.hs @@ -1,9 +1,17 @@ --- | Physical dimensions used all over the game. They are just type synonyms, --- but it's best to use meaningful names to make our type signatures more +-- | +-- Copyright : (c) Ivan Perez & Henrik Nilsson, 2014. +-- License : See LICENSE file. +-- Maintainer : Ivan Perez +-- +-- Physical dimensions used all over the game. They are just type synonyms, but +-- it's best to use meaningful names to make our type signatures more -- meaningful. module Physics.TwoDimensions.Dimensions where type Size2D = (Double, Double) -type Pos2D = (Double, Double) -type Vel2D = (Double, Double) -type Acc2D = (Double, Double) + +type Pos2D = (Double, Double) + +type Vel2D = (Double, Double) + +type Acc2D = (Double, Double) diff --git a/src/Physics/TwoDimensions/Physics.hs b/src/Physics/TwoDimensions/Physics.hs index d567f6c..bd69642 100644 --- a/src/Physics/TwoDimensions/Physics.hs +++ b/src/Physics/TwoDimensions/Physics.hs @@ -1,23 +1,31 @@ --- | A very simple physics subsytem. It currently detects shape --- overlaps only, the actual physics movement is carried out --- in Yampa itself, as it is very simple using integrals and --- derivatives. +-- | +-- Copyright : (c) Ivan Perez & Henrik Nilsson, 2014. +-- License : See LICENSE file. +-- Maintainer : Ivan Perez +-- +-- A very simple physics subsystem. It currently detects shape overlaps only, +-- the actual physics movement is carried out in Yampa itself, as it is very +-- simple using integrals and derivatives. module Physics.TwoDimensions.Physics where -import FRP.Yampa.VectorSpace as Yampa +-- External imports +import Data.VectorSpace + +-- Internal imports import Physics.TwoDimensions.Dimensions -data Shape = Rectangle Pos2D Size2D -- A corner and the whole size - -- Circle Pos2D Float -- Position and radius -- NOT FOR NOW - -- SemiPlane Pos2D Float -- Position and angle of plane normal -- NFN +data Shape = Rectangle Pos2D Size2D -- A corner and the whole size + -- Circle Pos2D Float -- Position and radius -- NOT FOR NOW + -- SemiPlane Pos2D Float -- Position, angle of plane normal NFN -- | Detects if two shapes overlap. -- --- Rectangles: overlap if projections on both axis overlap, --- which happens if x distance between centers is less than the sum --- of half the widths, and the analogous for y and the heights. +-- Rectangles: overlap if projections on both axis overlap, which happens if x +-- distance between centers is less than the sum of half the widths, and the +-- analogous for y and the heights. overlapShape :: Shape -> Shape -> Bool overlapShape (Rectangle p1 s1) (Rectangle p2 s2) = abs dx <= w && abs dy <= h - where (dx,dy) = (p1 ^+^ (0.5 *^ s1)) ^-^ (p2 ^+^ (0.5 *^ s2)) - (w,h) = 0.5 *^ (s1 ^+^ s2) + where + (dx, dy) = (p1 ^+^ (0.5 *^ s1)) ^-^ (p2 ^+^ (0.5 *^ s2)) + (w, h) = 0.5 *^ (s1 ^+^ s2) diff --git a/src/Resources.hs b/src/Resources.hs index b2eeff0..7a76348 100644 --- a/src/Resources.hs +++ b/src/Resources.hs @@ -1,16 +1,25 @@ +-- | +-- Copyright : (c) Ivan Perez & Henrik Nilsson, 2014. +-- License : See LICENSE file. +-- Maintainer : Ivan Perez +-- +-- Resource specifications. module Resources where data ResourceSpec = ResourceSpec - { fonts :: [FontResource] - , images :: [ImageResource] - , music :: [MusicResource] - , audio :: [AudioResource] - } + { fonts :: [FontResource] + , images :: [ImageResource] + , music :: [MusicResource] + , audio :: [AudioResource] + } + +type FontResource = Resource -type FontResource = Resource type ImageResource = Resource + type MusicResource = Resource + type AudioResource = Resource newtype Resource = Resource { _resourceFP :: FilePath } - deriving Eq + deriving Eq