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

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

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

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

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

经过大量的观察,我找到了安迪·马拉科夫的这篇神秘文章,这对改善我的循环很有帮助: 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,以便在我的Windows 7机器上获得最佳性能。

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

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

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

您可以使用与互斥锁关联的循环计时器 。 这是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(); } } } 

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

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

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

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

时间的东西在Windows上是出了名的糟糕。 这篇文章是一个很好的起点。 不确定您是否关心,但也注意到虚拟系统上可能存在更严重的问题(特别是使用System.nanoTime)(当Windows是客户操作系统时)。

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

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

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

Thread.sleep是不准确的,并且大多数时候动画都会抖动。

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

Java游戏开发论坛已经讨论了这个问题多年。