Using the clsGameClock Class

The previous post introduced the Game Clock class that I'll be using with the Nimble libraries and walked through a few concepts that were considered in putting the class together.  I mentioned in that post that I wanted the system to be flexible enough to fit the style of the programmer using it -- allowing everything from lock-frame timing to simple delta-time to handling split update/present cycles.

Simple Delta-Time - The simplest way to use the Game Clock is simple delta-time, with a single update and present on each game loop.  It is as simple as this:

'Declarations
Private GameClock as New clsGameClock()
'----------

'In Game Loop ...
GameClock.Update

UpdateGameObjects()

DrawAllGameObjects()

Inside your "UpdateGameObjects", you would use GameClock.DeltaTime and GameClock.DeltaTime_Raw as the multiplier for any physics math.  During "DrawAllGameObjects", you can use GameClock.CPS to show the cycles-per-second rate of the game.  Finally, GameClock.SpeedFactor can be adjusted to speed up and slow down the on-screen action.

Lock-Frame - It's quite easy to switch to a lock-frame approach.  Just adjust the GameClock.MinimumDelta value to fit the number of frames to be targetted each second:

'In Setup
'    To Target 50 frames-per-second
GameClock.MinimumDelta = 1/50

Of course, when using lock-frame, you may not want to use the .DeltaTime values ... just update the same amount each loop.  But then you lose the added benefit of having an automated SpeedFactor available if you have a need to add slow-down effects.

Splitting Update/Present - And that brings us to a more complex idea ... splitting your game logic update cycles from your presentation frames.  The concept is to cycle through your game logic as many times as possible while still leaving enough time to draw everything once within a target frame rate.  This done by getting an estimate of how long it is taking to draw everything and how much time it takes to get through a single game logic update cycle.  There is a check against the target total time wanted to take for a complete loop ... and if there is enough time left to work in another game logic cycle then we do.

In both cases, we base our estimates on the previous loop ... and, as a failsafe, we give ourselves a way to trigger the system to pair a single logic loop with the next draw loop.  This single-cycle trigger would be used in a case where we've drastically changed the number of objects that need to be updated and/or drawn.  Maybe we've switched from a menu to the start of the next level.  Or, maybe there was an explosion and we've just popped 100 new objects onto the screen.

Let's look at the code, and then walk through things ...

'Declarations
Private TargetLoopTime As Double
Private LastDrawStart As Double, LastLoopStart As Double
Private SingleCycleOnce As Boolean
'----------

'In Setup
'   To Target drawing a new frame 40 times per second
TargetLoopTime = 1/40

'Set the system to only do a single update loop the first time
SingleCycleOnce = True
'----------

'In Game Loop ...

' === Game Logic Update Loops
Do

   'Update Clock
   myGameClock.Update()

   'Record update loop's start time
   LastLoopStart = myGameClock.GameTime

   UpdateGameObjects()

Loop Until SingleCycleOnce Or _
   (((myGameClock.GameTime_Live - LastDrawStart) + _
      (myGameClock.GameTime_Live - LastLoopStart)) >= myTargetLoopTime)

'Reset Single Cycle Once flag
SingleCycleOnce = False


' === Draw to Game Screen

'Wait if we are drawing too early in the total cycle
Do
   'Nothing
Loop Until myGameClock.GameTime_Live - LastDrawStart >= myTargetLoopTime

'Capture GameTime before Draw Starts
LastDrawStart = GameClock.GameTime_Live

DrawAllGameObjects()

'Let GameClock know about screen refresh
GameClock.FrameDrawn()

We first pick a target for how long it should take before drawing the next frame.  As an example, to refresh the game screen 40 times per second, we set our TargetLoopTime to 1/40.  With that timeframe set, we have our guide in which to fit our multiple logic loops plus a single draw loop.

Then we start our game logic loops.  The game clock is updated to get the latest delta and to keep count of the cycles, and we record the game time at the start of our logic updates.  We allow all of the objects to update themselves and we check if we have enough time to make another loop.  Also, if we've had a reason to drop out of the logic looping early, it will do that as well.

The SingleCycleOnce flag allows the program code to catch situations where the logic and/or draw cycles will take longer than they have been.  Perhaps a new wave of enemies has just hit the gamespace, or a large particle effect, etc.  In that case, we simply need to set SingleCycleOnce to TRUE to get the update/draw loop to run as quickly as possible and re-evaluate the time needed to get everything done.

Once the updates are done, the game time that the Draw cycle starts is captured to be used during the next set of logic loops.  Finally, we let the GameClock know that a frame has been drawn in order to let it track FPS separately from CPS.

You'll notice that this system is very conservative -- only gaining extra logic loops when it is fairly sure to have enough time to get them in.  The reasoning is that having a steady FPS is very important for a nice player experience ... it's better to sacrifice one extra logic loop than to have it drop to a slower framerate.  At its slowest, the system ensures 1 logic cycle and 1 draw cycle -- the same as running on simple delta time.

I'll be working a system similar to this into Nimble2D's Core class.  It will be initially setup to default to a straight, simple delta-time system with a single logic cycle followed by a single draw cycle -- CPU will be the same as FPU.  However, with a few simple changes, the game can be running on a split cycle system with measurements for each cycle rate.

 

Published Monday, October 20, 2008 7:23 PM by MattWorden

Comments

No Comments