{-# LANGUAGE FlexibleInstances, MultiParamTypeClasses #-} {-# LANGUAGE ExistentialQuantification #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE RecordWildCards #-} {-| Module : HateCommon Description : Core and common parts of the Hate framework. License : MIT Maintainer : bananu7@o2.pl Stability : experimental Portability : requires OpenGL and GLFW build -} module Hate.Common ( module Hate.Common.Types , module Hate.Common.Instances , Hate.Common.Types.Hate(..) , runApp , getKey , whenKeyPressed ) where import Hate.Util import Hate.Common.Types import Hate.Common.Instances() import Hate.Events import Hate.Graphics.Rendering import Hate.Graphics.Backend import Control.Monad.State import Control.Applicative import System.Exit import System.IO import Graphics.Rendering.OpenGL(($=)) import qualified Graphics.Rendering.OpenGL as GL import qualified Graphics.UI.GLFW as G import Data.Maybe import Control.Concurrent (threadDelay) -- Assuming this doesn't change, for now -- this actually controls update rate, as the display -- rate is vsynced desiredFPS = 60.0 desiredSPF = 1.0 / desiredFPS stderrErrorCallback :: G.ErrorCallback stderrErrorCallback _ = hPutStrLn stderr --keyCallback :: G.KeyCallback --keyCallback win key _ action _ = -- when (key == G.Key'Escape && action == G.KeyState'Pressed) $ -- G.setWindowShouldClose win True choose :: [IO (Maybe a)] -> IO (Maybe a) choose [] = return Nothing choose (x:xs) = do a <- x if isJust a then return a -- short-circuit else choose xs -- eventually fall to Nothing data GlContextDescriptor = GlContextDescriptor { majVersion :: Int, minVersion :: Int, forwardCompat :: Bool } deriving Show hateInitWindow :: String -> (Int, Int) -> IO G.Window hateInitWindow titl wSize = do G.setErrorCallback (Just stderrErrorCallback) successfulInit <- G.init -- if init failed, we exit the program bool successfulInit (hateFailureExit "GLFW Init failed") $ do mw <- choose [ tryOpenWindow (GlContextDescriptor 4 5 True) wSize titl , tryOpenWindow (GlContextDescriptor 4 5 False) wSize titl , tryOpenWindow (GlContextDescriptor 4 4 True) wSize titl , tryOpenWindow (GlContextDescriptor 3 3 True) wSize titl , tryOpenWindow (GlContextDescriptor 3 3 False) wSize titl ] case mw of Nothing -> G.terminate >> (hateFailureExit "Window creation failed") Just win -> do G.makeContextCurrent mw G.swapInterval 1 --vsync hateInitGL return win tryOpenWindow :: GlContextDescriptor -> (Int, Int) -> String -> IO (Maybe G.Window) tryOpenWindow cd (width, height) titl = do putStrLn $ "Opening Window (" ++ show cd ++ ")" G.windowHint (G.WindowHint'ContextVersionMajor (majVersion cd)) G.windowHint (G.WindowHint'ContextVersionMinor (minVersion cd)) G.windowHint (G.WindowHint'OpenGLForwardCompat (forwardCompat cd)) G.windowHint (G.WindowHint'OpenGLProfile G.OpenGLProfile'Core) G.windowHint (G.WindowHint'OpenGLDebugContext True) G.createWindow width height titl Nothing Nothing hateInitGL :: IO () hateInitGL = do GL.blend $= GL.Enabled GL.blendFunc $= (GL.SrcAlpha, GL.OneMinusSrcAlpha) hateFailureExit :: String -> IO a hateFailureExit errMsg = do hPutStrLn stderr errMsg exitFailure hateSuccessfulExit :: G.Window -> IO b hateSuccessfulExit win = do G.destroyWindow win G.terminate exitSuccess -- this function is so generic because it used to work with universally -- quantified Renderer. Now that libraryState uses RendererI it's not strictly -- necessary, but I don't mind leaving it here either. updateRendererState :: (forall r. Renderer r => (r -> IO (a, r))) -> HateInner us a updateRendererState mutator = do g <- gets libraryState case g of (LibraryState{ graphicsState = gs, ..}) -> do (ret, ngs) <- liftIO $ mutator gs modify $ \g -> g { libraryState = LibraryState { graphicsState = ngs, .. }} return $ ret runHateDraw :: (forall r. Renderer r => StateT r IO a) -> HateInner us a runHateDraw m = updateRendererState (runStateT m) hateLoop :: HateInner us () hateLoop = do gs <- get let w = window gs shouldClose <- (liftIO . G.windowShouldClose) w unless shouldClose $ do liftIO $ do (width, height) <- G.getFramebufferSize w --let ratio = fromIntegral width / fromIntegral height GL.viewport $= (GL.Position 0 0, GL.Size (fromIntegral width) (fromIntegral height)) GL.clear [GL.ColorBuffer] -- call user drawing function let drawRequests = drawFn gs $ userState gs runHateDraw $ render drawRequests -- update the game state in constant intervals Just t <- liftIO G.getTime let tDiff = t - (lastUpdateTime gs) evts <- reverse <$> fetchEvents let groupedEvts = groupEventsByTime (lastUpdateTime gs) desiredSPF evts when (tDiff > desiredSPF) $ if length groupedEvts > 0 then mapM_ hateUpdate groupedEvts else hateUpdate [] when (tDiff < desiredSPF) $ liftIO $ threadDelay (floor $ 1000000 * (desiredSPF - tDiff)) liftIO $ do G.swapBuffers w G.pollEvents hateLoop groupEventsByTime :: Time -> Time -> [TimedEvent] -> [[TimedEvent]] groupEventsByTime _ _ [] = [] groupEventsByTime lastTime frameLength evts = current : groupEventsByTime (lastTime + frameLength) frameLength next where (current, next) = span inCurrentFrame evts inCurrentFrame = ((lastTime + frameLength) >) . fst hateUpdate :: [TimedEvent] -> HateInner us () hateUpdate evts = do handleInternalEvents . map snd $ evts -- uncomment to print all the events out -- liftIO $ mapM print evts let allowedEvts = filter allowedEvent . map snd $ evts gs <- get runHate $ (updateFn gs) allowedEvts modify $ \x -> x { lastUpdateTime = lastUpdateTime x + desiredSPF } handleInternalEvents :: [Event] -> HateInner us () handleInternalEvents = mapM_ handleEvent where handleEvent e = case e of EventWindowSize xs ys -> do liftIO $ GL.viewport $= (GL.Position 0 0, GL.Size (fromIntegral xs) (fromIntegral ys)) runHateDraw $ updateScreenSize (xs, ys) _ -> return () getKey :: G.Key -> Hate us Bool getKey k = UnsafeHate $ do gs <- get stt <- liftIO $ G.getKey (window gs) k return $ keystateToBool stt where keystateToBool s | s == G.KeyState'Released = False | otherwise = True whenKeyPressed :: G.Key -> Hate us () -> Hate us () whenKeyPressed k action = do b <- getKey k if b then action else return () -- This function is called after the window has been created pickRenderer :: (Int, Int) -> IO RendererI pickRenderer ws = do (G.Version vMaj vMin _) <- G.getVersion if vMaj >= 4 then initialRendererStateModern ws else initialRendererStateCompat ws initialLibraryState :: Config -> IO LibraryState initialLibraryState cfg = LibraryState <$> pickRenderer (windowSize cfg) <*> initialEventsState runApp :: Config -> LoadFn us -> UpdateFn us -> DrawFn us -> IO () runApp config ldFn upFn drFn = do win <- hateInitWindow (windowTitle config) (windowSize config) libState <- initialLibraryState config setCallbacks (eventsState libState) win print $! "Loading user state" initialUserState <- ldFn time <- fromJust <$> G.getTime evalStateT hateLoop $ HateState { userState = initialUserState, window = win, drawFn = drFn, updateFn = upFn, libraryState = libState, lastUpdateTime = time } hateSuccessfulExit win