補間とスレッド化のデータ構造?


20

私は最近、ゲームでフレームレートのジッターの問題に対処していますが、最適な解決策は、クラシックなFix Your Timestepで Glenn Fiedler(ゲームのGaffer)によって提案されたものだと思われます!記事。

今-私はすでに更新のために固定タイムステップを使用しています。問題は、レンダリングのために提案された補間を行っていないことです。その結果、レンダリングレートが更新レートと一致しない場合、フレームが2倍またはスキップされます。これらは視覚的に顕著です。

それで、ゲームに補間を追加したいと思います-そして、これをサポートするために他の人がどのようにデータとコードを構造化したか知りたいです。

明らかに、レンダラーに関連するゲーム状態情報の2つのコピーを(どこで/どのように)保存する必要があります。

さらに-これはスレッドを追加するのに適した場所のようです。更新スレッドはゲーム状態の3番目のコピーで動作し、他の2つのコピーをレンダリングスレッドの読み取り専用のままにしておくことができると思います。(これはいい考えですか?)

-ゲームの状態の二、三のバージョンを持つことは、パフォーマンスを紹介してできているようですはるかにもっと重要なこと-信頼性と開発者の生産性の問題を、単に1つのバージョンを持つに比べて。ですから、私はこれらの問題を軽減する方法に特に興味があります。

特に注意すべきなのは、ゲームの状態に対するオブジェクトの追加と削除の処理方法の問題です。

最後に、いくつかの状態はレンダリングに直接必要ではないか、異なるバージョン(たとえば、単一の状態を保存するサードパーティの物理エンジン)を追跡するのが難しすぎるようです-そのため、どのように知りたい人々はそのようなシステム内でそのようなデータを処理しました。

回答:


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を使用しているため、システム状態の複数のコピーを維持することは管理できません(物理システムを複製してから同期を維持しようとすると、より良い方法があるかもしれませんが、思い付くことができませんでした1)。

代わりに、物理生成の実行中のカウンターを保持します。物理システムが二重に更新されると、生成カウンターも二重に更新されます。

レンダリングシステムは、最後にレンダリングされた世代と、その世代以降のデルタを追跡します。位置を補間したいオブジェクトをレンダリングする場合、これらの値を位置と速度とともに使用して、オブジェクトをレンダリングする場所を推測できます。

物理エンジンが速すぎる場合の対処方法は説明しませんでした。私はあなたが速い動きのために補間すべきではないとほぼ主張します。両方を行った場合、推測が遅すぎて推測が速すぎてスプライトが飛び回らないように注意する必要があります。

補間データを書いたとき、私はグラフィックスを60Hzで、物理学を30Hzで実行していました。120Hzで動作する場合、Box2Dははるかに安定していることがわかります。このため、私の補間コードはほとんど使用されません。ターゲットフレームレートを2倍にすることにより、平均更新時の物理をフレームごとに2回更新します。ジッタも1倍または3倍になる可能性がありますが、0または4+になることはほとんどありません。より高い物理レートは、補間問題をそれ自体で修正します。物理演算とフレームレートの両方を60hzで実行すると、フレームごとに0〜2の更新が得られる場合があります。0と2の視覚的な違いは、1と3に比べて大きいです。


3
私もこれを見つけました。60Hz近くのフレーム更新を伴う120Hzの物理ループにより、補間はほとんど価値がありません。残念ながら、これは120Hzの物理ループを処理できるゲームのセットでのみ機能します。

120Hzの更新ループに切り替えてみました。これには、物理​​学をより安定させ、60Hz以外のフレームレートでゲームをスムーズに見せることの2つの利点があるようです。欠点は、慎重に調整されたゲームプレイの物理をすべて壊すことです。したがって、これは間違いなくプロジェクトの早い段階で選択する必要があるオプションです。
アンドリューラッセル

また、私は実際にあなたの補間システムの説明を理解していません。実際、外挿のように聞こえますか?
アンドリューラッセル

良い電話。実際に外挿システムについて説明しました。位置、速度、および最後の物理更新からの時間を考えると、物理エンジンがストールしていなかった場合のオブジェクトの位置を推定します。
deft_code

2

タイムステップに対するこのアプローチは非常に頻繁に提案されると聞きましたが、ゲームの10年間で、固定のタイムステップと補間に依存する実世界のプロジェクトに取り組んだことがありません。

一般に、可変タイムステップシステム(25Hz〜100Hzの範囲の適切なフレームレートの範囲を想定)よりも多くの努力が必要です。

非常に小さなプロトタイプに対して、固定タイムステップ+補間アプローチを1回試しました。スレッドはありませんが、固定タイムステップロジックの更新、および更新しない場合は可能な限り高速のレンダリングです。私のアプローチは、CInterpolatedVectorやCInterpolatedMatrixなどのいくつかのクラスを持つことでした-以前/現在の値を保存し、レンダリングコードから使用されるアクセサーを使用して、現在のレンダリング時間の値を取得しました(常に前と現在の時間)

各ゲームオブジェクトは、更新の最後に、現在の状態をこれらの補間可能なベクトル/行列のセットに設定します。この種のことは、スレッド化をサポートするために拡張することができます。少なくとも3セットの値が必要です-1つは更新されていました。

一部の値は簡単に補間できないことに注意してください(例:「スプライトアニメーションフレーム」、「特殊効果がアクティブ」)。ゲームのニーズに応じて、補間を完全にスキップすることも、問題が発生することもあります。

私見、可変タイムステップに行くのが最善です- あなたがRTS、またはあなたが膨大な数のオブジェクトを持っている他のゲームを作成していて、ネットワークゲームのために2つの独立したシミュレーションを同期させなければならない場合(オーダー/コマンドのみを送信するオブジェクトの位置ではなくネットワーク)。その場合、固定タイムステップが唯一のオプションです。


1
少なくともQuake 3はこのアプローチを使用していたようで、デフォルトの「ティック」は20 fps(50ミリ秒)です。
須磨

面白い。競争の激しいマルチプレイヤーPCゲームには利点があり、高速なPC /高フレームレートがあまり利点にならないようにしていると思います(応答性の高いコントロール、または物理的/衝突の振る舞いにおける小さなが悪用可能な違い) ?
bluescrn

1
10年以内に、シミュレーションとレンダラーとのロックステップではなく、物理学を実行したゲームに出くわしませんでしたか?あなたがそうする瞬間、あなたはアニメーションで知覚されたジャーキネスを補間するか、受け入れる必要があるだろうからです。
カイ

2

明らかに、レンダラーに関連するゲーム状態情報の2つのコピーを(どこで/どのように)保存する必要があります。

はい、ありがたいことに、ここで重要なのは「レンダラーに関連する」です。これは、古いポジションとそのタイムスタンプをミックスに追加するだけの場合があります。2つの位置を指定すると、それらの間の位置に補間できます。3Dアニメーションシステムがある場合は、通常、とにかくその正確な時点でポーズを要求できます。

とても簡単です-レンダラーがゲームオブジェクトをレンダリングできる必要があると想像してください。以前はオブジェクトにどのように見えるかを尋ねていましたが、今では特定の時間にどのように見えるかを尋ねなければなりません。その質問に答えるために必要な情報を保存するだけです。

さらに-これはスレッドを追加するのに適した場所のようです。更新スレッドはゲーム状態の3番目のコピーで動作し、他の2つのコピーをレンダリングスレッドの読み取り専用のままにしておくことができると思います。(これはいい考えですか?)

この時点で追加の痛みのレシピのように聞こえます。全体の意味を熟考していませんが、待ち時間が長くなる代わりに少しの余分なスループットが得られる可能性があると推測しています。ああ、あなたは別のコアを使用できることからいくつかの利点を得るかもしれませんが、私は知らない。


1

注:実際に補間を検討しているわけではないので、この回答では対処しません。私は、レンダリングスレッド用にゲーム状態のコピーを1つ、更新スレッド用にもう1つコピーすることに関心があります。したがって、補間の問題についてはコメントできませんが、次のソリューションを変更して補間することはできます。

マルチスレッドエンジンを設計し、考えているので、私はこれについて疑問に思っていました。そこで、スタックオーバーフローについて、ある種の「ジャーナリング」または「トランザクション」デザインパターンを実装する方法について質問しました。。私はいくつかの良い反応を得ました、そして、受け入れられた答えは本当に私に考えさせられました。

すべての子も不変でなければならないため、不変オブジェクトを作成するのは困難です。また、すべてが本当に不変であることに本当に注意する必要があります。しかし、本当に注意GameStateを払えば、ゲーム内のすべてのデータ(およびサブデータなど)を含むスーパークラスを作成できます。Model-View-Controller組織スタイルの「モデル」部分。

その後、ジェフリーが言うように、GameStateオブジェクトのインスタンスは高速で、メモリ効率が良く、スレッドセーフです。大きな欠点は、モデルについて何かを変更するために、モデルを再作成する必要があるということです。そのため、コードが巨大な混乱にならないように注意する必要があります。GameStateオブジェクト内の変数を新しい値に設定することはvar = val;、コードの行の観点から見ると、単なるよりも複雑です。

私はそれに非常に興味をそそられます。フレームごとにデータ構造全体をコピーする必要はありません。不変構造へのポインタをコピーするだけです。それ自体が非常に印象的です、あなたは同意しませんか?


それは実に興味深い構造です。ただし、ゲームに適しているかどうかはわかりません。一般的なケースは、フレームごとに正確に変化するオブジェクトのかなり平らなツリーです。また、動的なメモリ割り当ては非常に重要です。
アンドリューラッセル

このような場合の動的割り当ては、非常に簡単に効率的に実行できます。循環バッファを使用し、一方から拡大し、もう一方から解放できます。
須磨

...それは動的な割り当てではなく、事前に割り当てられたメモリを動的に使用するだけです;)
カイ

1

まず、シーングラフに各ノードのゲーム状態の3つのコピーを作成しました。1つはシーングラフスレッドによって書き込まれ、1つはレンダラーによって読み取られ、3つ目はそのうちの1つがスワップする必要があるとすぐに読み取り/書き込みに使用できます。これはうまくいきましたが、複雑すぎました。

その後、レンダリングされるものの3つの状態を保持するだけでよいことに気付きました。私の更新スレッドは、「RenderCommands」の3つの非常に小さいバッファーの1つを満たし、レンダラーは現在書き込まれていない最新のバッファーから読み取ります。これにより、スレッドは互いに待機しなくなります。

私の設定では、各RenderCommandには3Dジオメトリ/マテリアル、変換マトリックス、およびそれに影響するライトのリストがあります(まだフォワードレンダリングを実行しています)。

私のレンダースレッドは、カリングやライト距離の計算を行う必要がなくなりました。これにより、大きなシーンではかなり高速になりました。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.