packages feed

dyna-gloss-0.1.0.0: src/Dyna/Gloss/Run.hs

-- | Run the game application.
module Dyna.Gloss.Run(
  -- * App execution
  Run,
  Spec(..),
  defSpec,
  runApp,
  -- * IO interface
  mouse,
  mouseV,
  isDrag,
  drag,
  dragV,
  mouseA,
  mouseRight,
  mouseLeft,
  mouseWheel,
  Click(..),
  getClicks,
  getFrames,
  getResize,
  keyUp, keyDown,
  charUp, charDown,
  -- * Re-exports
  Key(..),
  SpecialKey(..),
  MouseButton(..),
  KeyState(..),
  Modifiers(..),
) where

import Control.Exception.Lifted
import Control.Concurrent.Chan.Unagi qualified as U
import Control.Monad.Reader
import Data.IORef

import Graphics.Gloss
import Graphics.Gloss.Interface.IO.Game
import Dyna qualified as D
import Dyna.Gloss.Types
import Dyna.Gloss.Data.Vec

-- | Initial parameters for the Game.
data Spec = Spec
  { spec'display    :: Display   -- ^ display settings
  , spec'background :: Color     -- ^ background color to clear each frame
  , spec'steps      :: Int       -- ^ number of steps for simulation
  }

-- | Default settings. Runs in ullscreen mode.
defSpec :: Spec
defSpec = Spec FullScreen white 1

-- | Run the aplication. It accepts initial settings
-- and the dynamic value of pictures wrapped in the Run monad.
--
-- Note that to work properly we need to compile to executable with
-- options -O2 and -threaded. The function does not work in ghci or with runhaskell
-- because it requires support for multiple threads.
--
-- Define the application with the Main module. Then compie it:
--
-- > stack exec -- ghc -O2 -threaded dyna-gloss/examples/Ball.hs
--
-- And run the result:
--
-- > 	./dyna-gloss/examples/Ball
--
-- How it works? It runs the dynamic process at the background thread and
-- every time the gloss function requests new frame it takes a snapshot of the current
-- value of the main dynamic process which produces pictures. It's exactly what gloss
-- simulation function needs to render it on the screen.
runApp :: Spec -> Run (Dyn Picture) -> IO ()
runApp Spec{..} dynAct = do
  env <- newEnv
  ref <- runReaderT (unRun $ D.runDyn . unDyn =<< dynAct) env
  playIO spec'display spec'background spec'steps ref (draw env) (onEvents env) (onIterate env)
    `finally` (runReaderT (unRun $ D.cancelDyn ref) env)
  where
    draw env ref = runReaderT (unRun $ D.readDyn ref) env

    onEvents Env{..} evt ref = do
      case evt of
        EventResize sizes      -> U.writeChan (fst env'resizeChan) sizes
        EventMotion (x, y)     -> do
          let pos = Vec x y
          prevPos  <- readIORef env'mousePos
          prevDif1 <- readIORef env'mouseDif1
          let dif1 = pos - prevPos
          writeIORef env'mousePos pos
          writeIORef env'mouseDif1 dif1
          writeIORef env'mouseDif2 (dif1 - prevDif1)
        EventKey k st mods pos -> U.writeChan (fst env'keyChan) (Click k st mods (fromTuple pos))
      pure ref

    onIterate Env{..} time ref = do
      U.writeChan (fst env'frameChan) time
      pure ref

----------------------------------------------------------------

-- | Read mouse positions. It produces dynamic of vectors. @(0, 0)@ is a center of the screen.
mouse :: Dyn Vec
mouse = Dyn $ D.constDyn $ do
  ref <- asks env'mousePos
  liftIO $ readIORef ref

-- | Mouse velocity or displacement
mouseV :: Dyn Vec
mouseV = Dyn $ D.constDyn $ do
  ref <- asks env'mouseDif1
  liftIO $ readIORef ref

-- | Mouse accelartion or speed of displacement
mouseA :: Dyn Vec
mouseA = Dyn $ D.constDyn $ do
  ref <- asks env'mouseDif2
  liftIO $ readIORef ref

isDrag :: MouseButton -> Dyn Bool
isDrag btn = Dyn $ D.scanD collect False $ D.mapMay go $ unEvt getClicks
  where
    go (Click key st mods pos) = case key of
      MouseButton mbtn | mbtn == btn -> Just st
      _                              -> Nothing

    collect a st = case a of
      Up   -> False
      Down -> True


-- | Displacement on drag, if no drag it becomes zero
dragV :: MouseButton -> Dyn Vec
dragV btn = (\x -> if x then id else const 0) <$> isDrag btn <*> mouseV

-- | Position of the mouse during drag, if no drag it becomes zero
drag :: MouseButton -> Dyn Vec
drag btn = (\x -> if x then id else const 0) <$> isDrag btn <*> mouse

-- | Event stream of clicks of the mouse right button
mouseRight :: Evt Vec
mouseRight = Evt $ D.mapMay go $ unEvt getClicks
  where
    go = \case
      Click (MouseButton RightButton) Down mods pos -> Just pos
      _                                             -> Nothing

-- | Event stream of key up actions
keyUp :: Key -> Evt Modifiers
keyUp = keyBy Up

-- | Event stream of key down actions
keyDown :: Key -> Evt Modifiers
keyDown = keyBy Down

-- | Event stream of char press up actions
charUp :: Char -> Evt Modifiers
charUp = keyUp . Char

-- | Event stream of char press down actions
charDown :: Char -> Evt Modifiers
charDown = keyDown . Char

keyBy :: KeyState -> Key -> Evt Modifiers
keyBy st' key' = Evt $ D.mapMay go $ unEvt getClicks
  where
    go (Click key st mods pos)
      | key == key' && st == st' = Just mods
      | otherwise                = Nothing

-- | Event stream of clicks of the mouse left button
mouseLeft :: Evt Vec
mouseLeft = Evt $ D.mapMay go $ unEvt getClicks
  where
    go = \case
      Click (MouseButton LeftButton) Down mods pos -> Just pos
      _                                            -> Nothing

-- | Mouse wheel displacement.
-- If positive then it goes up, if negative then it goes down.
mouseWheel :: Evt Float
mouseWheel = Evt $ D.mapMay go $ unEvt getClicks
  where
    go (Click key st mods pos) = case key of
      MouseButton WheelUp   -> Just 1
      MouseButton WheelDown -> Just (-1)
      _                     -> Nothing

-- | Reads generic click events
getClicks :: Evt Click
getClicks = Evt $ D.Evt $ \go -> do
  keyChan <- asks env'keyChan
  D.runEvt (D.uchanEvt (fst keyChan)) go

-- | Reads frame updates. Value of the event is a time that has passed since the previous frame.
--
-- Note that if we want to use the sort of event stream as a timeline for the game or simulation
-- we can also use time utilities from the FRP library: @clock@, @pulse@, @ticks@, @timer@.
getFrames :: Evt Float
getFrames = Evt $ D.Evt $ \go -> do
  frameChan <- asks env'frameChan
  D.runEvt (D.uchanEvt (fst frameChan)) go

-- | Reads window resize events
getResize :: Evt (Int, Int)
getResize = Evt $ D.Evt $ \go -> do
  resizeChan <- asks env'resizeChan
  D.runEvt (D.uchanEvt $ fst resizeChan) go