Skip to content

Iteration 3

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

Iteration 3: Introducing Animation

Previous: Iteration 2: Rotating the Ship

The source code for this iteration can be found here.

All the changes from the previous iteration can be viewed in diff format here.

Adding Motion

In past iterations we've only updated the game window with a static image, so the next step is to add some movement. This means that game state will somehow need to change between calls to our display callback.

First we'll add a new type class to Haskeroids/Tick:

module Haskeroids.Tick where

class Tickable t where
    tick :: t -> t

This means that for Tickable data, we can call the tick function to get a new version of the data.

Now let's make our GameState an instance of Tickable.

instance Tickable GameState where
    tick = tickState

-- | Tick state into a new game state
tickState :: GameState -> GameState
tickState (GameState pl) = GameState $ tick pl

This returns a new state that contains a ticked player, so we need to make our Player data tickable too. For this iteration, we'll just make the player ship rotate in place.

instance Tickable Player where
    tick (Player b) = Player $ rotate 0.1 b

Like the tickState function, this too creates a new Player that has a rotated body. We'll add the rotate method to Haskeroids/Geometry/Transform:

-- | Rotate a body
rotate :: Float -> Body -> Body
rotate d b@(Body {bodyAngle=a}) = b {bodyAngle = a+d}

Animation Callbacks

Now we just need something to call the tick method for our state, and to somehow pass the new updated state to our rendering method. For this, we add a new callback function to Haskeroids/Callbacks:

-- | Periodical logic tick
logicTick :: (LineRenderable t, Tickable t) => t -> IO ()
logicTick t = do
    let newTickable = tick t
    displayCallback $= renderViewport newTickable
    addTimerCallback 33 $ logicTick newTickable
    postRedisplay Nothing

The logicTick callback takes a single parameter, which has to be both tickable and renderable. It then updates the displayCallback to use the new, updated state and re-schedules itself 33ms into the future, fixing our logic step to an average of 30 updates per second. The postRedisplay call notifies GLUT that the window needs to be redrawn.

To actually call logicTick for the first time, we modify initializeCallbacks in Haskeroids/Initialize.

-- | Set up GLUT callbacks
initializeCallbacks = do
    displayCallback $= renderViewport initialGameState
    addTimerCallback 0 $ logicTick initialGameState

Note about addTimerCallback

Using addTimerCallback for updates like this is actually a pretty lousy choice, as it makes no guarantees about the accuracy of the timer. It only promises that the callback will not be triggered for at least 33ms, but there's no upper bound. This means that we are likely to get uneven frameframe, but we'll keep the implementation simple for now and improve this later.

Next: Iteration 4: Keyboard Controls

Clone this wiki locally