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

2022-09-02 13:54:44

在编写动画和小游戏时,我开始知道我依靠这种方法来告诉操作系统何时我的应用程序不需要任何CPU,并使我的程序以可预测的速度进行。Thread.sleep(n);

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

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

进一步环顾四周,我发现了一种常用的纠正睡眠时间不一致的方法,称为“旋转睡眠”,其中您一次只订购睡眠1毫秒,并使用该方法检查一致性,即使在Microsoft系统上也非常准确。这有助于正常1-2毫秒的睡眠不一致,但它无助于防止偶尔爆发的+10-20毫秒的睡眠不一致,因为这通常会导致花费的时间超过循环的一个周期应该花费的时间。System.nanoTime();

经过大量的研究,我发现了安迪·马拉科夫(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毫秒,大约10 000 ns,以便在我的Windows 7计算机上获得最佳性能。SPIN_YIELD_PRECISION

经过大量的辛勤工作,这是我能想到的绝对最好的。因此,由于我仍然关心提高这种睡眠方法的准确性,并且我仍然对性能不满意,因此我想向所有Java游戏黑客和动画师寻求有关Windows平台更好解决方案的建议。我可以在Windows上使用特定于平台的方式来使其更好吗?我不在乎在我的应用程序中有一些特定于平台的代码,只要大多数代码是独立于操作系统的。

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

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


答案 1

您可以使用与互斥体关联的循环计时器。这是IHMO做你想做的最有效的方式。但是,您应该考虑跳过帧,以防计算机滞后(您可以使用计时器代码中的另一个非阻塞互斥体执行此操作。

编辑:一些伪代码要澄清

定时器代码:

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

睡眠代码:

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

起始值:

mutexSkipRender = 1
mutexRender = 0

编辑:更正了初始化值。

以下代码在窗口上运行得很好(以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();
        }
    }
}

答案 2

您的选择是有限的,它们取决于您到底想做什么。你的代码片段提到了最大FPS,但最大FPS要求你永远不要睡觉,所以我不完全确定你打算这样做。然而,在大多数问题情况下,睡眠或良率检查都不会有任何区别 - 如果其他一些应用程序现在需要运行并且操作系统不想很快切换回来,那么您调用哪一个应用程序并不重要,当操作系统决定这样做时,您将获得控制权, 将来几乎肯定会超过1ms。但是,操作系统当然可以更频繁地进行切换 - Win32具有时间BeginPeriod调用正是出于此目的,您可以以某种方式使用它。但是有一个很好的理由不经常切换 - 效率较低。

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

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

不,这不会发生。请记住,睡眠只是一种方法,表示您希望程序睡眠多长时间。它不是关于它何时会或应该醒来的规范,也永远不会醒来。根据定义,任何具有睡眠和收益功能的系统都是多任务系统,其中必须考虑其他任务的要求,并且操作系统始终对此进行调度的最终调用。替代方案将无法可靠地工作,因为如果程序可以以某种方式要求在其选择的精确时间重新激活,则可能会使其他进程的CPU功率不足。(例如。一个程序生成一个后台线程,并让两个线程执行1ms的工作并在结束时调用sleep(1),可能会轮流占用CPU内核。因此,对于用户空间程序,睡眠(以及类似的功能)将始终是下限,而不是上限。要做得更好,需要操作系统本身允许某些应用程序几乎拥有调度,这在消费者硬件的操作系统中不是一个理想的功能(同时是工业应用程序的常见和有用的功能)。


推荐