Java 博弈循环中最优睡眠时间计算的研究

Java 博弈循环中最优睡眠时间计算的研究,java,animation,game-engine,Java,Animation,Game Engine,在编写动画和小游戏时,我逐渐了解了Thread.sleep(n)的难以置信的重要性我依靠这种方法告诉操作系统我的应用程序何时不需要任何CPU,并使用这种方法使我的程序以可预测的速度进行 我的问题是,JRE在不同的操作系统上使用不同的方法实现此功能。在基于UNIX(或受影响的)OS:es(如Ubuntu和OS X)上,底层JRE实现使用功能良好且精确的系统将CPU时间分配给不同的应用程序,从而使我的2D游戏流畅且无延迟。但是,在Windows 7和旧版Microsoft系统上,CPU时间分布的工作

在编写动画和小游戏时,我逐渐了解了
Thread.sleep(n)的难以置信的重要性我依靠这种方法告诉操作系统我的应用程序何时不需要任何CPU,并使用这种方法使我的程序以可预测的速度进行

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

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

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

经过大量查找,我发现了Andy Malakov的这篇神秘文章,这篇文章对改进我的循环非常有帮助:

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

// 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\u PRECISION
在我的Windows 7机器上,我通常将其设置为2毫秒左右,而
SPIN\u将其设置为10000纳秒左右,以获得最佳性能

经过大量的努力,这绝对是我能想到的最好的了。因此,由于我仍然关心如何提高这种睡眠方法的准确性,而且我仍然对其性能不满意,因此我想呼吁所有java游戏黑客和动画制作者为Windows平台提供更好的解决方案。我可以在Windows上使用特定于平台的方式来改进它吗?我不在乎在我的应用程序中有一点特定于平台的代码,只要大部分代码是独立于操作系统的

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


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

Thread.Sleep说你的应用不需要更多的时间。这意味着在最坏的情况下,您必须等待整个线程切片(40ms左右)

现在,在糟糕的情况下,当驱动程序或其他东西占用更多的时间时,你可能需要等待120毫秒(3*40毫秒),所以线程。睡眠不是好办法。换一种方式,比如注册一个1ms回调并开始绘制代码very X回调

(这是在windows上,我会使用多媒体工具获取那些1ms分辨率的回调)

您可以使用与关联的。这是做你想做的事情最有效的方法。但是,您应该考虑跳过帧,以防计算机延迟(您可以使用计时器代码中的另一个非阻塞互斥锁来完成)

编辑:一些伪代码来澄清

计时器代码:

While(true):
  if acquireIfPossible(mutexSkipRender):
    release(mutexSkipRender)
    release(mutexRender)
睡眠代码:

acquire(mutexSkipRender)
acquire(mutexRender)
release(mutexSkipRender)
起始值:

mutexSkipRender = 1
mutexRender = 0
编辑:更正初始化值

下面的代码在windows上运行得很好(循环速度精确到每秒50帧,精度达到毫秒)


windows上的定时功能是出了名的糟糕。这是一个很好的开始。不确定您是否在意,但也要注意,虚拟系统(当windows是来宾操作系统时)上也可能存在更严重的问题(尤其是System.nanoTime)。

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

最好的做法是,尽管有点复杂,但通常是进行一个不需要实时更新的游戏循环,而是以固定的间隔(例如每秒20次)执行逻辑更新,并尽可能渲染(如果不是全屏运行,可能通过任意短睡眠来释放CPU供其他应用使用)。通过将过去的逻辑状态缓冲为w
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();
        }
    }
}