尝试创建稳定的游戏引擎循环

发布于 2024-10-25 21:01:48 字数 1283 浏览 1 评论 0原文

我正在编写一个相当简单的 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 给出的结果非常糟糕)。我在网上查了很多资料,但结果令人失望。这似乎是我们运行的操作系统的线程调度程序及其所谓的粒度的错误。据我了解,这个调度程序在一定的时间内不断地检查系统中每个线程的需求,特别是将它们从睡眠状态中唤醒/唤醒。当重新检查时间很短(例如 1 毫秒)时,事情可能看起来很顺利。不过,据说WinXP的粒度高达10或15毫秒。我还了解到,不仅 Java 程序员,使用其他语言的程序员也面临这个问题。 知道了这一点,制造一个稳定、坚固、可靠的游戏引擎似乎几乎是不可能的。然而,它们无处不在。 我非常想知道通过什么方法可以解决或规避这个问题。有更有经验的人可以给我提示吗?

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?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

奢欲 2024-11-01 21:01:48

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

处理这个问题的方法是,不要设置睡眠/回调/轮询间隔,然后假设该间隔保持高精度,而是跟踪自上一次迭代以来经过的时间量,并将其用于确定当前状态应该是什么。将此数量传递给基于当前“框架”更新状态的任何内容(实际上,您应该以内部组件不知道或不关心框架等具体事物的方式设计引擎;因此,只有随时间流畅移动的状态,并且当需要发送新帧来渲染该状态的快照时使用)。

例如,您可能会这样做:

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() 来获得更大的计时器粒度。

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().

鱼忆七猫命九 2024-11-01 21:01:48

但您可能会得到更好的结果

LockSupport.parkNanos(long nanos) 

尽管与 sleep() 相比,它使代码有点复杂,

You may getter better results with

LockSupport.parkNanos(long nanos) 

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

为你拒绝所有暧昧 2024-11-01 21:01:48

也许这对你有帮助。
来自 david brackeen 的 bock 用 java 开发游戏
并计算平均粒度以伪造更流畅的帧速率:
链接

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;

    }

}

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;

    }

}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文