试图创建一个稳定的游戏引擎循环

时间:2011-03-23 12:10:39

标签: java loops game-engine thread-sleep

我正在编写一个相当简单的2D多人游戏网络游戏。现在,我发现自己几乎不可能创建一个稳定的循环。稳定我的意思是这样的循环,在其中进行某些计算并且在严格的时间段内重复(假设每25毫秒,这就是我现在正在为之奋斗的东西)。到目前为止,我没有遇到过许多严重的障碍。

在这个游戏中,有几个线程在服务器和客户端应用程序中运行,分配给各种任务。我们以服务器应用程序中的引擎线程为例。在这个帖子中,我尝试使用Thread.sleep创建游戏循环,试图考虑游戏计算所花费的时间。这是我的循环,放在run()方法中。 Tick()函数是循环的有效负载。它只包含对进行持续游戏更新的其他方法的有序调用。

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);
        }
    }
}

主要问题是Thread.sleep喜欢背叛你对睡眠的期望。它可以轻松地将线程放置更长时间或更短的时间,特别是在一些使用Windows XP的机器上(我自己测试过,与Win7和其他操作系统相比,WinXP给出了非常糟糕的结果)。我在互联网上搜索了很多,结果令人失望。它似乎是我们正在运行的操作系统的线程调度程序的错误,以及它所谓的粒度。据我了解,这个调度程序在一定时间内不断检查系统中每个线程的需求,特别是将它们从睡眠中唤醒/唤醒。当重新检查时间很短时,例如1ms,事情看似平稳。虽然,据说WinXP的粒度高达10或15毫秒。我还读到,不仅是Java程序员,而且那些使用其他语言的人也面临着这个问题。 知道这一点,似乎几乎不可能制作一个稳定,坚固,可靠的游戏引擎。然而,它们无处不在。 我非常想知道这个问题可以解决或规避。更有经验的人可以给我一个暗示吗?

3 个答案:

答案 0 :(得分:2)

不要依赖操作系统或任何计时器机制来唤醒您的线程或在精确的时间点或精确延迟后调用某些回调。这不会发生。

处理此问题的方法不是设置睡眠/回调/轮询间隔,而是假设间隔保持高精度,请跟踪自上次迭代以来经过的时间量,用它来确定当前状态应该是什么。将此数量传递给任何基于当前“框架”更新状态的内容(实际上,您应该以内部组件不知道的方式设计您的引擎,或者关心任何像框架那样具体的东西;所以相反,它只是通过时间流动移动的状态,当需要发送新帧以呈现此状态的快照时使用。)

例如,您可以这样做:

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);
        }
    }
}

另请注意,使用System.nanoTime()代替System.currentTimeMillis()可以获得更大的计时器粒度。

答案 1 :(得分:0)

也许这会对你有所帮助。 它来自david brackeen的博客在java中开发游戏 并计算平均粒度以伪造更流畅的帧率: 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;

    }

}

答案 2 :(得分:0)

使用

可能会获得更好的结果
LockSupport.parkNanos(long nanos) 

与sleep()

相比,它使代码复杂化了