Macos Cocoa中驱动主回路的正确方法

Macos Cocoa中驱动主回路的正确方法,macos,cocoa,architecture,Macos,Cocoa,Architecture,我正在编写一个目前在Windows和Mac OS X中运行的游戏。我的主要游戏循环如下所示: while(running) { ProcessOSMessages(); // Using Peek/Translate message in Win32 // and nextEventMatchingMask in Cocoa GameUpdate(); GameRender(); } 这显然简化了一点,但这就是要点。在我

我正在编写一个目前在Windows和Mac OS X中运行的游戏。我的主要游戏循环如下所示:

while(running)
{
    ProcessOSMessages(); // Using Peek/Translate message in Win32
                         // and nextEventMatchingMask in Cocoa
    GameUpdate();
    GameRender();
}
这显然简化了一点,但这就是要点。在我完全控制应用程序的Windows中,它工作得非常好。不幸的是,苹果在Cocoa应用程序中有自己的做法

当我第一次尝试用Cocoa实现我的主循环时,我不知道把它放在哪里,所以我创建了自己的
NSApplication
per。我把我的
GameFrame()
扔到了我的
run
函数中,一切正常

然而,我觉得这样做并不“正确”。我希望在苹果的生态系统中发挥良好的作用,而不是试图破解一个可行的解决方案

来自苹果的文章描述了使用
NSTimer
的老方法,以及使用
CVDisplayLink
的“新”方法。我已经连接了
CVDisplayLink
版本,但它只是感觉…奇怪。我不喜欢我的游戏被显示器驱动,而不是相反


使用
CVDisplayLink
或覆盖自己的
NSApplication
是否只有两个选项?这两种解决方案都不太合适。

我很想知道是否有人真的这么做过,但我的理解是:

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


其他解决方案包括将渲染与游戏逻辑分离,在主线程上定期运行游戏逻辑,并在
CVDisplayLink
线程上进行渲染。不过,如果你在游戏中遇到了由显示模式驱动的问题,我可能只会建议你这样做。

你不必创建自己的基于非应用程序的类,也不必使用CVDisplayLink来回避应用程序的runloop在Cocoa中对你隐藏的事实

您可以只创建一个线程,并将运行循环放在其中


尽管如此,我还是使用了CVDisplayLink。

我在这里贴了一些东西来重新提出这个问题……主要是出于可移植性。通过研究OLC Pixel游戏引擎,我发现它与do{}while循环和std::chrono一起工作,以检查帧的计时以计算fElapsed时间。下面是我为做同样的事情而编写的一些代码。它还添加了一个补偿部分,用于控制帧速率,使其不超过某个值(在本例中为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);
仍然要做的是调整计时器对象的公差。Apple Developer文档说明,如果计时器对象错过下一个窗口,它将等待下一帧时间。但是,容差允许它改变未来事件的计时,以实现更平滑的帧速率转换和更好地使用CPU电源

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


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

CVDisplayLink方法目前对我来说没有问题,只是感觉…奇怪。我知道这不是不使用它的最佳理由,我只是好奇是否有更好/更直观的解决方案。这真的是商业游戏工作室使用的吗?我想大多数商业游戏都会忽略苹果的建议,在主线程上循环,或者根本不直接使用Cocoa框架。另一方面,我认为大多数iPhone游戏使用的是
CADisplayLink
,这是等效的。我还想指出,如果你使用vsync,你的游戏已经(间接地)由显示器驱动了。讽刺的是,移植到iPhone正是这个灵感的来源,让你找到了“正确”的方法。iPhone版本确实是由显示屏驱动的,我觉得显示屏没有那么奇怪,因为它更有意义。在我的游戏中,Vsync是一个选项,但在开发过程中,我会将其关闭,以便进行真正的性能测量。
- (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];
}