-
Notifications
You must be signed in to change notification settings - Fork 2
Iteration 6
Previous: Iteration 5: Ship Movement
The source code for this iteration can be found here.
All the changes from the previous iteration can be viewed in diff format here.
In order to get rid of the uncertancies of GLUT's internal timing issues, we
are going to merge the logicTick
with the renderViewport
callback, and
implement the logic presented in the article Fix Your Timestep!.
This will make our render function a lot more imperative and all around uglier, but at least all the ugliness is still contained within a single function.
import Data.Time.Clock.POSIX
type KeyboardRef = IORef Keyboard
type TimeRef = IORef POSIXTime
type StateRef = IORef GameState
data CallbackRefs = CallbackRefs TimeRef TimeRef KeyboardRef StateRef
-- | Initialize a new group of callback references
initCallbackRefs :: IO CallbackRefs
initCallbackRefs = do
accum <- newIORef $ 0
prev <- getPOSIXTime >>= newIORef
keyb <- newIORef initKeyboard
st <- newIORef initialGameState
return $ CallbackRefs accum prev keyb st
We've added a new datatype to contain all the global references we'll be using in the new render function.
The render function itself grows to this abomination:
-- | Run the game logic, render the view and swap display buffers
renderViewport :: CallbackRefs -> IO ()
renderViewport refs@(CallbackRefs ar tr kb rr) = do
current <- getPOSIXTime
prev <- readIORef tr
accum <- readIORef ar
keys <- readIORef kb
let frameTime = min 0.1 $ current - prev
newAccum = accum + frameTime
let consumeAccum acc = if acc >= 0.033
then do
modifyIORef rr $ tick keys
consumeAccum $ acc - 0.033
else return acc
newAccum' <- consumeAccum newAccum
writeIORef tr current
writeIORef ar newAccum'
r <- readIORef rr
clear [ColorBuffer]
render r
swapBuffers
postRedisplay Nothing
The idea is that we measure the time between this and previous call (frameTime
)
and add that to our time accumulator. The consumeAccum
then executes game ticks
for as long as there is enough accumulated time. I.e. for every 33ms we have
accumulated so far, the GameState
tick function is executed once. The remaning
time in the accumulator is saved for the next call.
The initializeCallbacks
method gets a bit shorter, since don't have the separate
logic callback anymore.
-- | Set up GLUT callbacks
initializeCallbacks = do
refs <- initCallbackRefs
keyboardMouseCallback $= Just (handleKeyboard refs)
displayCallback $= renderViewport refs
The resulting framerate is a lot more stabile than in the previous iteration, but we still get some unpleasant ghosting because the ship location is only updated every 33ms. To make the movement smoother, we can use the value remaining in the accumulator to interpolate the ship coordinates between two consequtive frames.
Next: Iteration 7: Frame Interpolation