Question

I'm writing a fairly simple 2D multiplayer-over-network game. Right now, I find it nearly impossible for myself to create a stable loop. By stable I mean such kind of loop inside which certain calculations are done and which is repeated over strict periods of time (let's say, every 25 ms, that's what I'm fighting for right now). I haven't faced many severe hindrances this far except for this one.

In this game, several threads are running, both in server and client applications, assigned to various tasks. Let's take for example engine thread in my server application. In this thread, I try to create game loop using Thread.sleep, trying to take in account time taken by game calculations. Here's my loop, placed within run() method. Tick() function is payload of the loop. It simply contains ordered calls to other methods doing constant game updating.

long engFPS = 40;
long frameDur = 1000 / engFPS;
long lastFrameTime;
long nextFrame;

<...>

while(true)
{
    lastFrameTime = System.currentTimeMillis();
    nextFrame = lastFrameTime + frameDur;

    Tick();

    if(nextFrame - System.currentTimeMillis() > 0)
    {
        try
        {
            Thread.sleep(nextFrame - System.currentTimeMillis());
        }
        catch(Exception e)
        {
            System.err.println("TSEngine :: run :: " + e);
        }
    }
}

The major problem is that Thread.sleep just loves to betray your expectations about how much it will sleep. It can easily put thread to rest for much longer or much shorter time, especially on some machines with Windows XP (I've tested it myself, WinXP gives really nasty results compared to Win7 and other OS). I've poked around internets quite a lot, and result was disappointing. It seems to be fault of the thread scheduler of the OS we're running on, and its so-called granularity. As far as I understood, this scheduler constantly, over certain amount of time, checks demands of every thread in system, in particular, puts/awakes them from sleep. When re-checking time is low, like 1ms, things may seem smooth. Although, it is said that WinXP has granularity as high as 10 or 15 ms. I've also read that not only Java programmers, but those using other languages face this problem as well. Knowing this, it seems almost impossible to make a stable, sturdy, reliable game engine. Nevertheless, they're everywhere. I'm highly wondering by which means this problem can be fought or circumvented. Could someone more experienced give me a hint on this?

Was it helpful?

Solution

Don't rely on the OS or any timer mechanism to wake your thread or invoke some callback at a precise point in time or after a precise delay. It's just not going to happen.

The way to deal with this is instead of setting a sleep/callback/poll interval and then assuming that the interval is kept with a high degree of precision, keep track of the amount of time that has elapsed since the previous iteration and use that to determine what the current state should be. Pass this amount through to anything that updates state based upon the current "frame" (really you should design your engine in a way that the internal components don't know or care about anything as concrete as a frame; so that instead there is just state that moves fluidly through time, and when a new frame needs to be sent for rendering a snapshot of this state is used).

So for example, you might do:

long maxWorkingTimePerFrame = 1000 / FRAMES_PER_SECOND;  //this is optional
lastStartTime = System.currentTimeMillis();
while(true)
{
    long elapsedTime = System.currentTimeMillis() - lastStartTime;
    lastStartTime = System.currentTimeMillis();

    Tick(elapsedTime);

    //enforcing a maximum framerate here is optional...you don't need to sleep the thread
    long processingTimeForCurrentFrame = System.currentTimeMillis() - lastStartTime;
    if(processingTimeForCurrentFrame  < maxWorkingTimePerFrame)
    {
        try
        {
            Thread.sleep(maxWorkingTimePerFrame - processingTimeForCurrentFrame);
        }
        catch(Exception e)
        {
            System.err.println("TSEngine :: run :: " + e);
        }
    }
}

Also note that you can get greater timer granularity by using System.nanoTime() in place of System.currentTimeMillis().

OTHER TIPS

You may getter better results with

LockSupport.parkNanos(long nanos) 

altho it complicates the code a bit compared to sleep()

maybe this helps you. its from david brackeen's bock developing games in java and calculates average granularity to fake a more fluent framerate: link

public class TimeSmoothie {
    /**
        How often to recalc the frame rate
    */
    protected static final long FRAME_RATE_RECALC_PERIOD = 500;
    /**
            Don't allow the elapsed time between frames to be more than 100 ms

    */
    protected static final long MAX_ELAPSED_TIME = 100;
    /**

        Take the average of the last few samples during the last 100ms

    */
    protected static final long AVERAGE_PERIOD = 100;
    protected static final int NUM_SAMPLES_BITS = 6; // 64 samples
    protected static final int NUM_SAMPLES = 1 << NUM_SAMPLES_BITS;
    protected static final int NUM_SAMPLES_MASK = NUM_SAMPLES - 1;
    protected long[] samples;
    protected int numSamples = 0;
    protected int firstIndex = 0;
    // for calculating frame rate
    protected int numFrames = 0;
    protected long startTime;
    protected float frameRate;

    public TimeSmoothie() {
        samples = new long[NUM_SAMPLES];
    }
    /**
        Adds the specified time sample and returns the average
        of all the recorded time samples.
    */

    public long getTime(long elapsedTime) {
        addSample(elapsedTime);
        return getAverage();
    }

    /**
        Adds a time sample.
    */

    public void addSample(long elapsedTime) {
        numFrames++;
        // cap the time
        elapsedTime = Math.min(elapsedTime, MAX_ELAPSED_TIME);
        // add the sample to the list
        samples[(firstIndex + numSamples) & NUM_SAMPLES_MASK] =
            elapsedTime;
        if (numSamples == samples.length) {
            firstIndex = (firstIndex + 1) & NUM_SAMPLES_MASK;
        }
        else {
            numSamples++;
        }
    }
    /**
        Gets the average of the recorded time samples.
    */

    public long getAverage() {
        long sum = 0;
        for (int i=numSamples-1; i>=0; i--) {
            sum+=samples[(firstIndex + i) & NUM_SAMPLES_MASK];
            // if the average period is already reached, go ahead and return
            // the average.
            if (sum >= AVERAGE_PERIOD) {
                Math.round((double)sum / (numSamples-i));
            }
        }

        return Math.round((double)sum / numSamples);

    }

    /**

        Gets the frame rate (number of calls to getTime() or

        addSample() in real time). The frame rate is recalculated

        every 500ms.

    */

    public float getFrameRate() {

        long currTime = System.currentTimeMillis();



        // calculate the frame rate every 500 milliseconds

        if (currTime > startTime + FRAME_RATE_RECALC_PERIOD) {

            frameRate = (float)numFrames * 1000 /

                (currTime - startTime);

            startTime = currTime;

            numFrames = 0;

        }



        return frameRate;

    }

}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top