我正在编写一款当前可以在 Windows 和 Mac OS X 上运行的游戏。我的主游戏循环如下所示:
while(running)
{
ProcessOSMessages(); // Using Peek/Translate message in Win32
// and nextEventMatchingMask in Cocoa
GameUpdate();
GameRender();
}
这显然有点简化了,但这就是要点。在我可以完全控制应用程序的 Windows 中,它运行得很好。不幸的是,Apple 在 Cocoa 应用程序中有自己的行事方式。
当我第一次尝试在 Cocoa 中实现我的主循环时,我不知道该把它放在哪里,所以我根据
这篇文章创建了自己的
NSApplication
。我将 GameFrame()
直接放入 run
函数中,一切正常。
但是,我觉得这不是“正确”的做法。我想在苹果的生态系统中很好地发挥作用,而不是试图破解一个有效的解决方案。
苹果的这篇文章描述了使用 NSTimer
的旧方法,以及使用
CVDisplayLink
的“新”方法。我已经连接了 CVDisplayLink
版本,但感觉......很奇怪。我不喜欢我的游戏由显示器驱动,而不是相反。我只有两个选择使用
CVDisplayLink
或覆盖我自己的
NSApplication
吗?这些解决方案都感觉不太正确。Apple 推出
CVDisplayLink
解决方案,而不是在使用
-nextEventMatchingMask:untilDate:inMode:dequeue:
的主线程上进行循环,因为我认为,它为 UI 控件提供了更好的响应能力。这可能与全屏游戏无关。 (注意:您不需要替换 NSApplication
来使用这种形式的游戏循环。)我 认为使用
CVDisplayLink
的主要潜在问题是它只会提前运行一帧,并且它会进行此确定早,比垂直同步还要强。从好的方面来说,它可能会改善延迟。其他解决方案包括将渲染与游戏逻辑解耦,以及在主线程上定期运行游戏逻辑并在
CVDisplayLink
线程上渲染。但是,如果您遇到游戏驱动显示范例的问题,我可能只会推荐这样做。
您可以创建一个线程并将运行循环放在那里。
尽管如此,我只是使用 CVDisplayLink。
我在这里贴了一些东西来重新提出这个问题......主要是出于可移植性。我通过研究 OLC Pixel Game Engine 发现,它与 do{}while 循环和 std::chrono 配合使用来检查帧的计时以计算 fElapsed Time。下面是我编写的一些代码来完成同样的事情。它还添加了化妆部分,以控制拍摄帧速率不超过特定值,在本例中为 60 FPS。
c++代码
int maxSpeedMicros = 16700;
float fTimingBelt; //used to calculate fElapsedTime for internal calls.
std::chrono::steady_clock::time_point timingBelt[2];
bool engineRunning = false; //always have it true, until the engine stops.
bool isPaused = false;
do {
timingBelt[1] = std::chrono::steady_clock::now();
fTimingBelt = std::chrono::duration_cast<std::chrono::microseconds>(timingBelt[1] - timingBelt[0]).count() * 0.000001;
if (isPaused) {
do {
std::this_thread::sleep_for (std::chrono::milliseconds(100));
timingBelt[1] = std::chrono::steady_clock::now();
} while (isPaused);
}
timingBelt[0] = std::chrono::steady_clock::now();
// do updating stuff here.
timingBelt[1] = std::chrono::steady_clock::now();
int frameMakeup = std::chrono::duration_cast<std::chrono::microseconds>(timingBelt[1] - timingBelt[0]).count();
if (frameMakeup < maxSpeedMicros) {
int micros = maxSpeedMicros - frameMakeup;
std::this_thread::sleep_for (std::chrono::microseconds(micros));
}
} while (engineRunning);
但是,该代码与 Cocoa 的事件驱动模型直接冲突。cocoa 中的自定义主应用程序循环
因此,作为创可贴,我注释掉了整个循环,并创建了一个运行循环的一次迭代的新方法。然后我在我的 AppDelegate 中实现了这个: 目标C代码
- (void)applicationDidFinishLaunching:(NSNotification *)notification {
engine->resetTimer();
[NSTimer scheduledTimerWithTimeInterval:0.016666666667 target:self selector:@selector(engineLoop) userInfo:nil repeats:YES];
}
-(void) engineLoop { //Let's handle this by the engine object. That's too complicated!
engine->updateState();
[glView update]; //Since the engine is doing all of its drawing to a GLView
[[glView openGLContext] flushBuffer];
}
还需要调整计时器对象的容差。苹果开发者文档指出,如果计时器对象错过了下一个窗口,它将等待下一帧时间。然而,容差允许它改变未来事件的时间,以实现更平滑的帧速率转换并更好地利用 CPU 功率。
因此,在这一点上,我愿意接受有关其他人为制作更可移植的代码所做的工作的建议和意见。我计划在名为“eventDriven”的引擎构造函数中使用一个布尔参数,如果为 false,将启动自己的游戏循环线程,然后分离出顶部事件循环以调用一个“engineUpdate”方法来处理所有代码可以是事件驱动的。然后,在构建事件驱动系统的情况下,委托人只需使用 engineUpdate = TRUE 构建引擎,并让其事件驱动 gameUpdate。
有人做过吗?如果是的话,它是如何跨平台执行的?