极度困惑“恒定游戏速度最大FPS”游戏循环


12

我最近在Game Loops上阅读了这篇文章:http : //www.koonsolo.com/news/dewitters-gameloop/

推荐的最后一种实现方式使我深感困惑。我不明白它是如何工作的,看起来像是一团糟。

我了解以下原则:以恒定的速度更新游戏,剩下的一切将使游戏尽可能多地渲染。

我认为您不能使用:

  • 获取输入的25个报价
  • 渲染游戏975次

方法,因为您将在第二部分的第一部分得到输入,这会感觉很奇怪吗?还是那是文章中正在发生的事情?


实质上:

while( GetTickCount() > next_game_tick && loops < MAX_FRAMESKIP)

那怎么有效呢?

让我们假设他的价值观。

MAX_FRAMESKIP = 5

让我们假设next_game_tick,它是在初始化之后的瞬间分配的,在说主游戏循环之前... 500。

最后,由于我在游戏中使用SDL和OpenGL,并且仅将OpenGL用于渲染,因此假设GetTickCount()返回自调用SDL_Init以来的时间。

SDL_GetTicks -- Get the number of milliseconds since the SDL library initialization.

资料来源:http : //www.libsdl.org/docs/html/sdlgetticks.html

作者还假设:

DWORD next_game_tick = GetTickCount();
// GetTickCount() returns the current number of milliseconds
// that have elapsed since the system was started

如果我们扩展该while语句,则会得到:

while( ( 750 > 500 ) && ( 0 < 5 ) )

750,因为已next_game_tick分配时间已过去。loops如您在文章中所见,它为零。

现在我们进入了while循环,让我们做一些逻辑并接受一些输入。

Yadayadayada。

在while循环结束时,我提醒您,我们的主要游戏循环是:

next_game_tick += SKIP_TICKS;
loops++;

让我们更新while代码的下一次迭代的样子

while( ( 1000 > 540 ) && ( 1 < 5 ) )

1000,因为在到达下一个调用GetTickCount()的循环的实例之前,获取输入和执行操作已经过去了。

540,因为在代码1000/25 = 40中,因此,500 + 40 = 540

1因为我们的循环已经迭代了一次

5,你知道为什么。


因此,由于显然需要这个While循环,MAX_FRAMESKIP而不是预期TICKS_PER_SECOND = 25;的,游戏应该如何正确运行?

毫不奇怪,当我将其实现到我的代码中时,我可以正确添加,因为我只是简单地重命名了功能以处理用户输入并将游戏绘制为文章作者在其示例代码中所具有的功能,所以游戏什么也没做。

我放置了一个fprintf( stderr, "Test\n" );while循环,直到游戏结束才打印。

如何保证游戏循环每秒运行25次,同时尽可能快地渲染?

对我而言,除非我缺少巨大的东西,否则看起来……什么都没有。

而且,这个while循环的结构不是应该每秒运行25次,然后完全按照我在本文开头提到的内容更新游戏吗?

如果是这样,我们为什么不能做一些简单的事情:

while( loops < 25 )
{
    getInput();
    performLogic();

    loops++;
}

drawGame();

并以其他方式计算插值。

原谅我的冗长问题,但是这篇文章对我来说弊大于利。我现在非常困惑-由于所有这些出现的问题,也不知道如何实现适当的游戏循环。


1
最好将Rant指向文章的作者。您的客观问题是哪一部分?
Anko

3
有人解释说,这个游戏循环是否有效?根据我的测试,它的结构不正确,无法每秒运行25次。向我解释为什么会这样。同样,这也不是问题,这是一系列问题。我必须使用表情符号,我看起来生气吗?
tsujp

2
由于您的问题可以归结为“我对这个游戏循环不了解什么”,而且您有很多粗体字,因此至少会激怒。
Kirbinator

@Kirbinator我很感激,但我试图将本文中发现的所有不寻常之处作为基础,因此这不是一个空洞而空洞的问题。无论如何,我不认为这是典型的,我想认为我有一些正确的观点-毕竟这是一篇试图教书,但做得不好的文章。
tsujp

7
不要误会我的意思,这是一个很好的问题,它可能要短80%。
Kirbinator

Answers:


8

我认为作者犯了一个小错误:

while( GetTickCount() > next_game_tick && loops < MAX_FRAMESKIP)

应该

while( GetTickCount() < next_game_tick && loops < MAX_FRAMESKIP)

那就是:只要还没来得及绘制下一个帧,并且我们还没有跳过MAX_FRAMESKIP这样的帧,我们就应该等待。

我也不明白他为什么next_game_tick在循环中更新,我认为这是另一个错误。由于在帧的开始处,您可以确定何时应该下一帧(使用固定帧频时)。在next game tick不取决于我们有多少时间更新和渲染后遗留下来的。

作者还犯了另一个常见错误

剩下的东西会尽可能多地渲染游戏。

这意味着多次渲染同一帧。作者甚至意识到:

游戏将以每秒稳定的50次更新,并且渲染速度尽可能快。请注意,当每秒执行渲染超过50次时,某些后续帧将相同,因此实际视觉帧将以每秒最多50帧的速度显示。

这只会导致GPU进行不必要的工作,并且如果渲染花费的时间比预期的长,则可能导致您比预期的时间晚开始下一帧的工作,因此最好屈服于OS并等待。


+投票。嗯 这的确使我不知道该怎么办。感谢您的见解。我可能会玩一玩,问题实际上是有关限制FPS或动态设置FPS的知识,以及如何做到这一点。在我看来,固定输入处理是必需的,因此游戏对所有人的运行速度均相同。这只是一个简单的2D Platformer MMO(从长远来看)
tsujp

虽然循环看起来是正确的,但为此增加了next_game_tick。它可以使仿真在越来越慢的硬件上以恒定的速度运行。仅在渲染代码中进行插值处理以使其对快速对象等更为平滑时,才“尽可能快地”运行渲染代码(或无论如何都比物理方法快)才有意义(但是,这只是浪费能量,如果它呈现的效果远远超过任何输出设备(屏幕)能够显示的内容。
Dotti

所以他的代码才是有效的实现。我们只需要忍受这种浪费吗?还是有我可以寻找的方法。
tsujp

如果您对操作系统何时将控制权交还给您有所了解,那么屈服于操作系统并等待是一个好主意-可能不是您想要的那样。
Kylotan

我不能投票赞成这个答案。他完全没有插值的内容,而是使答案的后半部分全部无效。
AlbeyAmakiir

4

如果我简化一下,也许是最好的:

while( game_is_running ) {

    current = GetTickCount();
    while(current > next_game_tick) {
        update_game();

        next_game_tick += SKIP_TICKS;
    }
    display_game();
}

whilemainloop内部的loop用于从任何地方运行到现在应该运行的模拟步骤。update_game()函数应始终假定SKIP_TICKS自上次调用以来仅经过了一段时间。这将使游戏物理在慢速和快速硬件上以恒定的速度运行。

next_game_tickSKIP_TICKS移动量的增加而增加,使其更接近当前时间。当它大于当前时间时,它将中断(current > next_game_tick),并且mainloop继续渲染当前帧。

渲染后,下一次调用GetTickCount()将返回新的当前时间。如果此时间大于此时间,则next_game_tick意味着我们已经落后于1-N步,并且应该追赶,以相同的恒定速度运行模拟中的每一步。在这种情况下,如果它较低,它将仅再次渲染同一帧(除非存在插值)。

如果我们离得太远(MAX_FRAMESKIP),原始代码会限制循环的次数。这只会使它实际上显示一些东西,而不是看起来像被锁定,例如,从暂停或游戏在调试器中暂停了很长时间(假设GetTickCount()在这段时间内没有停止),直到它赶上了时间。

如果您未在内部使用插值display_game(),则要摆脱无用的同一帧渲染,可以将其包装在if语句中,例如:

while (game_is_running) {
    current = GetTickCount();
    if (current > next_game_tick) {
        while(current > next_game_tick) {
            update_game();

            next_game_tick += SKIP_TICKS;
        }
    display_game();
    }
    else {
    // could even sleep here
    }
}

这也是关于此的好文章:http : //gafferongames.com/game-physics/fix-your-timestep/

另外,也许fprintf游戏结束时输出的原因可能只是没有刷新。

对不起我的英语。


4

他的代码看起来完全有效。

考虑while最后一组的循环:

// JS / pseudocode
var current_time = function () { return Date.now(); }, // in ms
    framerate = 1000/30, // 30fps
    next_frame = current_time(),

    max_updates_per_draw = 5,

    iterations;

while (game_running) {

    iterations = 0;

    while (current_time() > next_frame && iterations < max_updates_per_draw) {
        update_game(); // input, physics, audio, etc

        next_frame += framerate;
        iterations += 1;
    }

    draw();
}

我有一个适当的系统,上面写着“在游戏运行时,请检查当前时间-如果它大于我们正在运行的帧速率计数,并且我们已跳过绘制少于5帧的操作,那么跳过绘制并只是更新输入并物理:否则绘制场景并开始下一个更新迭代”

每次更新发生时,您将“ next_frame”时间增加理想帧率。然后,您再次检查您的时间。如果现在的当前时间少于应该更新next_frame的时间,则跳过更新并绘制所得到的内容。

如果您的current_time更大(可以想象最后的绘制过程花费了很长时间,因为某个地方有一些小问题,或者是托管语言中的一堆垃圾回收,或者是C ++中的托管内存实现,等等),那么跳过绘制,并next_frame使用另外一帧额外的时间更新更新,直到更新赶上了我们应该在时钟上的位置,或者我们跳过了绘制足够我们绝对必须绘制一个帧的帧,以便玩家可以看到他们在做什么。

如果您的计算机超快或游戏非常简单,则current_time可能会不那么next_frame频繁,这意味着您在这些时间点不会更新。

这就是插值的地方。同样,您可以在循环外部声明一个单独的布尔值,以声明“脏”空间。

在更新循环中,您将设置dirty = true,表示您实际上已经执行了更新。

然后,draw()您不会说,而是说:

if (is_dirty) {
    draw(); 
    is_dirty = false;
}

然后您会错过任何平滑运动的插值方法,但要确保仅在实际发生更新时才进行更新(而不是状态之间的插值)。

如果您很勇敢,则有一篇名为“固定时间步长”的文章。由GafferOnGames提供。
它对问题的处理方式略有不同,但是我认为它是一种更漂亮的解决方案,几乎可以完成相同的事情(取决于您的语言的功能以及您对游戏的物理计算的关心程度)。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.