From 410f86eb9c94957eda9c02de93dcd376ef90fe75 Mon Sep 17 00:00:00 2001 From: Zack Corr Date: Thu, 8 Sep 2016 22:51:17 +1000 Subject: [PATCH] Separating out engine functions, adding extra functions to the Engine typeclass but makes it more consistent (gives less space for the engines to do stuff they shouldn't). Additionally, cleaning the code and working on documentation. --- .editorconfig | 3 + LICENSE | 2 +- README.md | 100 +++++++++++---------------------- src/Helm.hs | 61 +++++++++++++++++++- src/Helm/Cmd.hs | 15 ++++- src/Helm/Engine.hs | 4 +- src/Helm/Engine/SDL.hs | 123 ++++++++++++----------------------------- src/Helm/Graphics.hs | 4 +- src/Helm/Keyboard.hs | 20 +++++-- src/Helm/Mouse.hs | 38 +++++++++---- src/Helm/Sub.hs | 10 +++- src/Helm/Time.hs | 40 +++++++++----- src/Helm/Window.hs | 14 ++++- 13 files changed, 241 insertions(+), 193 deletions(-) diff --git a/.editorconfig b/.editorconfig index 4a7ea30..923f11f 100644 --- a/.editorconfig +++ b/.editorconfig @@ -10,3 +10,6 @@ insert_final_newline = true [*.md] trim_trailing_whitespace = false + +[*.hs] +max_line_length = 120 \ No newline at end of file diff --git a/LICENSE b/LICENSE index bb37a9c..c2d8bf5 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (C) 2013-2014, Zack Corr +Copyright (C) 2013-2016, Zack Corr Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to diff --git a/README.md b/README.md index 435e28c..5b097d0 100644 --- a/README.md +++ b/README.md @@ -12,16 +12,19 @@ the [Elerea FRP framework](https://github.com/cobbpg/elerea). Helm was originally inspired by the [Elm programming language](http://elm-lang.org). In Helm, every piece of input that can be gathered from a user (or the operating system) -is hidden behind a subscription. For those unfamiliar with FRP, signals are essentially -a value that changes over time. This sort of architecture used for a game allows for pretty -simplistic (and in my opinion, artistic) code. +is contained in a subscription, which is essentially +as a collection of input events changing over time. Think of it this way - when you hold down +the w and s keys, two keyboard events are being captured at every moment. In this case, a subscription to keyboard presses +would then yield you with a collection of two events at every game tick. -Documentation of the Helm API is available on [Hackage](http://hackage.haskell.org/package/helm). -There is currently a heavily work-in-progress guide on [Helm's website](http://helm-engine.org/guide), -which is a resource aiming to give thorough explanations of the way Helm and its API work through examples. -You can [ask on the mailing list](https://groups.google.com/d/forum/helm-dev) if you're having any trouble -with using the engine for games or working on the engine itself, or if you just want to chit-chat about -Helm. +Helm provides a structure similar to MVC (model-view-controller). +There is a model (which represents the state of your game), +a view of the current model (i.e. what's actually shown on the screen) and a controller that folds the model +forward based off of input actions (which are mapped from the subscription events). + +This presents a powerful paradigm shift for game development. Instead of writing event listeners, +Helm treats input events as first-class citizens of the type system, and the actual interaction between +the game state and input events becomes immediately clearer. ## Features @@ -51,67 +54,37 @@ Helm. * `Helm.Utilities` contains an assortment of useful functions, * `Helm.Window` contains signals for working with the game window state. -## Example - -The simplest example of a Helm game that doesn't require any input from the user is the following: - -```haskell -import Helm -import qualified Helm.Window as Window - -render :: (Int, Int) -> Element -render (w, h) = collage w h [move (100, 100) $ filled red $ square 64] - -main :: IO () -main = run defaultConfig $ render <~ Window.dimensions -``` - -It renders a red square at the position `(100, 100)` with a side length of `64`. - -The next example is the barebones of a game that depends on input. It shows how to create -an accumulated state that depends on the values sampled from signals (e.g. mouse input). -You should see a white square on the screen and pressing the arrow keys allows you to move it. - -```haskell -import Helm -import qualified Helm.Keyboard as Keyboard -import qualified Helm.Window as Window - -data State = State { mx :: Double, my :: Double } - -step :: (Int, Int) -> State -> State -step (dx, dy) state = state { mx = (10 * (realToFrac dx)) + mx state, - my = (10 * (realToFrac dy)) + my state } - -render :: (Int, Int) -> State -> Element -render (w, h) (State { mx = mx, my = my }) = - centeredCollage w h [move (mx, my) $ filled white $ square 100] - -main :: IO () -main = run defaultConfig $ render <~ Window.dimensions ~~ stepper - where - state = State { mx = 0, my = 0 } - stepper = foldp step state Keyboard.arrows -``` - ## Installing and Building -Helm requires GHC 7.6 (Elerea doesn't work with older versions due to a compiler bug). -To install the latest (stable) version from the Hackage repository, use: +Before you can install Helm, you'll to follow the +[Gtk2Hs installation guide](https://wiki.haskell.org/Gtk2Hs/Installation) +(which is required for the Haskell Cairo bindings). Additionally, Helm +requires a GHC version of 7.6 or higher. + +To install the latest stable version from the Hackage repository, use: ``` cabal install helm ``` -Alternatively to get the latest development version, you can clone this repository and then run: +Alternatively to get the latest development version run: ``` +git clone git://github.com/z0w0/helm.git +cd helm cabal install ``` -You may need to jump a few hoops to install the Cairo bindings (which are a dependency), -which unfortunately is out of my hands. Read the [installing guide](http://helm-engine.org/guide/installing/) -on the website for a few platform-specific instructions. +## Getting Started + +Check out the `examples` directory for some examples; the `hello` example is a particularly good start. +Unfortunately, there's little to no example games yet, so if you end up making something cool and lightweight +that you'd think would be a good example, feel free to open a pull request! + +## Documentation + +API documentation for the latest stable version of Helm is available on [Hackage](http://hackage.haskell.org/package/helm). +Alternatively, if you've cloned this repo, you can build the documentation manually using Haddock. ## License @@ -124,12 +97,5 @@ Helm would benefit from either of the following contributions: 1. Try out the engine, reporting any issues or suggestions you have. 2. Look through the source, get a feel for the code and then contribute some features or fixes. If you plan on contributing - code please submit a pull request and follow the formatting - styles set out in the current code: 2 space indents, documentation - on every top-level function, favouring monad operators over - do blocks when there is a logical flow of data, spaces between operators - and after commas, etc. Please also confirm that the code passes under - HLint. - -There are a number of issues [tagged with the bounty tag](https://github.com/switchface/helm/issues?labels=bounty&state=open), -meaning they have associated bounties on [Bountysource](https://www.bountysource.com/trackers/290443-helm). + code, please follow [Johan Tibell's Haskell style guide](https://github.com/tibbe/haskell-style-guide/blob/master/haskell-style.md) + - with one exception allowed - line length may be up to 120 characters (wide screens for life). diff --git a/src/Helm.hs b/src/Helm.hs index 4b2fa7f..b73803f 100644 --- a/src/Helm.hs +++ b/src/Helm.hs @@ -13,5 +13,64 @@ module Helm ,loadSound) where -import Helm.Engine (Cmd(..), Sub(..), GameConfig(..), Engine(run, loadImage, loadSound)) +import Control.Exception (finally) +import Control.Monad (foldM, void) +import Control.Monad.Trans.State.Lazy (evalStateT) +import FRP.Elerea.Param (start, embed) + +import Helm.Engine (Cmd(..), Sub(..), GameConfig(..), Engine(..)) import Helm.Graphics + +{-| A data structure describing a game's state (that is running under an engine). -} +data Game e m a = Game + { gameConfig :: GameConfig e m a + , gameModel :: m + , actionSmp :: e -> IO [a] + } + +prepare :: Engine e => e -> GameConfig e m a -> IO (Game e m a) +prepare engine config = do + {- The call to 'embed' here is a little bit hacky, but seems necessary + to get this working. This is because 'start' actually computes the signal + gen passed to it, and all of our signal gens try to fetch + the 'input' value within the top layer signal gen (rather than in the + contained signal). But we haven't sampled with the input value yet, so it'll + be undefined unless we 'embed'. -} + smp <- start $ embed (return engine) gen + + return Game + { gameConfig = config + , gameModel = fst initialFn + , actionSmp = smp + } + + where + GameConfig { initialFn, subscriptionsFn = Sub gen } = config + +run :: Engine e => e -> GameConfig e m a -> IO () +run engine config = void $ (prepare engine config >>= step engine) `finally` cleanup engine + +step :: Engine e => e -> Game e m a -> IO () +step engine game = do + mayhaps <- sinkEvents engine + + case mayhaps of + Nothing -> return () + + Just sunkEngine -> do + actions <- actionSmp sunkEngine + model <- foldM (stepModel sunkEngine game) gameModel actions + + render sunkEngine $ viewFn model + step sunkEngine $ game { gameModel = model } + + where + Game { actionSmp, gameModel, gameConfig = GameConfig { viewFn } } = game + +stepModel :: Engine e => e -> Game e m a -> m -> a -> IO m +stepModel engine game model action = + evalStateT monad engine >>= foldM (stepModel engine game) upModel + + where + Game { gameConfig = GameConfig { updateFn } } = game + (upModel, Cmd monad) = updateFn model action diff --git a/src/Helm/Cmd.hs b/src/Helm/Cmd.hs index 744022e..a5d5dd7 100644 --- a/src/Helm/Cmd.hs +++ b/src/Helm/Cmd.hs @@ -12,16 +12,27 @@ import Control.Monad.Trans.Class (lift) import Helm.Engine (Engine, Cmd(..)) -batch :: Engine e => [Cmd e a] -> Cmd e a +-- | Combined a list of mapped commands into a single one. +batch :: + Engine e + => [Cmd e a] -- ^ The list of mapped commands. + -> Cmd e a -- ^ The mapped commands accumulated. batch cmds = Cmd $ do lists <- mapM (\(Cmd m) -> m) cmds return $ concat lists +-- | A mapped command that does nothing. none :: Engine e => Cmd e a none = Cmd $ return [] -execute :: Engine e => IO a -> (a -> b) -> Cmd e b +-- | Execute an IO monad and then map it to a game action. +-- This can be used as a kind of 'liftIO'. +execute :: + Engine e + => IO b -- ^ The IO monad to execute. + -> (b -> a) -- ^ The function to map the monad result to an action. + -> Cmd e a -- ^ The mapped command. execute monad f = Cmd $ do result <- f <$> lift monad diff --git a/src/Helm/Engine.hs b/src/Helm/Engine.hs index 3f2b66c..4568dcb 100644 --- a/src/Helm/Engine.hs +++ b/src/Helm/Engine.hs @@ -20,7 +20,9 @@ import Helm.Graphics (Graphics) class Engine e where loadImage :: e -> IO Image loadSound :: e -> IO Sound - run :: e -> GameConfig e m a -> IO () + render :: e -> Graphics -> IO () + sinkEvents :: e -> IO (Maybe e) + cleanup :: e -> IO () windowSize :: e -> IO (V2 Int) runningTime :: e -> IO Double diff --git a/src/Helm/Engine/SDL.hs b/src/Helm/Engine/SDL.hs index ca4f445..7589c59 100644 --- a/src/Helm/Engine/SDL.hs +++ b/src/Helm/Engine/SDL.hs @@ -11,9 +11,6 @@ module Helm.Engine.SDL ,startupWith) where -import Control.Exception (finally) -import Control.Monad (foldM, void) -import Control.Monad.Trans.State.Lazy (evalStateT) import Data.Int (Int32) import qualified Data.Text as T import Data.Word (Word32) @@ -31,8 +28,7 @@ import SDL.Video (WindowConfig(..)) import qualified SDL.Video.Renderer as Renderer import Helm.Asset -import Helm.Engine (GameConfig(..), Cmd(..), Sub(..), - Engine(..), Key, MouseButton) +import Helm.Engine (Engine(..), Key, MouseButton) import Helm.Graphics (Graphics(..)) import Helm.Graphics2D (Element) import Helm.Engine.SDL.Keyboard (mapKey) @@ -52,6 +48,7 @@ data SDLEngine = SDLEngine { window :: Video.Window , renderer :: Video.Renderer , engineConfig :: SDLEngineConfig + , lastMousePress :: Maybe (Word32, V2 Int32) , mouseMoveEventSignal :: SignalGen SDLEngine (Signal [V2 Int]) , mouseMoveEventSink :: V2 Int -> IO () @@ -73,19 +70,26 @@ data SDLEngine = SDLEngine , windowResizeEventSink :: V2 Int -> IO () } -{-| A data structure describing a game's state (that is running under an engine). -} -data SDLGame m a = SDLGame - { gameConfig :: GameConfig SDLEngine m a - , gameModel :: m - , running :: Bool - , actionSmp :: SDLEngine -> IO [a] - , lastMousePress :: Maybe (Word32, V2 Int32) - } - instance Engine SDLEngine where loadImage _ = return $ Image () loadSound _ = return $ Sound () + render engine (Graphics2D element) = render2d engine element + cleanup _ = Init.quit + + sinkEvents engine = do + mayhaps <- Event.pumpEvents >> Event.pollEvent + + case mayhaps of + -- Handle the quit event exclusively first to simplify our code + Just Event.Event { eventPayload = Event.QuitEvent } -> + return Nothing + + Just Event.Event { .. } -> + sinkEvent engine eventPayload >>= sinkEvents + + Nothing -> return $ Just engine + mouseMoveSignal = mouseMoveEventSignal mouseDownSignal = mouseDownEventSignal mouseUpSignal = mouseUpEventSignal @@ -101,9 +105,6 @@ instance Engine SDLEngine where windowSize SDLEngine { window } = fmap (fmap fromIntegral) . SDL.get $ Video.windowSize window - run engine config = - void $ (prepare engine config >>= step engine) `finally` Init.quit - {-| Creates the default configuration for the engine. You should change the values where necessary. -} defaultConfig :: SDLEngineConfig @@ -142,6 +143,7 @@ startupWith config@SDLEngineConfig{..} = do { window = window , renderer = renderer , engineConfig = config + , lastMousePress = Nothing , mouseMoveEventSignal = fst mouseMoveEvent , mouseMoveEventSink = snd mouseMoveEvent @@ -173,47 +175,6 @@ startupWith config@SDLEngineConfig{..} = do , windowResizable = windowIsResizable } -step :: SDLEngine -> SDLGame m a -> IO (SDLGame m a) -step engine game@SDLGame{actionSmp,gameModel,gameConfig = GameConfig{viewFn}} = do - sunkGame <- sinkEvents engine game - - if running sunkGame - then do - actions <- actionSmp engine - model <- foldM (stepModel engine game) gameModel actions - - render engine $ viewFn model - step engine $ sunkGame { gameModel = model } - else return sunkGame - -stepModel :: SDLEngine -> SDLGame m a -> m -> a -> IO m -stepModel engine game@SDLGame { gameConfig = GameConfig { updateFn } } model action = - evalStateT monad engine >>= foldM (stepModel engine game) model - - where - (model, Cmd monad) = updateFn model action - -prepare :: SDLEngine -> GameConfig SDLEngine m a -> IO (SDLGame m a) -prepare engine config@GameConfig { initialFn, subscriptionsFn = Sub gen } = do - {- The call to 'embed' here is a little bit hacky, but seems necessary - to get this working. This is because 'start' actually computes the signal - gen passed to it, and all of our signal gens try to fetch - the 'input' value within the top layer signal gen (rather than in the - contained signal). But we haven't sampled with the input value yet, so it'll - be undefined unless we 'embed'. -} - smp <- start $ embed (return engine) gen - - return SDLGame - { gameConfig = config - , gameModel = fst initialFn - , running = True - , actionSmp = smp - , lastMousePress = Nothing - } - -render :: SDLEngine -> Graphics -> IO () -render engine (Graphics2D element) = render2d engine element - render2d :: SDLEngine -> Element -> IO () render2d SDLEngine{window,renderer} element = do dims <- SDL.get $ Video.windowSize window @@ -229,60 +190,46 @@ render2d SDLEngine{window,renderer} element = do mode = Renderer.ARGB8888 access = Renderer.TextureAccessStreaming -sinkEvents :: SDLEngine -> SDLGame m a -> IO (SDLGame m a) -sinkEvents engine game = do - mayhaps <- Event.pumpEvents >> Event.pollEvent - - case mayhaps of - -- Handle the quit event exclusively first to simplify our code - Just Event.Event { eventPayload = Event.QuitEvent } -> - return game { running = False } - - Just Event.Event { .. } -> - sinkEvent engine game eventPayload >>= sinkEvents engine - - Nothing -> return game - -depoint :: Point f a -> (f a) +depoint :: Point f a -> f a depoint (P x) = x -sinkEvent :: SDLEngine -> SDLGame m a -> Event.EventPayload -> IO (SDLGame m a) -sinkEvent engine game (Event.WindowResizedEvent Event.WindowResizedEventData { .. }) = do +sinkEvent :: SDLEngine -> Event.EventPayload -> IO SDLEngine +sinkEvent engine (Event.WindowResizedEvent Event.WindowResizedEventData { .. }) = do windowResizeEventSink engine $ fromIntegral <$> windowResizedEventSize - return game + return engine -sinkEvent engine game (Event.MouseMotionEvent Event.MouseMotionEventData { .. }) = do +sinkEvent engine (Event.MouseMotionEvent Event.MouseMotionEventData { .. }) = do mouseMoveEventSink engine $ fromIntegral <$> depoint mouseMotionEventPos - return game + return engine -sinkEvent engine game (Event.KeyboardEvent Event.KeyboardEventData { .. }) = do +sinkEvent engine (Event.KeyboardEvent Event.KeyboardEventData { .. }) = case keyboardEventKeyMotion of Event.Pressed -> do keyboardDownEventSink engine key if keyboardEventRepeat - then keyboardPressEventSink engine key >> return game - else return game + then keyboardPressEventSink engine key >> return engine + else return engine Event.Released -> do keyboardUpEventSink engine key keyboardPressEventSink engine key - return game + return engine where Keysym { .. } = keyboardEventKeysym key = mapKey keysymKeycode -sinkEvent engine game (Event.MouseButtonEvent Event.MouseButtonEventData { .. }) = do +sinkEvent engine (Event.MouseButtonEvent Event.MouseButtonEventData { .. }) = case mouseButtonEventMotion of Event.Pressed -> do ticks <- Time.ticks mouseDownEventSink engine tup - return game { lastMousePress = Just (ticks, pos) } + return engine { lastMousePress = Just (ticks, pos) } Event.Released -> do mouseUpEventSink engine tup @@ -293,7 +240,7 @@ sinkEvent engine game (Event.MouseButtonEvent Event.MouseButtonEventData { .. }) event being in a very close proximity to a previous mouse down event. We manually calculate whether this was a click or not. -} case lastMousePress of - Just (lastTicks, (V2 lastX lastY)) -> do + Just (lastTicks, V2 lastX lastY) -> do ticks <- Time.ticks -- Check that it's a expected amount of time for a click and that the mouse has basically stayed in place @@ -303,13 +250,13 @@ sinkEvent engine game (Event.MouseButtonEvent Event.MouseButtonEventData { .. }) Nothing -> return () - return game + return engine where - SDLGame { lastMousePress } = game + SDLEngine { lastMousePress } = engine clickMs = 500 -- How long between mouse down/up to recognise clicks clickRadius = 1 -- The pixel radius to be considered a click. pos@(V2 x y) = depoint mouseButtonEventPos tup = (mapMouseButton mouseButtonEventButton, fromIntegral <$> pos) -sinkEvent _ game _ = return game +sinkEvent engine _ = return engine diff --git a/src/Helm/Graphics.hs b/src/Helm/Graphics.hs index 0139db4..3c0be7a 100644 --- a/src/Helm/Graphics.hs +++ b/src/Helm/Graphics.hs @@ -1,4 +1,4 @@ -{-| Contains the graphics type. -} +-- | Contains the graphics type. module Helm.Graphics ( -- * Types Graphics(..) @@ -6,4 +6,6 @@ module Helm.Graphics ( import Helm.Graphics2D (Element) +-- The graphics type contains any form of structure that +-- produces visual graphics to the screen, i.e. either 2D or 3D elements. data Graphics = Graphics2D Element diff --git a/src/Helm/Keyboard.hs b/src/Helm/Keyboard.hs index 59c2fbd..31d3669 100644 --- a/src/Helm/Keyboard.hs +++ b/src/Helm/Keyboard.hs @@ -1,4 +1,4 @@ -{-| Contains subscriptions to events from the keyboard. -} +-- | Contains subscriptions to events from the keyboard. module Helm.Keyboard ( -- * Types @@ -13,19 +13,31 @@ import FRP.Elerea.Param (input, snapshot) import Helm.Engine (Engine(..), Sub(..), Key(..)) -presses :: Engine e => (Key -> a) -> Sub e a +-- | Subscribe to keyboard press events and map to a game action. +-- A key press event is produced whenever a key is either released +-- or continously held down. +presses :: + Engine e + => (Key -> a) -- ^ The function to map the key pressed to an action. + -> Sub e a -- ^ The mapped subscription. presses f = Sub $ do engine <- input >>= snapshot fmap (fmap f) <$> keyboardPressSignal engine -downs :: Engine e => (Key -> a) -> Sub e a +-- | Subscribe to keyboard down events and map to a game action. +downs :: Engine e + => (Key -> a) -- ^ The function to map the key held down to an action. + -> Sub e a -- ^ The mapped subscription. downs f = Sub $ do engine <- input >>= snapshot fmap (fmap f) <$> keyboardDownSignal engine -ups :: Engine e => (Key -> a) -> Sub e a +-- | Subscribe to keyboard up events and map to a game action. +ups :: Engine e + => (Key -> a) -- ^ The function to map the key released to an action. + -> Sub e a -- ^ The mapped subscription. ups f = Sub $ do engine <- input >>= snapshot diff --git a/src/Helm/Mouse.hs b/src/Helm/Mouse.hs index 92b663d..f846fd7 100644 --- a/src/Helm/Mouse.hs +++ b/src/Helm/Mouse.hs @@ -11,30 +11,48 @@ module Helm.Mouse ) where import FRP.Elerea.Param (input, snapshot) -import Linear.V2 (V2(V2)) +import Linear.V2 (V2) import Helm.Engine (Sub(..), Engine(..), MouseButton(..)) -moves :: Engine e => (V2 Int -> a) -> Sub e a +-- | Subscribe to mouse movement events and map to a game action. +moves :: + Engine e + => (V2 Int -> a) -- ^ The function to map a mouse position to an action. + -> Sub e a -- ^ The mapped subscription. moves f = Sub $ do engine <- input >>= snapshot fmap (fmap f) <$> mouseMoveSignal engine -clicks :: Engine e => (MouseButton -> V2 Int -> a) -> Sub e a -clicks _ = Sub $ do +-- | Subscribe to mouse click events and map to a game action. +-- This subscription is for all mouse buttons - you'll need to +-- match over a mouse button if you want to capture a specific one. +clicks :: + Engine e + => (MouseButton -> V2 Int -> a) -- ^ The function to map a mouse button and position to an action. + -> Sub e a -- ^ The mapped subscription. +clicks f = Sub $ do engine <- input >>= snapshot - fmap (fmap (\(b, p) -> f b p)) <$> mouseClickSignal engine + fmap (fmap (uncurry f)) <$> mouseClickSignal engine -downs :: Engine e => (MouseButton -> V2 Int -> a) -> Sub e a -downs _ = Sub $ do +-- | Subscribe to mouse button down events and map to a game action. +downs :: + Engine e + => (MouseButton -> V2 Int -> a) -- ^ The function to map a mouse button and position to an action. + -> Sub e a -- ^ The mapped subscription. +downs f = Sub $ do engine <- input >>= snapshot - fmap (fmap (\(b, p) -> f b p)) <$> mouseDownSignal engine + fmap (fmap (uncurry f)) <$> mouseDownSignal engine -ups :: Engine e => (MouseButton -> V2 Int -> a) -> Sub e a +-- | Subscribe to mouse button up events and map to a game action. +ups :: + Engine e + => (MouseButton -> V2 Int -> a) -- ^ The function to map a mouse button and position to an action. + -> Sub e a -- ^ The mapped subscription. ups f = Sub $ do engine <- input >>= snapshot - fmap (fmap (\(b, p) -> f b p)) <$> mouseUpSignal engine + fmap (fmap (uncurry f)) <$> mouseUpSignal engine diff --git a/src/Helm/Sub.hs b/src/Helm/Sub.hs index d50db2d..4ce0423 100644 --- a/src/Helm/Sub.hs +++ b/src/Helm/Sub.hs @@ -10,7 +10,14 @@ module Helm.Sub import Helm.Engine (Engine, Sub(..)) -batch :: Engine e => [Sub e a] -> Sub e a +-- | Combine a list of mapped subscriptions into a single one. +-- This is allows for subscriptions to multiple input events to be +-- combined into one mapped subscription that encompasses all the actions +-- mapped from events. +batch :: + Engine e + => [Sub e a] -- ^ The list of mapped subscriptions. + -> Sub e a -- ^ The mapped subscriptions accumulated. batch subs = Sub $ do signals <- mapM (\(Sub gen) -> gen) subs @@ -19,5 +26,6 @@ batch subs = Sub $ do return $ concat lists +-- | A mapped subscription that does nothing. none :: Engine e => Sub e a none = Sub . return $ return [] diff --git a/src/Helm/Time.hs b/src/Helm/Time.hs index 9164382..dfeebe6 100644 --- a/src/Helm/Time.hs +++ b/src/Helm/Time.hs @@ -1,5 +1,5 @@ -{-| Contains functions for composing units of time and - subscriptions to events from the game clock. -} +-- | Contains functions for composing units of time and +-- subscriptions to events from the game clock. module Helm.Time ( -- * Types @@ -24,48 +24,60 @@ import Control.Monad.IO.Class (liftIO) import Helm.Engine (Cmd(..), Sub(..), Engine(..)) -{-| A type describing an amount of time in an arbitary unit. Use the time - composing/converting functions to manipulate time values. -} +-- | A type describing an amount of time in an arbitary unit. +-- This type can then be composed with the relevant utility functions. type Time = Double -{-| A time value representing one millisecond. -} +-- | A time value representing one millisecond. millisecond :: Time millisecond = 1 -{-| A time value representing one second. -} +-- | A time value representing one second. second :: Time second = 1000 -{-| A time value representing one minute. -} +-- | A time value representing one minute. minute :: Time minute = 60000 -{-| A time value representing one hour. -} +-- | A time value representing one hour. hour :: Time hour = 3600000 -{-| Converts a time value to a fractional value, in milliseconds. -} +-- | Converts a time value to a fractional value, in milliseconds. inMilliseconds :: Time -> Double inMilliseconds n = n -{-| Converts a time value to a fractional value, in seconds. -} +-- | Converts a time value to a fractional value, in seconds. inSeconds :: Time -> Double inSeconds n = n / second -{-| Converts a time value to a fractional value, in minutes. -} +-- | Converts a time value to a fractional value, in minutes. inMinutes :: Time -> Double inMinutes n = n / minute -{-| Converts a time value to a fractional value, in hours. -} +-- | Converts a time value to a fractional value, in hours. inHours :: Time -> Double inHours n = n / hour -now :: Engine e => (Time -> a) -> Cmd e a +-- | Map the running time of the engine to a game action. +-- Note that this is not the current clock time but rather the engine time, +-- i.e. when the engine first starts running, the applied value will be zero. +now :: + Engine e + => (Time -> a) -- ^ The function to map the running time to an action. + -> Cmd e a -- ^ The mapped command. now f = Cmd $ do engine <- get ticks <- liftIO $ f <$> runningTime engine return [ticks] -every :: Engine e => Time -> (Time -> a) -> Sub e a +-- | Subscribe to the running time of the engine and map to a game action, +-- producing events at a provided interval. +every :: + Engine e + => Time -- ^ The interval of time to produce events at. + -> (Time -> a) -- ^ The function to map the running time to an action. + -> Sub e a -- ^ The mapped subscription. every _ _ = Sub $ return $ return [] diff --git a/src/Helm/Window.hs b/src/Helm/Window.hs index a471b77..d7a9931 100644 --- a/src/Helm/Window.hs +++ b/src/Helm/Window.hs @@ -1,4 +1,4 @@ -{-| Contains signals that sample input from the game window. -} +-- | Contains signals that sample input from the game window. module Helm.Window ( -- * Commands @@ -15,14 +15,22 @@ import Linear.V2 (V2) import Helm.Engine (Engine(..), Cmd(..), Sub(..)) -size :: Engine e => (V2 Int -> a) -> Cmd e a +-- | Map the game window size to a game action. +size :: + Engine e + => (V2 Int -> a) -- ^ The function to map the window size to an action. + -> Cmd e a -- ^ The mapped command. size f = Cmd $ do engine <- get sized <- liftIO $ f <$> windowSize engine return [sized] -resizes :: Engine e => (V2 Int -> a) -> Sub e a +-- | Subscribe to the resize events from the game window and map to a game action. +resizes :: + Engine e + => (V2 Int -> a) -- ^ The function to map the changed window size to an action. + -> Sub e a -- ^ The mapped subscription. resizes f = Sub $ do engine <- input >>= snapshot