1
1
mirror of https://github.com/z0w0/helm.git synced 2024-07-14 15:40:32 +03:00

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.

This commit is contained in:
Zack Corr 2016-09-08 22:51:17 +10:00
parent 1b1af4bc1f
commit 410f86eb9c
13 changed files with 241 additions and 193 deletions

View File

@ -10,3 +10,6 @@ insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
[*.hs]
max_line_length = 120

View File

@ -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

100
README.md
View File

@ -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).

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 []

View File

@ -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 []

View File

@ -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