用于插值和线程处理的数据结构?


20

我最近一直在处理游戏中的一些帧速率抖动问题,看来最好的解决方案是Glenn Fiedler(游戏开发人员)在经典的“ 修复您的时间步伐”中提出的解决方案文章。

现在-我已经在使用固定的时间步进行更新。问题是我没有进行建议的插值渲染。结果是,如果渲染速率与更新速率不匹配,我将获得双倍或跳过的帧。这些可以在视觉上引起注意。

因此,我想在游戏中添加插值法-我很想知道其他人如何构建数据和代码来支持这一点。

显然,我将需要存储(在何处/如何?)与渲染器相关的游戏状态信息的两个副本,以便可以在它们之间进行插值。

另外-这似乎是添加线程的好地方。我想像一个更新线程可以在游戏状态的第三个副本上工作,而将其他两个副本保留为渲染线程的只读状态。(这是一个好主意吗?)

看来,有游戏的状态的两个或三个版本可能会引入性能和- 更重要的是-可靠性和开发人员的生产力问题,相比于仅具有单一版本。因此,我对缓解这些问题的方法特别感兴趣。

我认为应该特别注意的是如何处理游戏状态中添加和删除对象的问题。

最后,似乎某种状态不是直接需要渲染的,还是要跟踪不同版本的状态太难了(例如,存储单个状态的第三方物理引擎)-因此,我想知道如何人们已经在这样的系统中处理了这类数据。

Answers:


4

不要尝试复制整个游戏状态。插值将是一场噩梦。只需通过渲染隔离出需要变化的部分即可(我们称其为“可视状态”)。

为每个对象类创建一个伴随类,该类将能够保存对象的视觉状态。该对象将由仿真产生,并由渲染消耗。插值将很容易插入之间。如果状态是不可变的,并按值传递,则不会有线程问题。

渲染通常不需要了解对象之间的逻辑关系,因此用于渲染的结构将是纯矢量,或至多是简单的树。

传统设计

class Actor
{
  Matrix4x3 position;
  float fuel;
  float armor;
  float stamina;
  float age;

  void Simulate(float deltaT)
  {
    age += deltaT;
    armor -= HitByAWeapon();
  }
}

使用视觉状态

class IVisualState
{
  public:
  virtual void Interpolate(const IVisualState &newVS, float f) {}
};
class Actor
{
  struct VisualState: public IVisualState
  {
    Matrix4x3 position;
    float fuel;
    float armor;
    float stamina;
    float age;

    virtual auto_ptr<IVisualState> Interpolate(const IVisualState &newVS, float f)
    {
      const VisualState &newState = static_cast<const VisualState &>(newVS);
      IVisualState *ret = new VisualState;
      ret->age = lerp(this->age,newState.age);
      // ... interpolate other properties as well, using any suitable interpolation method
      // liner, spline, slerp, whatever works best for the given property
      return ret;
    };
  };

  auto_ptr<VisualState> state_;

  void Simulate(float deltaT)
  {
    state_->age += deltaT;
    state_->armor -= HitByAWeapon();
  }
}

1
如果您不使用“ new”(C ++中的保留字)作为参数名称,那么您的示例将更易于阅读。
史蒂夫·S

3

我的解决方案远没有大多数解决方案那么优雅/复杂。我使用Box2D作为我的物理引擎,因此保持多个系统状态副本是不可管理的(克隆物理系统然后尝试使其保持同步,也许有更好的方法,但我无法提出一)。

相反,我与物理学世代相抵触。每次更新都会增加物理生成,当物理系统进行两次更新时,生成计数器也会进行两次更新。

渲染系统跟踪最后渲染的世代以及该世代以来的增量。当渲染希望对其位置进行插值的对象时,可以使用这些值以及它们的位置和速度来猜测应该在哪里渲染对象。

如果物理引擎速度太快,我没有解决该怎么办。我几乎要争辩说,您不应该为快速移动而插值。如果两者都做,则需要注意不要通过猜得太慢然后猜得太快而导致精灵跳来跳去。

当我写插值的东西时,我以60Hz运行图形,以30Hz运行物理。事实证明,Box2D以120Hz运行时更加稳定。因此,我的插值代码很少使用。通过将目标帧速率加倍,物理平均每帧更新两次。抖动也可能是1或3倍,但几乎永远不会是0或4+。较高的物理速率可以自己解决插值问题。在60hz下运行物理和帧速率时,每帧可能会获得0-2更新。与1和3相比,0和2之间的视觉差异巨大。


3
我也发现了这一点。具有接近60Hz帧更新的120Hz物理循环使插值几乎一文不值。不幸的是,这仅适用于可以承受120Hz物理循环的一组游戏。

我只是尝试切换到120Hz更新循环。这似乎具有双重好处,可以使我的物理更加稳定,并使游戏在不超过60Hz的帧频下看起来更流畅。缺点是,它破坏了我所有经过精心调整的游戏物理性-因此,这绝对是一个需要在项目早期选择的选项。
安德鲁·罗素

另外:我实际上不理解您对插补系统的解释。听起来有点像推断吗?
安德鲁·罗素

好决定。我实际上描述了一个外推系统。给定位置,速度以及自上一次物理更新以来的时间,我推断出如果物理引擎没有停止,则对象将位于何处。
deft_code 2010年

2

我听说这种建议的时间步伐很常见,但是在游戏中的10年中,我从未从事过依赖固定时间步伐和插值的现实世界项目。

与可变时间步长系统相比,似乎通常要付出更多的努力(假设帧速率的合理范围为25Hz-100Hz)。

我确实为一个非常小的原型尝试了一次固定的时间步长+插值方法-没有线程,但是固定的时间步长逻辑更新,并且在不更新时尽可能快地进行渲染。我的方法是使用一些类,例如CInterpolatedVector和CInterpolatedMatrix-它们存储先前/当前值,并具有从渲染代码中使用的访问器,以检索当前渲染时间的值(该值始终介于先前和当前之间)。当前时间)

每个游戏对象将在更新结束时将其当前状态设置为这些可插值向量/矩阵的集合。这种事情可以扩展为支持线程,您至少需要3组值-一个正在更新的值,以及至少2个先前值才能在这些值之间进行内插...

请注意,某些值不能被简单地插值(例如,“ sprite animation frame”,“ special effect active”)。您可能可以完全跳过插值,也可能会导致问题,具体取决于您的游戏需求。

恕我直言,最好只是按照可变的时间步长- 除非您要制作RTS或其他具有大量对象的游戏,并且必须使网络游戏保持2个独立的仿真同步(在游戏中仅发送订单/命令)网络,而不是对象位置)。在这种情况下,固定时间步长是唯一的选择。


1
看来至少Quake 3使用了这种方法,默认的“刻度”为20 fps(50毫秒)。
苏门答腊

有趣。我想它确实对高度竞争的多人PC游戏具有优势,可以确保更快的PC /更高的帧率不会带来太大的优势(响应能力更强的控件,或者物理/碰撞行为的微小但可利用的差异) ?
bluescrn

1
您是否在10年内没有遇到过任何与模拟和渲染器不同步的物理游戏?因为执行此操作的那一刻,您几乎必须在动画中进行插值或接受可感觉到的混乱。
Kaj

2

显然,我将需要存储(在何处/如何?)与渲染器相关的游戏状态信息的两个副本,以便可以在它们之间进行插值。

是的,值得庆幸的是,这里的键是“与我的渲染器有关”。这可能仅仅是在混合中添加旧职位和时间戳。给定2个位置,您可以将其插值到它们之间的位置,并且如果您拥有3D动画系统,则通常无论如何都只能在该精确时间点请求姿势。

确实非常简单-想象您的渲染器必须能够渲染您的游戏对象。它曾经询问对象的外观,但现在必须询问它在特定时间的外观。您只需要存储回答该问题所需的任何信息即可。

另外-这似乎是添加线程的好地方。我想象一个更新线程可以在游戏状态的第三个副本上工作,而将其他两个副本保留为渲染线程的只读状态。(这是一个好主意吗?)

这听起来像是在这一点上增加痛苦的秘诀。我还没有考虑全部问题,但我想您可能会以更高的延迟为代价获得少量额外的吞吐量。哦,使用其他核心可能会带来一些好处,但是我不知道。


1

注意,我实际上并没有考虑插值,所以这个答案无法解决。我只关心为渲染线程提供一个游戏状态副本,为更新线程提供另一个副本。因此,尽管您可以修改以下解决方案以进行插值,但我无法对插值问题发表评论。

在设计和考虑多线程引擎时,我一直对此感到疑惑。所以我问了一个关于Stack Overflow的问题,关于如何实现某种“新闻”或“事务”设计模式。我收到了一些很好的答复,被接受的答案确实让我思考。

创建一个不可变的对象很困难,因为它的所有子对象也必须是不可变的,并且您需要非常小心,以确保所有东西都是真正不可变的。但是如果您确实小心的话,您可以创建一个超类GameState,其中包含游戏中的所有数据(以及子数据等)。Model-View-Controller组织样式的“模型”部分。

然后,就像Je​​ffrey所说的,您的GameState对象的实例是快速的,内存有效的和线程安全的。最大的缺点是,为了更改有关模型的任何内容,您都需要重新创建模型,因此您需要非常小心,以免您的代码不会变成一团糟。var = val;就代码行而言,将GameState对象内的变量设置为新值要比仅仅涉及更多。

我对此非常感兴趣。您无需在每一帧都复制整个数据结构。您只需复制一个指向不变结构的指针即可。这本身就令人印象深刻,您不同意吗?


这确实是一个有趣的结构。但是我不确定它是否适合游戏-因为一般情况下是一个相当平坦的对象树,每个对象每帧更改一次。也因为动态内存分配是一个很大的禁忌。
安德鲁·罗素

在这种情况下,动态分配非常容易有效地进行。您可以使用圆形缓冲区,从一侧增大,从另一侧释放。
苏马

...这不是动态分配,而是动态使用预分配的内存;)
Kaj 2010年

1

首先,我在场景图中拥有每个节点的游戏状态的三个副本。一种是由场景图线程写入的,一种是由渲染器读取的,而第三种则可以在其中之一需要交换时用于读取/写入。这很好,但是太复杂了。

然后我意识到我只需要保留将要渲染的三个状态。现在,我的更新线程将填充“ RenderCommands”的三个小得多的缓冲区之一,并且Renderer从当前未写入的最新缓冲区中读取数据,从而防止线程彼此等待。

在我的设置中,每个RenderCommand都具有3d几何体/材质,转换矩阵以及影响其的光源列表(仍在进行正向渲染)。

我的渲染线程不再需要执行任何剔除或光照距离计算,这在大型场景中可大大加快工作速度。

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.