October 2008 - Posts

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.

 

Posted by MattWorden | with no comments

Tick-Tock Game Clock

There are some things needed in a game that are independent of the graphics system.  One of them is the system used to keep track of time passing as the game runs.  I prefer to work with a delta-time based system.  This means that we figure out how much time has passed since the last loop and use that -- whatever it may be -- to handle our logic, physics calcs, collision detection, etc.  The other traditional way of doing things is to lock on to a consistent loop rate and then you can assume things will be happen in the same amounts each loop.

It's also a good idea to separate game logic (object movement & changes, AI, physics, etc.) from user presentation (graphics and sound).  To give a realistic experience, you only need to update the presentation about 30 frames per second.  (However, updating more often can give a more intense experience.)  Game logic can usually benefit from looping as quickly as possible -- small time deltas allow for easier and more responsive collision checks, physics and AI.  The approach would then be to cycle through the game logic as many times as possible while still leaving enough time to draw everything at the wanted presentation rate.

I would like Nimble2D to provide ways to take any of these approaches.  And this all starts with how the game clock system works.  Let's walk through a few concepts to get to our complete clsGameClock class to see what's going on there.

Concept 1 - DeltaTime - VB.Net provides a very nice Stopwatch object that works very similarly to a handheld stopwatch.  It can be started, stopped, reset, and asked for the current elapsed time.  If a high precision timer is available through the OS, it will use that.  The elapsed time can be reported in milliseconds, seconds, and several other deliminations.  The general idea with a game clock is to get a Stopwatch running, and then check how much time has elapsed on each game logic loop.  The difference from one loop to the next will be your "delta time".  It is used with your game objects' velocity, acceleration, etc., to update their positions.

For example, if an object is moving at 300 pixels per second and your DeltaTime is captured in seconds, then in a single game loop the object will move a distance of 300 * DeltaTime.

Our DeltaTime-finding code would look like this:

'Declarations ...
Private SW As Stopwatch
Private myLastTime As Double, myDeltaTime as Double
'----------

'In the Constructor ...
SW = New Stopwatch()
SW.Start()
myLastTime = 0.0F
myDeltaTime = 0.0F
'----------

'In the Update method ...
myDeltaTime = SW.Elapsed.TotalSeconds - myLastTime
myLastTime = SW.Elapsed.TotalSeconds

Concept 2 - Careful with Data Types - In the previous post, I mentioned wanting to avoid data type conversions whenever possible.  The Stopwatch uses a Double time to give the best possible precision to the elapsed time when working in seconds.  However, GDI generally likes to work with Single data types.  So, once we capture our DeltaTime, we will convert it to a Single data type and provide that for anything that needs a DeltaTime value.  This will mean that object variables for velocities, accelerations, etc., should be held as Singles and multiplied by the Single DeltaTime to get a Single result (such as a position) which can be used by GDI without having to do extra data type conversions along the way.  In other words, we're purposely doing 1 data conversion per loop to avoid multiple conversions per object per loop.

This adds the following things to our code:

'Declaration ...
Private sngDelta As Single
'----------

'In the Update method, after the value for myDeltaTime is found
sngDelta = CSng(myDeltaTime)

Concept 3 - Minimum DeltaTime - There are a lot of reasons to have an assumed "smallest possible delta time".  For true DeltaTime users, having a minimum DeltaTime will avoid problems on very, very fast machines providing extremely small DeltaTime values, which may play havoc with phyics math.  For those who like locked frame rates, you can use a larger minimum DeltaTime (such as 1/40 for 40 frames-per-second) to cause the gameclock to be the governor of the frame timing.  While waiting for the minimum DeltaTime, we should allow the application to DoEvents so that form events can be responded to.

To make this happen, the following code elements are added:

'Declaration ...
Private myMinDelta As Double
'----------

'In the Constructor ...
'     A Minimum DeltaTime can be passed as a parameter
Public Sub New(Optional ByVal MinimumDelta As Double = 0.0001F)
     myMinDelta = MinimumDelta
'----------

'In the Update method, replace what we've already done with ...
Do
     Application.DoEvents()
     myDeltaTime = SW.Elapsed.TotalSeconds - myLastTime
Loop Until myDeltaTime >= myMinDelta

myLastTime = SW.Elapsed.TotalSeconds
sngDelta = CSng(myDeltaTime)

Concept 4 - Speed Factor - To allow for special effects along the lines of slowing everything down ("bullet time") or speeding everything up at the same time, we can include a "speed factor" to be applied to the DeltaTime.  Normally, this will just be at a value of 1.0f.  However, for example, it could be adjusted to 0.5f to slow things down to half-speed, or to 2.0f for double-time.  If our DeltaTime is going to be adjusted by this SpeedFactor value, then we should also provide a "raw" version of DeltaTime for those things that should be moving at normal speed at all times (like camera movement ... or a main character during a "timewarp" effect).

It only take a little extra code to make this work:

'Declarations ...
Private mySpeedFactor As Double
Private sngDeltaRaw As Double
'----------

'In the Update method, change the DeltaTime setting to ...
sngDelta = CSng(myDeltaTime * mySpeedFactor)
sngDeltaRaw = CSng(myDeltaTime)

Concept 5 - CPS - The game clock is a great place to put cycles-per-second tracking code.  This is a pretty typical measurement used by game programmers to monitor just how fast things are working.  (Normally, this is referred to as "frames per second", but since we might be tracking game logic cycles separate from frame drawing cycles, I'm giving this the more generic "cycles-per-second" label.)  This can be done by simply counting the number of times the Update method is called during the past second, then storing that value for reporting.

The code looks like this:

'Declarations
Private CycleTime As Double, CycleCount As Integer, CurCPS As Integer
'----------

'In the Update method, the following code is added at the end ...
CycleTime = CycleTime + myDeltaTime
CycleCount = CycleCount + 1

If CycleTime > 1.0F Then
     CurCPS = CycleCount
     CycleCount = 0
     CycleTime = CycleTime - 1.0F
End If

This wraps up the major concepts.  Add in some Properties for exposing information that will be useful to the rest of the game, and some code for pausing, restarting, and resetting ... and you'll get the entire clsGameClock class.  Get the full VB file here: LINK

This doesn't yet address how to go about splitting your game logic loops from your drawing loops.  That will be covered in the next post about how to make use of the clsGameclock class.

Making GDI+ Nimble

One thing that the GDI+ (System.Drawing namespace in VB.Net) system is *not* is overly quick.  It does a great job of certain things required by a more general, modern OS-based drawing system -- fonts, alpha, lines & shapes, etc.  But it isn't game-graphics quick.

Mainly, it uses the CPU to do its work (as opposed to the graphics card's GPU, like DirectX and OpenGL use) ... and the CPU is already busy trying to take care of overthing else going on (memory management, math calculations, input/output routing, etc.).  So, there are some things that we'll want to keep in mind when working in the GDI for game graphics.

Read Up:  First, read up on how the GDI works.  Here's a great site for higher-level overviews and some lower-level tips and tricks: http://www.bobpowell.net/gdiplus_faq.htm ... Also, Microsoft's very own official doco page is here: http://msdn.microsoft.com/en-us/library/ms533798.aspx.  These in-depth resources will be useful when you're trying to squeeze the last couple performance drops out of the GDI.

Keep Things Compact:  One of the best things for speeding up the GDI is keeping things as small as possible while still being able to deliver the game experience you're looking for.

Play Area:  The size of the "play area" (the on-screen window used to show what's going on in the game) will have an overall effect on speed.  The bigger it is, the longer it takes to clear, the more surface area available to draw game objects too, and the longer it takes to draw the backbuffer to the screen object.  While not impossible to go 800x600 (a pretty traditional size for retro-style games) on your play area, it will simply mean that the CPU requirement for your end-users will need to be bumped up.  Doing "mini-games" at 500x500 or 640x480 (another traditional size) is very doable.

Game Object Sizes:  Similar to the impact of size on the play area, the size of your actual in-screen game object graphics will matter.  Drawing screen-sized backgrounds will have a greater impact than clearing the game screen to a solid color and drawing just a few smaller items in as the background.  Keeping items that appear in number (particles, explosions, bullets, HUD and UI items, etc.) smaller is even more important.

Game Object Counts:  Limiting the number of game objects being drawn on screen in any particular loop is an area of important balance.  You want to provide the player with a certain type of game experience.  Sometimes, this requires a lot of stuff happening on-screen at the same time.  But the "feel" of the game will be diminished if it becomes chunky and slow while all of those things are happening.  So, balancing the two (and keeping an eye on the CPU horsepower requirements) will be a good challenge for a game programmer using GDI.  One quick solution is to have a (moveable) cap on particles and other things that appear in bulk but are mainly for eye candy.  When we get into putting some particles on the screen, I'll show a "particle manager" class that handles the limiting by ditching older particles for newer ones when its at the limit.  We'll also work through how to auto-test and auto-adjust these systems to be able to give those with better computers a "thicker" experience and still allow those with slower machines to play the game in an enjoyable manner.

Testing for Gaphics Efficiency:  In a lot of cases, there may be 2 or more ways to accomplish a similar effect.  If it seems that it might be a routine that is called often enough, it would be best to find the fastest way to deliver what you're after.  What's the fastest way to draw a 2x2 solid colored square?  DrawRectangle?  FillRectangle?  DrawLine?  DrawImage (with a 2x2 bitmap)?  To be honest, I'm not really sure ... but I plan to test these things as I come across them.  I'll do some searching for other's advice and I'll run some tests similar to what Almar Joling used to do on his VBfibre site (http://www.persistentrealities.com/vbfibre).  Once I have my answer, I'll code that as my go-to routine for that effect.

Coding for Efficiency Everywhere Else:  There are typical programming things that can be done to speed up the rest of your code, freeing up extra cycles for graphics drawing (and making things smoother overall).  Avoiding type conversions or heavy calcs inside loops, etc.  These are the typical good coding practices that we should be doing anyway ... but now we have an extra reason.  (Full disclosure:  I am *not* well brushed up on good coding practices in general ... so, feel free to help me out and point our areas where I can improve ... seriously!)

Beware the Garbage Collector:  Garbage Collectors are nice, helpful and good things.  But you just don't want them to come calling mid-game-loop ... they seem to want to show up when your right in a very intense moment in the game.  When they do their thing, they steal CPU cycles away from your game and usually cause it to give a big hitch.  This can really mess with your physics and logic calcs and generally makes the game look chunk and slow, if only for a fraction of a second.  To avoid this, use a create-hold-dispose approach for objects, where the drawing objects it needs (brushes, pens, bitmaps, paths, etc.) are created when the object is first instantiated.  They're held for the life of the object, and then finally properly disposed of when the object is being destroyed.  It's also important that you are careful of when you create and destroy your game objects.  Use "manager" classes to pre-create the number of game objects you will likely need during the intense part of the game ... then release them during "between level" and other slower times in the game.

The (lack of) speed of the GDI+ system was raised in a comment on my previous post ... and it's a valid concern.  But there are advantages to using GDI+ -- mainly having to do with compatibility and ease of development and deployment.  If a fun game can be made made quickly and easily distributed to work on a lot of machines, that's a good thing.  The trick is keeping a realistic viewpoint -- we won't be creating monster games ... but it should be the perfect fit for small games.

Posted by MattWorden | 1 comment(s)
Filed under: ,