Entity System Architecture with Task Based Parallelismの使用


9

バックグラウンド

私は自分の暇な時間にマルチスレッドゲームエンジンの作成に取り組んでおり、エンティティシステムを既に作成したものに組み込むための最良の方法を決定しようとしています。これまでのところ、私は自分のエンジンの出発点としてIntelからのこの記事を使用しました。これまでのところ、タスクを使用して通常のゲームループを実装しており、システムやエンティティシステム、あるいはその両方の組み込みに取り掛かっています。私は過去にアルテミスに似たものを使ったことがありますが、並行性が私を捨ててしまいます。

Intelの記事では、エンティティデータのコピーが複数あり、各エンティティに加えられた変更が完全な更新の最後に内部的に配布されることが推奨されているようです。つまり、レンダリングは常に1フレーム遅れますが、得られるべきパフォーマンス上の利点を考えると、許容できる妥協案のようです。ただし、Artemisのようなエンティティシステムの場合、システムごとに各エンティティが複製されるため、各コンポーネントも複製する必要があります。これは実行可能ですが、多くのメモリを消費するように思えます。これを議論するインテルのドキュメントの部分は主に2.2と3.2.2です。探しているアーキテクチャを統合するための適切なリファレンスが見つかるかどうかを確認するためにいくつか検索を行いましたが、まだ有用なものを見つけることができていません。

注:このプロジェクトではC ++ 11を使用していますが、私が求めていることのほとんどは言語にとらわれないものでなければならないことを想像しています。

可能な解決策

EntitiesとEntityAttributesの作成と管理に使用されるグローバルEntityManagerを用意します。更新フェーズでのみ読み取りアクセスを許可し、すべての変更をスレッドごとにキューに保存します。すべてのタスクが完了すると、キューが結合され、それぞれの変更が適用されます。これは、同じフィールドへの複数の書き込みで問題が発生する可能性がありますが、それを整理する優先システムまたはタイムスタンプがあると確信しています。変更の配布段階でシステムにエンティティへの変更をかなり自然に通知できるため、これは私にとっては良いアプローチのようです。

質問

それが理にかなっているかどうかを確認するために私の解決策についてのフィードバックを探しています。私は嘘をついてマルチスレッドの専門家であると主張することはありません。私はこれを主に練習のために行っています。複数のシステムが複数の値を読み書きしている私のソリューションからいくつかの複雑な混乱が発生することを予測できます。私が言及した変更キューも、PODを使用していないときに起こりうる変更を簡単に通知できるようにフォーマットするのが難しい場合があります。

フィードバック/アドバイスは大歓迎です!ありがとう!

リンク集

回答:


12

フォークジョイン

コンポーネントの個別のコピーは必要ありません。Intelの記事で(非常に不十分に)言及されているfork-joinモデルを使用するだけです。

ECSでは、実質的に次のようなループがあります。

while in game:
  for each system:
    for each component in system:
      update component

これを次のように変更します。

while in game:
  for each system:
    divide components into groups
    for each group:
      start thread (
        for each component in group:
          update component
      )
    wait for all threads to finish

トリッキーな部分は、「コンポーネントをグループに分割する」ビットです。グラフィックの場合、共有データはほとんど必要ないため、シンプルです(レンダリング可能なオブジェクトを使用可能なワーカースレッドの数で均等に分割します)。物理学とAIの場合、相互作用しないオブジェクトの論理的な「島」を見つけて、それらを組み合わせる必要があります。コンポーネント間の相互作用が少ないほど、優れています。

存在しなければならない対話では、遅延メッセージが最も効果的に機能します。オブジェクトAがオブジェクトBに損傷を与えるように指示する必要がある場合、Aはメッセージをスレッドごとのプールにエンキューするだけです。スレッドが結合されると、プールはすべて1つのプールに連結されます。スレッド化とは直接関係ありませんが、BitSquid開発者による一連のイベントのイベントを参照してください(実際、ブログ全体を読んでください。そこにあるすべてに同意するわけではありませんが、素晴らしいリソースです)。

「フォークジョイン」という注意ません使用することを意味fork()(スレッド、プロセスを作成するではない)、またそれは、あなたが実際のスレッドに参加しなければならないことを意味するものではありません。これは、1つのタスクを実行し、ワーカースレッドのプールで処理できるように細かく分割し、すべてのパーセルが処理されるのを待つことを意味します。

プロキシ

このアプローチは、単独で使用することも、fork-joinメソッドと組み合わせて使用​​して、厳密な分離の必要性をそれほど重要でないものにすることもできます。

単純な2層のアプローチを使用することで、スレッドの相互作用をより親しみやすくすることができます。「権限のある」エンティティと「プロキシ」エンティティがあります。権限のあるエンティティは、権限のあるエンティティの明確な所有者である単一のスレッドからのみ変更できます。プロキシエンティティは変更できません。読み取りのみです。ゲームループの同期ポイントで、権限のあるエンティティから対応するプロキシにすべての変更を伝達します。

「エンティティ」を「コンポーネント」に適宜置き換えます。要点は、オブジェクトのコピーを最大2つ必要とすることであり、ほとんどの正気なスレッドゲームエンジンの設計では、1つのオブジェクトから別のオブジェクトにコピーできるときに、ゲームループに明確な「同期」ポイントがあります。

プロキシを拡張して、メソッド/メッセージ(のサブセット)を、許可されたオブジェクトの次のフレームに配信されるキューに転送するだけで、メソッド/メッセージを使用できるようにすることができます。

プロキシアプローチは、ネットワークサポートを非常に簡単にするため、より高いレベルで使用できる素晴らしい設計です。


前述のフォークジョインについていくつか読んだことがあり、並列処理を利用できる一方で、一部のワーカースレッドが1つのグループの終了を待機している場合があるという印象を受けました。理想的には、そのような状況を回避しようとしています。プロキシのアイデアは興味深く、私が取り組んでいたものと少し似ています。エンティティにはEntityAttributesがあり、それらはエンティティによって格納された実際の値のラッパーです。したがって、値はいつでもそれらから読み取ることができますが、特定の時間にのみ設定され、属性にプロキシ値を含めることができますよね?
ロス・ヘイズ

1
待機を回避しようとすると、依存関係グラフの分析に多くの時間を費やして、全体的に時間を失う可能性があります。
Patrick Hughes

@roflha:はい、プロキシをEntityAttributeレベルに配置できます。または、2番目の属性セットを使用して別のエンティティを作成します。または、属性の概念を完全に削除し、より粒度の低いコンポーネント設計を使用します。
Sean Middleditch 2013

@SeanMiddleditch私が属性と言うとき、私は本質的に私が思うコンポーネントを参照しています。属性は、浮動小数点数や文字列のような単一の値ではなく、それが私がそれをそのように聞こえるようにした場合です。むしろ、PositionAttributeなどの特定の情報を含むクラスです。コンポーネントがその受け入れられる名前である場合は、おそらく変更する必要があります。しかし、コンポーネント/属性レベルではなくエンティティレベルでプロキシすることをお勧めしますか?
ロスヘイズ2013

1
実装が最も簡単な方法をお勧めします。ロックを取得せずに、アトミックを使用せずに、デッドロックなしでプロキシにクエリを実行できるようになることを覚えておいてください。
Sean Middleditch 2013
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.