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.