在Cocoa中驾驶主循环的正确方法

时间:2012-03-25 22:06:21

标签: macos cocoa architecture

我正在编写一款目前在Windows和Mac OS X上运行的游戏。我的主游戏循环如下:

while(running)
{
    ProcessOSMessages(); // Using Peek/Translate message in Win32
                         // and nextEventMatchingMask in Cocoa
    GameUpdate();
    GameRender();
}

这显然有点简化,但这就是它的要点。在我可以完全控制应用程序的Windows中,它运行良好。不幸的是,Apple在Cocoa应用程序中有自己的做事方式。

当我第一次尝试在Cocoa中实现我的主循环时,我无法弄清楚在哪里放置它,所以我按this post创建了自己的NSApplication。我将GameFrame()权限放在run函数中,一切正常。

然而,我不觉得这是“正确”的方式。我想在Apple的生态系统中很好地发挥作用,而不是试图破解有效的解决方案。

来自apple的

This article描述了使用NSTimer执行此操作的旧方法,以及使用CVDisplayLink执行此操作的“新方法”。我已经联系了CVDisplayLink版本,但感觉......很奇怪。我不喜欢我的游戏是由显示器驱动而不是相反的想法。

我只有两个选项可以使用CVDisplayLink或覆盖我自己的NSApplication吗?这些解决方案中没有一个感觉非常正确。

3 个答案:

答案 0 :(得分:2)

我很想知道是否有人真正做过这件事需要权衡,但这是我的理解:

Apple推动CVDisplayLink解决方案在使用-nextEventMatchingMask:untilDate:inMode:dequeue:的主线程上执行循环,因为我认为它为UI控件提供了更好的响应能力。这可能与全屏游戏无关。 (注意:您不需要替换NSApplication来使用这种形式的游戏循环。)我认为使用CVDisplayLink的主要潜在问题是它只会运行提前一帧,它提前做出这个决定,甚至比垂直同步更强。从好的方面来说,它可能会改善延迟。

其他解决方案包括从游戏逻辑中解耦渲染并在主线程上定期运行游戏逻辑并在CVDisplayLink线程上渲染。但是,如果你遇到游戏驱动的显示范式的问题,我可能只会推荐这个。

答案 1 :(得分:1)

您不一定要创建自己的基于NSApplication的类或使用CVDisplayLink来解决在Cocoa中隐藏应用程序的runloop这一事实。

你可以创建一个线程,然后让你的运行循环。

虽然它值得,但我只使用了CVDisplayLink。

答案 2 :(得分:0)

我在这里贴一些东西来重振这个问题......主要是出于便携性。我通过研究 OLC 像素游戏引擎发现,它与 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 的事​​件驱动模型直接冲突。 Custom main application loop in 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];
}

还需要调整定时器对象的容差。 Apple Developer 文档指出,如果计时器对象错过了下一个窗口,它将等待下一帧时间。但是,容差允许它改变未来事件的时间,以实现更平滑的帧速率转换和更好地利用 CPU 能力。

所以在这一点上,我愿意接受关于其他人为制作更可移植的代码所做的工作的建议和意见。我计划在名为“eventDriven”的引擎的构造函数中使用一个布尔参数,如果为false,将启动自己的游戏循环线程,然后拆分顶部事件循环以调用处理所有代码的“engineUpdate”方法可以是事件驱动的。然后,在基于事件驱动系统构建的情况下,委托只需使用 engineUpdate = TRUE 构建引擎,并让他们的事件驱动游戏更新。

有人做过吗?如果是这样,它如何执行跨平台?