游戏逻辑线程与渲染线程之间的同步


16

一个游戏逻辑和渲染如何分开?我知道这里似乎已经有问题要问,但是答案对我来说并不令人满意。

据我所知,将它们分成不同的线程的目的是使游戏逻辑可以立即开始为下一个滴答运行,而不必等待下一个vsync,在此,渲染最终从swapbuffer调用返回而一直处于阻塞状态。

但是,具体来说,使用什么数据结构来防止游戏逻辑线程和渲染线程之间的竞争状况。大概渲染线程需要访问各种变量来确定要绘制的内容,但是游戏逻辑可以更新这些相同的变量。

是否有实际的标准技术来处理此问题。就像在每次执行游戏逻辑后复制渲染线程所需的数据一样。无论采用什么解决方案,同步的开销还是比仅运行单线程的所有开销都要少?


1
我讨厌只是给链接添加垃圾邮件,但我认为这是一本很好的书,它应该回答您的所有问题:altdevblogaday.com/2011/07/03/threading-and-your-game-loop
Roy T.


1
这些链接给出了人们想要的典型最终结果,但没有详细说明如何实现。您会每帧还是其他内容复制整个场景图?讨论水平太高且含糊不清。
user782220

我认为链接对于每种情况下复制多少状态相当明确。例如。(来自第一个链接)“批处理包含绘制框架所需的所有信息,但不包含任何其他游戏状态。” 或(从第二个链接)“但是仍然需要共享数据,但是现在每个系统都具有自己的副本,而不是每个系统访问一个公共数据位置(例如,获取位置或方向数据)”(尤其是3.2.2-状态经理)
DMGregory

撰写英特尔文章的人似乎都不知道顶级线程是一个非常糟糕的主意。没有人做那些愚蠢的事。突然,整个应用程序必须通过专门的渠道进行通信,并且到处都有锁和/或巨大的协调状态交换。更不用说没有告知何时将处理发送的数据,因此很难推断代码的作用。在某一点复制相关的场景数据(不可变为引用计数的指针,可变-通过值)要容易得多,让子系统按需要对其进行分类。
snake5 2015年

Answers:


1

我一直在做同样的事情。另一个问题是OpenGL(据我所知,OpenAL)和许多其他硬件接口是有效的状态机,不能与多个线程调用相处得很好。我认为它们的行为甚至都没有定义,对于LWJGL(可能还有JOGL),它通常会引发异常。

我最终要做的是创建一个实现特定接口的线程序列,并将它们加载到控制对象的堆栈中。当该对象收到关闭游戏的信号时,它将遍历每个线程,调用已实现的stopOperations()方法,并等待它们关闭后再关闭自身。可能与呈现声音,图形或任何其他数据有关的通用数据保存在易失性对象序列中,或者对所有线程通用,但从未保存在线程内存中。那里的性能稍有下降,但是使用得当,它使我可以灵活地将音频分配给一个线程,将图形分配给另一个线程,将物理分配给另一个线程,依此类推,而无需将它们绑定到传统的(且令人恐惧的)“游戏循环”中。

因此,通常,所有OpenGL调用都通过Graphics线程,所有OpenAL通过Audio线程,所有通过Input线程的输入,以及组织控制线程需要担心的所有事情都是线程管理。游戏状态保存在GameState类中,他们都可以根据需要进行查看。如果我决定说JOAL已经过时,而我想改用JavaSound的新版本,那么我只是为Audio实现一个不同的线程。

希望您明白我在说什么,我已经在这个项目上有数千行。如果您希望我尝试将样品刮在一起,那么我会做的。


您最终将面临的问题是,这种设置在多核计算机上的扩展性不是特别好。是的,游戏的某些方面通常最好在其自己的线程中使用,例如音频,但是游戏循环的其余大部分实际上可以结合线程池任务进行串行管理。如果您的线程池支持相似性掩码,则可以轻松地将要在同一线程上执行的渲染任务排队,并让您的线程调度程序管理线程工作队列并根据需要进行窃取工作,从而为您提供多线程和多核支持。
Naros 2013年

1

通常,用于处理图形渲染过程的逻辑(以及它们的时间表,以及它们何时运行等)由单独的线程处理。但是,您用来开发游戏循环(和游戏)的平台已经实现(运行了该线程)。

因此,为了获得一个游戏循环,其中游戏逻辑独立于图形刷新时间表进行更新,您不需要创建额外的线程,您只需点击已有的线程即可进行图形更新。

这取决于您使用的平台。例如:

  • 如果您在大多数与Open GL相关的平台(用于C / C ++的GLUT用于Java的JOLG,与Android的OpenGL ES相关的Action)中进行操作,它们通常会为您提供方法/函数,该方法/函数会定期由渲染线程调用,并且可以集成到您的游戏循环中(而不必使Gameloop的迭代取决于调用该方法的时间)。对于使用C的GLUT,您可以执行以下操作:

    glutDisplayFunc(myFunctionForGraphicsDrawing);

    glutIdleFunc(myFunctionForUpdatingState);

  • 在JavaScript中,由于没有多线程(可以通过编程方式访问)因此可以使用Web Workers ,也可以使用“ requestAnimationFrame”机制来通知何时安排新的图形渲染,并相应地更新游戏状态。

基本上,您想要的是一个混合步骤的游戏循环:您有一些代码可以更新游戏状态,并且可以在游戏的主线程中调用该代码,并且还希望定期使用已经存在的代码。现有的图形渲染线程可以提醒您何时刷新图形。


0

在Java中,有一个“ synchronized”关键字,它锁定传递给它的变量以使其具有线程安全性。在C ++中,您可以使用Mutex实现相同的目的。例如:

Java:

synchronized(a){
    //code using a
}

C ++:

mutex a_mutex;

void f(){
    a_mutex.lock();
    //code using a
    a_mutex.unlock();
}

锁定变量可确保在运行其后的代码时它们不会更改,因此在渲染变量时变量不会被更新线程更改(实际上它们确实会更改,但是从渲染线程的角度来看,它们不会更改) t)。但是,您必须使用Java中的synced关键字来提防,因为它只能确保指向变量/ Object的指针不变。属性仍然可以更改而无需更改指针。为此,您可以自己复制对象,也可以对不需要更改的对象的所有属性进行同步调用。


1
互斥对象并不一定是答案,因为OP不仅需要解耦游戏逻辑和渲染,而且还希望避免某个线程在其处理中前进的能力的任何停顿,而不管另一个线程当前在处理中的位置如何环。
Naros 2013年

0

我通常看到的处理逻辑/渲染线程通信的方法是对数据进行三倍缓冲。这样,渲染线程便说出要读取的存储区0。逻辑线程使用存储区1作为下一帧的输入源,并将帧数据写入存储区2。

在同步点,将交换三个存储桶中每个存储桶的含义的索引,以便将下一帧的数据提供给渲染线程,并且逻辑线程可以继续向前。

但是,没有必要将渲染和逻辑划分为各自的线程。实际上,您可以使游戏循环保持串行状态,并使用插值将渲染帧速率与逻辑步骤解耦。要使用这种设置来利用多核处理器,您将拥有一个在任务组上运行的线程池。这些任务很简单,例如,而不是从0到100迭代对象列表,而是在5个线程(20个线程)中跨5个线程对列表进行迭代,可以有效地提高性能,但又不会使主循环复杂化。


0

这是旧帖子,但仍然弹出,因此想在这里加2美分。

应该存储在UI /显示线程与逻辑线程中的第一列表数据。在UI线程中,您可能包括3d网格,纹理,光照信息以及位置/旋转/方向数据的副本。

在游戏逻辑线程中,您可能需要3d中的游戏对象大小,边界图元(球形,立方体),简化的3d网格数据(例如,用于详细的碰撞),所有影响运动/行为的属性,例如对象速度,转弯比率等,以及位置/旋转/方向数据。

如果比较两个列表,可以看到仅需要将位置/旋转/方向数据的副本从逻辑传递到UI线程。您可能还需要某种相关ID,才能确定该数据属于哪个游戏对象。

您的操作方式取决于您使用的语言。在Scala中,您可以使用软件事务存储,而在Java / C ++中,则可以使用某种锁定/同步。我喜欢不可变数据,因此我倾向于每次更新都返回新的不可变对象。这有点内存浪费,但是对于现代计算机而言,这并不是什么大问题。不过,如果您想锁定共享数据结构,则可以执行此操作。在Java中签出Exchanger类,使用两个或多个缓冲区可以加快处理速度。

在开始在线程之间共享数据之前,请计算出实际需要传递多少数据。如果您有一个八叉树来划分您的3d空间,并且您可以看到总共10个对象中的5个游戏对象,即使您的逻辑需要更新所有10个对象,您也只需重绘所看到的5个对象。欲了解更多信息,请查看此博客:http : //gameprogrammingpatterns.com/game-loop.html 这与同步无关,但它确实显示了游戏逻辑与显示的分离方式以及需要克服的挑战(FPS)。希望这可以帮助,

标记

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.