Basic Game Loop

Started by
11 comments, last by Inferiarum 11 years, 8 months ago
Hello there, I quickly developed this solution to developing a fixed time step for a server. (client is XNA so time step is already taken care of there).


[source lang="csharp"]
Stopwatch stopwatch = new Stopwatch();
float timer = (1f / 60f) * 1000000;
while (true)
{
stopwatch.Reset();
stopwatch.Start();
//start update world
//end update world
Thread.Sleep(1);
while (stopwatch.ElapsedTicks < timer) { }
Console.WriteLine(stopwatch.ElapsedTicks);
stopwatch.Stop();
}[/source]

Its pretty basic, it sleeps for 1 millisecond to prevent 100% CPU usage (well really it sleeps for less than 15-16 milliseconds just because of the way Thread.Sleep works) then uses the stopwatch to see when its time to loop again. It does print out 16667 every cycle which is good, except for some times when it abnormaly printed out 32000+ (32 milliseconds). I am guessing this is due to the point of how Thread.Sleep works. The question is: is the code above good for a completely fixed time-step? should I also make use of winmm.dll (i think thats what it was called) for a more accurate fixed time step?

Also id love to not use Thread.Sleep() but I just cant find another way, the server is console based.

Replies are appriciated, Thanks!
Advertisement
I'm assuming this is c#
This might help http://msdn.microsof...y/ff650674.aspx

EDIT:
I realized after some reading that the Stopwatch uses the high performance counter.

I haven't really programmed in c# but here is my attempt:

[source lang="csharp"]using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;
using System.Threading;

namespace StopwatchApp
{
class TimeStep
{
public TimeStep(float Step)
{
Enable = true;

mStep = Step;

mCurrent = Stopwatch.GetTimestamp();
mLast = mCurrent;

mFrequency = Stopwatch.Frequency;
}

public void Reset() {mLast = Stopwatch.GetTimestamp();}

public bool WaitNextStep()
{
if(Enable)
{
mCurrent = Stopwatch.GetTimestamp();

long diff = mCurrent - mLast;
mDelta = (float)diff / (float)mFrequency;

float msDelta = 1000.0f * mDelta;
float msStep = 1000.0f * mStep;

if(msDelta < msStep)
{
int msSleep = (int)(msStep - msDelta);
if (msSleep == 0) msSleep = 1;
Thread.Sleep(msSleep);
}

mLast = mCurrent;
}
else return false;
return true;
}

public float GetDelta() {return mDelta;}

public bool Enable;

private long mFrequency;
private long mLast;
private long mCurrent;
private float mStep;
private float mDelta;
}

class Program
{
static void Main(string[] args)
{
TimeStep LoopStep = new TimeStep(1.0f/60.0f);

while (LoopStep.WaitNextStep())
{
float fDelta = LoopStep.GetDelta();

// Do stuff

Console.Clear();
Console.Write(fDelta);

if (Console.KeyAvailable)
{
LoopStep.Enable = false;
}
}
}
}
}[/source]

Never mind, this has the exact problem you are having. It does boil down to Thread.Sleep(msSleep). I think it's the nature of the OS returning control to your app.
After playing with Thread.Sleep I learned this:

Thread.Sleep(1) = ~15ms
Thread.Sleep(15) = ~15ms
Thread.Sleep(16) = ~30ms

Windows probably allocates 15ms per thread and then switches to next thread.
Grr, this is anoying. I thought creating a basic time-specified loop would be extremely easy, but instead it turns out windows does not like that... I will probably use this solution for the time being, the only problem is 10% of the CPU is being used all the time when using this method (and I have 8 cores), so really more than 50% of the core is being used. Thanks for the replies.
Sleep works via the Windows system timer (which uses the Programmable Interrupt Controller), which has a default resolution of 15.6ms on consumer versions of Windows. You can change the system timer resolution with the multimedia functions timeBeginPeriod and timeEndPeriod. But bear in mind that the system timer is a global system timer, so it will increase the timer resolution for every application running on the system and will cause the system to use more energy, though it should not eat all that much CPU.

Here is a white paper from Microsoft regarding system timer resolution:

Timers, Timer Resolution, and Development of Efficient Code

That being said, using Sleep to facilitate a fixed-step game loop may not give you the best results.

Instead you can use something like timeSetEvent or CreateTimerQueueTimer to set a periodic timer. While you will not be able to guarantee that your loop executes exactly every n milliseconds (nothing will), it will guarantee that it will be called exactly once for every n milliseconds elapsed. The difference is that, for instance, if your timer is set to fire every 16ms, and some other high priority thread hogs the CPU for 64ms, when it is free then your thread will immediately receive 4 callbacks for the 4 16ms periods that have elapsed, which will allow your simulation to catch up.

Sorry, not sure right off the bat how to use these from C#, but if you Google it you can probably find out.
i will look into that Windows API, thank you :)
Another method that doesn't require fiddling with the Windows API would be to use an event instead:

[source lang="csharp"]using System;
using System.Diagnostics;
using System.Threading;

static class P
{
static void Main()
{
var stopwatch = new Stopwatch();
var timer = (int)Math.Round((1f / 60f) * 1000f);
var wait = new ManualResetEvent(false);
while (true)
{
stopwatch.Reset();
stopwatch.Start();
//start update world
//end update world
wait.WaitOne((int)Math.Max(0, timer - stopwatch.ElapsedMilliseconds));
Console.WriteLine(stopwatch.ElapsedMilliseconds);
stopwatch.Stop();
}
}
}[/source]

This approach will work with a millisecond resolution only, but I seriously doubt that a higher resolution would be required anyway... especially given the fact that Windows is not a real-time OS and won't allocate thread execution time slots at a significantly higher resolution anyway :)
What about something like this (pseudocode, no idea how to use XNA):


// Init timekeeping variables

step = 0;
timeSinceLastUpdate = 0;

while(notDeadYet)
{
// total up the time pool, including leftovers from the last run through
step += timeSinceLastUpdate;
// Get the start of the update
start = currentTime();

// as long as there's enough time to step, step
while(step > timestepAmount)
{
stepGameState();

// Take a step out of the time pool
step -= timestepAmount;
}
render(); // Or not, it's whatever

// How long did that update take? If you're not rendering, you best hope it's less than timestepAmount
// or that variations are due to external processes or else your system is in the positive feedback loop
// of death and despair and youneedoptimisationness
timeSinceLastUpdate = currentTime() - start;
}


Basically, instead of hoping your system runs once a frame, you see how long it was since it last ran, and run it as long as there are ticks to run; i.e. if your step is 15ms and the game loop takes 30ms due to the scheduler or renderer, the game ticks twice, and if the game loop takes 14ms, it doesn't tick at all because it's not ready yet. I used += for step to accumulate partial ticks so the loop won't have a problem with two 14ms updates in a row, and if you pass the partial amount to the renderer it can use that to smooth animations and other motion. You could also probably modify it to step one tick into the "future" and use the partial tick to interpolate between the states to display instead of extrapolate to a guess (though at 60FPS I don't know how visible this is).

Another method that doesn't require fiddling with the Windows API would be to use an event instead:


Events with timeouts depend on the system clock too, so this will behave exactly the same as using Sleep.

What about something like this (pseudocode, no idea how to use XNA):


That would typically be my preferred method since you don't need to rely on or change the granularity of the system clock, but it will result in 100% CPU usage, which is precisely what he was trying to avoid by using Sleep. I'm not sure why that is a big deal in a game server unless he plans on running many instances of the game server application on the same machine at the same time.

This topic is closed to new replies.

Advertisement