ゲームロジックスレッドとレンダリングスレッド間の同期


16

ゲームロジックとレンダリングをどのように分離しますか?ここで正確にそれを尋ねる質問が既にあるように思えますが、答えは私にとって満足のいくものではありません。

私がこれまでに理解していることから、それらを異なるスレッドに分ける点は、ブロックされているスワップバッファ呼び出しからレンダリングが最終的に返される次のvsyncを待つのではなく、ゲームロジックが次のティックですぐに実行を開始できるようにすることです。

しかし、具体的には、ゲームロジックスレッドとレンダリングスレッド間の競合状態を防ぐためにどのデータ構造が使用されているか。おそらく、レンダリングスレッドは、描画対象を特定するためにさまざまな変数にアクセスする必要がありますが、ゲームロジックがこれらの同じ変数を更新している可能性があります。

この問題を処理するための事実上の標準技術はありますか。ゲームロジックを実行するたびにレンダリングスレッドが必要とするデータをコピーするようなものです。解決策が何であれ、同期のオーバーヘッドや、すべてを単一スレッドで実行するよりも少ないものは何ですか?


1
リンクをスパム送信するのは嫌ですが、これは非常に良い読み物であり、すべての質問に答えるべきだと思います:altdevblogaday.com/2011/07/03/threading-and-your-game-loop
Roy T.


1
これらのリンクは、希望する典型的な最終結果を提供しますが、その方法については詳しく説明しません。シーングラフ全体を各フレームまたは他の何かにコピーしますか?議論はレベルが高すぎてあいまいです。
user782220

リンクは、それぞれの場合にコピーされる状態の量についてかなり明確であると思いました。例えば。(最初のリンクから)「バッチには、フレームの描画に必要なすべての情報が含まれていますが、他のゲーム状態は含まれていません。」または(2番目のリンクから)「データを共有する必要がありますが、各システムが共通のデータロケーションにアクセスして位置または方向データを取得する代わりに、各システムに独自のコピーがあります」(特に3.2.2-状態を参照)マネージャー)
DMGregory

Intelの記事を書いた人は誰でも、トップレベルのスレッドが非常に悪い考えであることを知らないようです。誰も愚かなことをしません。突然、アプリケーション全体が特殊なチャネルを介して通信する必要があり、ロックや巨大な調整された状態交換が至る所にあります。言うまでもなく、送信されたデータがいつ処理されるかわからないため、コードが何をするのかを推論するのは非常に困難です。関連するシーンデータ(ref.countedポインターとして不変、値によって変更可能)をある時点でコピーし、サブシステムで必要に応じてソートする方がはるかに簡単です。
snake5

回答:


1

私も同じことに取り組んでいます。追加の懸念は、OpenGL(および私の知る限り、OpenAL)、および他の多くのハードウェアインターフェイスが、複数のスレッドによって呼び出されることにうまく合わない状態マシンであるということです。私はそれらの振る舞いさえ定義されていないと思います、そして、LWJGL(多分JOGLのために)それはしばしば例外を投げます。

最終的には、特定のインターフェイスを実装する一連のスレッドを作成し、それらを制御オブジェクトのスタックにロードしました。そのオブジェクトは、ゲームをシャットダウンする信号を受け取ると、各スレッドを実行し、実装されたceaseOperations()メソッドを呼び出し、それらが閉じるのを待ってから自分自身を閉じます。サウンド、グラフィックス、またはその他のデータのレンダリングに関連する可能性のあるユニバーサルデータは、揮発性のオブジェクトのシーケンスに保持されるか、すべてのスレッドでユニバーサルに利用できますが、スレッドメモリには保持されません。そこにはわずかなパフォーマンスのペナルティがありますが、適切に使用すると、オーディオをあるスレッドに、グラフィックスを別のスレッドに、物理をさらに別のスレッドなどに柔軟に割り当てることができます。

そのため、原則として、すべてのOpenGL呼び出しはグラフィックススレッドを通り、すべてのOpenALはオーディオスレッドを通り、すべての入力は入力スレッドを通り、組織化制御スレッドはスレッド管理を心配する必要があります。ゲームの状態はGameStateクラスに保持されており、必要に応じてすべて確認できます。たとえば、JOALが古くなって、代わりにJavaSoundの新しいエディションを使用したいと判断した場合、Audioに別のスレッドを実装するだけです。

うまくいけば、私が言っていることを見て、このプロジェクトにはすでに数千の行があります。サンプルを一緒に試してみてほしい場合は、私に何ができるか見てみましょう。


最終的に直面する問題は、このセットアップがマルチコアマシンで特にうまくスケールしないことです。はい、ゲームには通常、オーディオなどの独自のスレッドで最適に提供される側面がありますが、ゲームループの残りの多くは、実際にはスレッドプールタスクと連係して管理できます。スレッドプールがアフィニティマスクをサポートしている場合、同じスレッドで実行されるレンダータスクを簡単にキューに入れ、スレッドスケジューラーがスレッドワークキューを管理し、必要に応じてワークスティールを実行して、マルチスレッドおよびマルチコアをサポートできます。
ナロス

1

通常、グラフィックスレンダリングパス(およびそのスケジュール、実行時期など)を処理するロジックは、別のスレッドによって処理されます。ただし、そのスレッドは、ゲームループ(およびゲーム)の開発に使用するプラットフォームによって既に実装(実行)されています。

したがって、グラフィックリフレッシュスケジュールとは無関係にゲームロジックが更新されるゲームループを取得するには、追加のスレッドを作成する必要はなく、既存のスレッドをタップしてグラフィックを更新するだけです。

これは、使用しているプラ​​ットフォームによって異なります。例えば:

  • あなたは、ほとんどのオープンGLの関連プラットフォームでそれをやっている場合(C / C ++のためのGLUTJava用JOLGAndroidのOpenGL ESのアクションを関連)彼らは通常、あなたに定期的にレンダリングスレッドによって呼び出されるメソッド/関数を与え、これを使用しますゲームループに統合できます(ゲームループの反復を、そのメソッドが呼び出されるタイミングに依存させることなく)。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のsynchronizedキーワードに注意する必要があります。ポインターを変更しなくても、属性を変更できます。これを検討するには、オブジェクトを自分でコピーするか、変更したくないオブジェクトのすべての属性でsynchronizedを呼び出します。


1
OPはゲームロジックとレンダリングを分離する必要があるだけでなく、他のスレッドが現在処理中の場所に関係なく、処理中に1つのスレッドの機能が停止することを回避する必要があるため、ここでは必ずしもmutexは答えではありませんループ。
ナロス

0

ロジック/レンダリングスレッド通信を処理するために私が一般的に見たのは、データをトリプルバッファリングすることです。これにより、レンダリングスレッドはバケット0から読み取ります。ロジックスレッドは次のフレームの入力ソースとしてバケット1を使用し、フレームデータをバケット2に書き込みます。

同期点では、次のフレームのデータがレンダースレッドに渡され、ロジックスレッドが先に進むことができるように、3つのバケットのそれぞれの意味のインデックスが交換されます。

ただし、レンダリングとロジックをそれぞれのスレッドに分割する理由は必ずしもありません。実際、ゲームループをシリアルに保ち、補間を使用して、レンダリングフレームレートをロジックステップから切り離すことができます。この種のセットアップを使用してマルチコアプロセッサを活用するには、タスクのグループで動作するスレッドプールが必要です。これらのタスクは、オブジェクトのリストを0から100まで反復するのではなく、5スレッドで20の5バケットでリストを反復するなど、単純に実行できますが、メインループを複雑にすることはありません。


0

これは古い投稿ですが、まだポップアップしているので、ここに2セントを追加したいと思いました。

UI /表示スレッドとロジックスレッドに保存される最初のデータをリストします。UIスレッドには、3Dメッシュ、テクスチャ、ライト情報、および位置/回転/方向データのコピーを含めることができます。

ゲームロジックスレッドでは、3dのゲームオブジェクトサイズ、境界プリミティブ(球、立方体)、単純化された3dメッシュデータ(詳細な衝突など)、オブジェクトの速度、回転比などの動き/動作に影響するすべての属性が必要になる場合があります。また、位置/回転/方向データ。

2つのリストを比較すると、ロジックからUIスレッドに渡す必要があるのは、位置/回転/方向データのコピーのみであることがわかります。また、このデータがどのゲームオブジェクトに属するかを判断するために、何らかの種類の相関IDが必要になる場合があります。

その方法は、使用している言語によって異なります。Scalaでは、ソフトウェアトランザクションメモリ、Java / C ++では何らかのロック/同期を使用できます。不変データが好きなので、更新ごとに新しい不変オブジェクトを返す傾向があります。これは少しメモリを浪費しますが、現代のコンピュータではそれほど大したことではありません。それでも、共有データ構造をロックする場合は、実行できます。JavaのExchangerクラスを確認してください。2つ以上のバッファーを使用すると、速度が向上します。

スレッド間でデータを共有する前に、実際に渡す必要があるデータの量を計算します。3Dスペースを分割するoctreeがあり、ロジックが10個すべてを更新する必要がある場合でも、表示している5個だけを再描画する必要がある場合でも、合計1​​0個のオブジェクトのうち5個のゲームオブジェクトを見ることができます。詳細については、このブログをご覧くださいhttp : //gameprogrammingpatterns.com/game-loop.html これは同期に関するものではありませんが、ゲームロジックが表示からどのように分離されるか、どのような課題を克服する必要があるか(FPS)を示しています。お役に立てれば、

マーク

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