Skip to content

Iteration 6

Sami Hangaslammi edited this page Mar 19, 2011 · 4 revisions

Iteration 6: Improved Timing

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.

Refactoring the Logic Tick

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.

Haskeroids/Callbacks.hs

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.

Haskeroids/Initialize.hs

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

Clone this wiki locally