游戏循环中最佳睡眠时间计算的研究

时间:2011-03-11 15:11:21

标签: java animation game-engine

编写动画和小游戏时,我已经开始了解Thread.sleep(n);的重要性,我依靠这种方法告诉操作系统何时我的应用程序不需要任何CPU,并使用这个制作我的程序以可预测的速度进步。

我的问题是JRE在不同的操作系统上使用不同的方法来实现此功能。在基于UNIX(或受影响)的操作系统上,例如Ubuntu和OS X,底层JRE实现使用功能良好且精确的系统将CPU时间分配给不同的应用程序,从而使我的2D游戏平滑无滞后。但是,在Windows 7和较旧的Microsoft系统上,CPU时间分配似乎有所不同,并且您通常会在给定睡眠量后恢复CPU时间,与目标睡眠时间相差约1-2毫秒。但是,偶尔会有额外10-20毫秒的睡眠时间爆发。这导致我的游戏在发生这种情况时每隔几秒就会滞后一次。我注意到我在Windows上尝试过的大多数Java游戏都存在这个问题,Minecraft就是一个明显的例子。

现在,我一直在互联网上寻找解决这个问题的方法。我见过很多人只使用Thread.yield();而不是Thread.sleep(n);,无论你的游戏实际需要多少CPU,它都会以当前使用的CPU内核满负荷为代价完美运行。这对于在笔记本电脑或高能耗工作站上玩游戏并不理想,而且在Mac和Linux系统上进行不必要的权衡。

进一步展望我发现了一种常用的纠正睡眠时间不一致的方法,称为“旋转睡眠”,你只能一次命令睡眠1毫秒,并使用System.nanoTime();方法检查一致性,这是即使在Microsoft系统上也非常准确。这有助于正常1-2毫秒的睡眠不一致,但它无法帮助抵抗偶尔爆发+ 10-20毫秒的睡眠不一致,因为这通常会导致花费更多的时间比我的循环的一个循环应该花费所有在一起。

经过大量的观察,我发现了这篇神秘的Andy Malakov文章,这对改善我的循环很有帮助:http://andy-malakov.blogspot.com/2010/06/alternative-to-threadsleep.html

根据他的文章我写了这个睡眠方法:

// Variables for calculating optimal sleep time. In nanoseconds (1s = 10^-9ms).
private long timeBefore = 0L;
private long timeSleepEnd, timeLeft;

// The estimated game update rate.
private double timeUpdateRate;

// The time one game loop cycle should take in order to reach the max FPS.
private long timeLoop;

private void sleep() throws InterruptedException {

    // Skip first game loop cycle.
    if (timeBefore != 0L) {

        // Calculate optimal game loop sleep time.
        timeLeft = timeLoop - (System.nanoTime() - timeBefore);

        // If all necessary calculations took LESS time than given by the sleepTimeBuffer. Max update rate was reached.
        if (timeLeft > 0 && isUpdateRateLimited) {

            // Determine when to stop sleeping.
            timeSleepEnd = System.nanoTime() + timeLeft;

            // Sleep, yield or keep the thread busy until there is not time left to sleep.
            do {
                if (timeLeft > SLEEP_PRECISION) {
                    Thread.sleep(1); // Sleep for approximately 1 millisecond.
                }
                else if (timeLeft > SPIN_YIELD_PRECISION) {
                    Thread.yield(); // Yield the thread.
                }
                if (Thread.interrupted()) {
                    throw new InterruptedException();
            }
                timeLeft = timeSleepEnd - System.nanoTime();
            }
            while (timeLeft > 0);
        }
        // Save the calculated update rate.
        timeUpdateRate =  1000000000D / (double) (System.nanoTime() - timeBefore);
    }
    // Starting point for time measurement.
    timeBefore = System.nanoTime();
}

SLEEP_PRECISION我通常需要大约2 ms,SPIN_YIELD_PRECISION大约10 000 ns才能获得最佳性能。

经过大量的努力,这是我能想到的绝对最好的。所以,既然我仍然关心提高这种睡眠方法的准确性,而且我仍然对性能不满意,我想呼吁你们所有的java游戏黑客和动画师那里有关于更好的解决方案的建议Windows平台。我可以在Windows上使用特定于平台的方式来使其更好吗?我不关心在我的应用程序中使用一些特定于平台的代码,只要大多数代码与操作系统无关。

我还想知道是否有人知道微软和甲骨文正在更好地实施Thread.sleep(n);方法,或者甲骨文的未来计划是什么,以改善他们的环境作为需要高的应用程序的基础时间准确度,如音乐软件和游戏?

感谢大家阅读我冗长的问题/文章。我希望有些人可能会发现我的研究很有帮助!

5 个答案:

答案 0 :(得分:4)

您可以使用与cyclic timer相关联的mutex。这是IHMO做你想做的最有效的方式。但是,如果计算机滞后,您应该考虑跳帧(您可以使用计时器代码中的其他非阻塞互斥锁来执行此操作。)

编辑:一些伪代码来澄清

计时器代码:

While(true):
  if acquireIfPossible(mutexSkipRender):
    release(mutexSkipRender)
    release(mutexRender)

睡眠代码:

acquire(mutexSkipRender)
acquire(mutexRender)
release(mutexSkipRender)

起始值:

mutexSkipRender = 1
mutexRender = 0

修改:更正了初始化值。

以下代码在windows上工作得很好(循环精确到50fps,精度为毫秒)

import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.Semaphore;


public class Main {
    public static void main(String[] args) throws InterruptedException {
        final Semaphore mutexRefresh = new Semaphore(0);
        final Semaphore mutexRefreshing = new Semaphore(1);
        int refresh = 0;

        Timer timRefresh = new Timer();
        timRefresh.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                if(mutexRefreshing.tryAcquire()) {
                    mutexRefreshing.release();
                    mutexRefresh.release();
                }
            }
        }, 0, 1000/50);

        // The timer is started and configured for 50fps
        Date startDate = new Date();
        while(true) { // Refreshing loop
            mutexRefresh.acquire();
            mutexRefreshing.acquire();

            // Refresh 
            refresh += 1;

            if(refresh % 50 == 0) {
                Date endDate = new Date();
                System.out.println(String.valueOf(50.0*1000/(endDate.getTime() - startDate.getTime())) + " fps.");
                startDate = new Date();
            }

            mutexRefreshing.release();
        }
    }
}

答案 1 :(得分:4)

您的选择有限,取决于您想要做什么。你的代码片段提到了最大FPS,但是最大FPS会要求你根本不会睡觉,所以我不完全确定你打算用它做什么。然而,在大多数问题情况下,睡眠或产量检查都不会产生任何影响 - 如果某个其他应用程序现在需要运行且操作系统不想很快切换回来,则无论哪一个都无关紧要你打电话,当操作系统决定这样做时你会得到控制,这几乎肯定会在未来超过1毫秒。然而,操作系统当然可以被哄骗更频繁地制作交换机 - Win32正是为了这个目的而调用timeBeginPeriod,你可能会以某种方式使用它。但是有一个很好的理由不经常转换 - 效率低下。

最好的事情,虽然有点复杂,通常是为了一个不需要实时更新的游戏循环,而是以固定的间隔(例如,每秒20x)执行逻辑更新并随时渲染可能(如果没有全屏运行,可能会有任意短暂的睡眠以释放其他应用程序的CPU)。通过缓冲过去的逻辑状态以及当前逻辑状态,您可以在它们之间进行插值,使渲染看起来像每次都进行逻辑更新一样平滑。有关此方法的详细信息,请参阅Fix Your Timestep文章。

  

我还想知道是否有人知道Microsoft和Oracle正在制定一个更好的Thread.sleep(n)实现;方法,或者甲骨文未来的计划是改善环境,作为需要高时序精度的应用程序的基础,如音乐软件和游戏?

不,这不会发生。请记住,睡眠只是一种方法,可以说明您希望程序保持多久。它不是一个什么时候会醒来,也不应该醒来的规范。根据定义,任何具有睡眠和产量功能的系统都是一个多任务系统,其中必须考虑其他任务的要求,并且操作系统总是在调度时获得最终调用。替代方案不能可靠地工作,因为如果一个程序可能以某种方式要求在其选择的精确时间重新激活它可能会使其他CPU能力过程匮乏。 (例如,产生后台线程并且两个线程执行1ms工作并且最后调用sleep(1)的程序可以轮流占用CPU内核。)因此,对于用户空间程序,睡眠(和功能)喜欢它)将始终是一个下限,从来不是一个上限。要做得更好,需要操作系统本身允许某些应用程序几乎拥有调度,这对于消费类硬件的操作系统来说并不是一个理想的功能(同时也是工业应用程序的常用功能)。

答案 2 :(得分:2)

Windows上的定时功能非常糟糕。 This article是一个很好的起点。不确定您是否关心,但也注意到虚拟系统上可能存在更严重的问题(尤其是使用System.nanoTime)(当Windows是客户操作系统时)。

答案 3 :(得分:1)

Thread.Sleep说你的应用程序不再需要时间了。这意味着在最坏的情况下,您将不得不等待整个线程切片(大约40ms)。

现在在坏的情况下,当一个驱动程序或某些东西占用更多时间时,你可能需要等待120毫秒(3 * 40ms),因此Thread.Sleep不是可行的方法。另一种方式,比如注册一个1ms的回调并开始绘制代码非常X回调。

(这是在Windows上,我使用MultiMedia工具来获得那些1ms分辨率的回调)

答案 4 :(得分:0)

Thread.sleep不准确,并且大部分时间都会让动画抖动。

如果用Thread.yield完全替换它,你将得到一个没有滞后或抖动的稳定FPS,但CPU使用率会大大增加。很久以前我搬到了Thread.yield。

多年来,Java Game Development论坛已经讨论过这个问题。