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