在OpenGL中建立游戏循环的好方法


31

我目前正在学校里开始学习OpenGL,而前几天我已经开始制作一个简单的游戏(我自己,不是在学校里)。我正在使用freeglut,并使用C进行构建,因此对于我的游戏循环,我确实一直在使用传递给我的函数来glutIdleFunc一次性更新所有图形和物理特性。对于我不太在乎帧频的简单动画来说,这很好,但是由于游戏主要是基于物理的,因此我真的想(需要)确定更新速度。

因此,我的第一个尝试是让函数传递给glutIdleFuncmyIdle()),以跟踪自上次调用以来经过了多少时间,并每隔毫秒更新一次物理(以及当前的图形)。我曾经timeGetTime()这样做(通过使用<windows.h>)。这让我开始思考,使用空闲功能真的是进行游戏循环的好方法吗?

我的问题是,在OpenGL中实现游戏循环的更好方法是什么?我应该避免使用空闲功能吗?



我觉得这个问题是针对OpenGL的,是否建议对循环使用GLUT
Jeff

1
在这里,不要在大项目中使用GLUT 。不过,对于较小的服务器来说很好。
bobobobo

Answers:


29

一个简单的答案是“不”,您不想在具有某种模拟的游戏中使用glutIdleFunc回调。这样做的原因是此函数将动画和绘制代码从窗口事件处理中分离出来,但不是异步的。换句话说,接收和处理窗口事件会拖延绘制代码(或您在此回调中放置的内容),这对于交互式应用程序(交互,然后是响应)来说是完美的,但不适用于必须发展物理或游戏状态的游戏与交互或渲染时间无关。

您想完全分离输入处理,游戏状态和绘制代码。有一个简单易用的解决方案,它不直接涉及图形库(即,可移植且易于可视化)。您希望整个游戏循环产生时间并让模拟消耗产生的时间(以块为单位)。但是,关键是将模拟消耗的时间整合到动画中。

我发现的最好的解释和教程是Glenn Fiedler的“ 修复您的时间步长”

本教程内容详尽,但是,如果您没有实际的物理模拟,则可以跳过真正的积分,但是基本循环仍然可以归结为(详细的伪代码):

// The amount of time we want to simulate each step, in milliseconds
// (written as implicit frame-rate)
timeDelta = 1000/30
timeAccumulator = 0
while ( game should run )
{
  timeSimulatedThisIteration = 0
  startTime = currentTime()

  while ( timeAccumulator >= timeDelta )
  {
    stepGameState( timeDelta )
    timeAccumulator -= timeDelta
    timeSimulatedThisIteration += timeDelta
  }

  stepAnimation( timeSimulatedThisIteration )

  renderFrame() // OpenGL frame drawing code goes here

  handleUserInput()

  timeAccumulator += currentTime() - startTime 
}

通过这种方式,渲染代码,输入处理或操作系统中的停顿不会导致游戏状态落后。此方法也是可移植的,并且与图形库无关。

GLUT是一个很好的库,但是严格地是事件驱动的。您注册回调并触发主循环。您总是使用GLUT移交对主循环的控制。有一些解决方法,您也可以使用计时器等伪造外部循环,但是使用另一个库可能是一种更好(更轻松)的方法。有很多选择,以下是一些(具有良好文档和快速教程的选择):

  • GLFW使您能够内联获取输入事件(在您自己的主循环中)。
  • SDL,但是它的重点不是OpenGL。

好文章。因此,要在OpenGL中执行此操作,我不会使用glutMainLoop,而是使用我自己的?如果这样做,我是否可以使用glutMouseFunc和其他功能?我希望这是有道理的,我对这一切仍然很
Jeff

不幸的是没有。当事件队列耗尽并进行迭代时,将在glutMainLoop内部触发所有在GLUT中注册的回调。我在答案正文中添加了一些指向替代方法的链接。
charstar 2011年

1
+1用于放弃GLUT进行其他任何操作。据我所知,除了测试应用程序外,GLUT从未用于其他任何用途。
Jari Komppa

您仍然必须处理系统事件,不是吗?我的理解是,通过使用glutIdleFunc,它仅处理(在Windows中)GetMessage-TranslateMessage-DispatchMessage循环,并在无消息时调用您的函数。您还是要自己做,否则会因为操作系统的不耐烦而将应用程序标记为无响应,对吗?
Ricket

@Ricket从技术上讲,glutMainLoopEvent()通过调用已注册的回调来处理传入事件。glutMainLoop()实际上是{glutMainLoopEvent(); IdleCallback(); }这里的主要考虑因素是,重绘也是从事件循环中处理的回调。您可以使事件驱动的库表现得像时间驱动的库一样,但是Maslow的Hammer以及所有这些……
charstar

4

我认为glutIdleFunc还好;本质上,如果您不使用它,最终还是要手工完成。不管您要有一个不以固定模式调用的紧密循环,您都需要选择是将其转换为固定时间循环还是将其保留为可变时间循环并确保您所有的数学都考虑了插值时间。甚至以某种方式将它们混合在一起。

您当然可以拥有myIdle传递给的函数glutIdleFunc,然后在其中测量经过的时间,将其除以固定的时间步长,然后多次调用另一个函数。

这是不可以做的事情:

void myFunc() {
    // (calculate timeDifference here)
    int numOfTimes = timeDifference / 10;
    for(int i=0; i<numOfTimes; i++) {
        fixedTimestep();
    }
}

fixedTimestep不会每10毫秒调用一次。实际上,它的运行速度会比实时运行的速度慢,并且当问题的根源在于除以timeDifference10 时剩余的损失时,您可能最终会伪造数字以进行补偿。

相反,请执行以下操作:

long queuedMilliseconds; // static or whatever, this must persist outside of your loop

void myFunc() {
    // (calculate timeDifference here)
    queuedMilliseconds += timeDifference;
    while(queuedMilliseconds >= 10) {
        fixedTimestep();
        queuedMilliseconds -= 10;
    }
}

现在,此循环结构确保您的fixedTimestep函数每10毫秒调用一次,而不会损失任何毫秒作为余数或类似的东西。

由于操作系统具有多任务处理的性质,因此您不能依赖每10毫秒精确调用一次函数;但是上面的循环会发生得足够快,以至于希望足够接近,并且您可以假设每次调用fixedTimestep都会使您的模拟值增加10毫秒。

还请阅读此答案,尤其是答案中提供的链接。

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.