今作っているゲームで、ボスキャラクターが数回に一回くらいの割合で移動しなくなる、というバグが起きたことがありました。こういう再現しにくいバグの原因究明は大変で、このときは1時間は費やしてしまいました。今回はそのBevyの落とし穴について触れたいと思います。
システムの実行順序
以下のサンプルプログラムを考えます。
use bevy::prelude::*;
fn a() {
info!("a");
}
fn b() {
info!("b");
}
fn main() {
let mut app = App::new();
app.add_plugins(DefaultPlugins);
app.add_systems(Update, (a, b));
app.run();
}
aと出力するだけのシステムa
と、bと出力するだけのシステムb
が定義されており、それらが同じUpdate
スケジュールに登録されています。つまり1フレームごとに、aという文字列を出力する、bという文字列を出力する、というだけのコードです。このプログラムを実行すると、abababab……のようにaとbが交互に出力されるかと思いきや、babaababbaab……のようにたびたび順序が入れ替わって出力されます。この結果は実行するたびにランダムに変わります。実は、システムa
とシステムb
は別々のスレッドで実行されています。
Bevyはデフォルトでマルチスレッディングが有効になっています。先ほどの単純なソースコードからはマルチスレッドプログラミングの気配はまったく感じませんが、Bevyはあれを自動的に別々のスレッドに割り当てて並列処理します。このため、同じスケジュールに登録されたシステムの実行順序はデフォルトでは不定です。上のような簡単なプログラムでも、簡単に結果が不定になってしまうのです。
冒頭て述べたボスキャラクターが動かなくなるバグの原因を探ってみると、ボスキャラクターの剛体に移動のために外力を設定したのですが、別のシステムでも外力を設定するコードが書かれており、それらのシステムの実行順序が不定だったため、ボスが動いたり動かなかったりしたのでした。そちらの古いシステムのコードはすっかり存在を忘れていて、新しく書いたシステムとバッティングしてしまったというわけです。
システムの順序を指定する
ではシステムの実行順序を指定するにはどうすればいいかというと、before
やafter
のようなメソッドを使います。以下のようにa.before(b)
のように書くと、b
はa
が完了したあとに実行を開始するようにスケジュールされるので、abababababababab...のように必ずaとbが交互に出力されるようになります。
app.add_systems(Update, (a.before(b), b));
もっとも、こうするとa
とb
は並列に実行されません。すべてのシステムに順序を指定してしまうとせっかくのマルチスレッディングが台無しになってしまうので、必要な部分だけ順序を指定します。
ほかの対策としては、a
とb
の両方のシステムから同じデータへ個別に書きこむのをやめて、a
とb
を統合するという手もあります。標準出力という全体で共通のリソースに対して別々のシステムから書きこむから結果が不定になってしまうわけで、標準出力に書きこむシステムをひとつにしてしまえばいいわけです。でも、本当にシステムを統合できるかはケースバイケースです。また、Bevyではシステムは分割していったほうがコードがすっきりするので、統合する方法が適しているケースはあまり多くないと思います。
さらにほかの対策としてはマルチスレッディングの機能自体を無効にする手もあるのですが、ゲーム開発において実行速度は重要であり、マルチスレッディングはそのための重要な道具ではありますので、頑張って付き合っていくのがいい気がしてます。
なお、システムセットという機能もあるようですが、チートブックを読んでもどういう使い方をすればいいのか私にはよくわかりませんでした。複数のシステムをまとめて実行順序を指定できるという機能のようですが、どちらかというとサードパーティのプラグインで使う機能かもしれません。私の作っているゲームでは、唯一物理エンジン関係のシステムのスケジュールでこのシステムセットを使っていますが、正直よく理解できていないです。だれか日本語で解説書いてください!!!
他のゲームエンジンとの比較
私は他のゲームエンジンでのマルチスレッディングについては知らないのですが、たとえばGodotのマルチスレッディングやUnityのマルチスレッディングのコードを見てみると、マルチスレッディングで動作するように書き方を工夫する必要があり、追加でかなり多くのコードを書かなければならないようです。またミューテックスのようなマルチスレッド特有の概念も取り扱う必要が出てきます。デッドロックのような厄介なバグにも直面するかもしれません。
それに対し、Bevyでマルチスレッディングを行うのに、特別な書き方はほとんど必要ありません。せいぜいbefore
やafter
で必要に応じて実行順序を指定するだけです。
Bevyで書くと、アプリケーションが自然に小さなシステム群へと分解されます。そうやってできた小さなタスク群はそれぞれの独立性が高いので、マルチスレッディングにより別々のスレッドで実行するのに適しています。そしてBevyが自動的にそれらのシステム、それらのタスクを上手にスケジューリングして並列実行してくれます。 これはほかのゲームエンジンでは到底真似のできない、Bevyの大きな強みといえると思います。
もちろんこの記事で述べたように、自動的なスケジューリングがうまくいくのは、それらのシステムが完全に独立して実行できる場合だけです!どちらも同じデータに書きこむなどで依存関係があって独立していない場合は、ちゃんと実行順序を指定しないと不安定な結果になることがあります。これはBevyの諸刃の剣、敵も味方も吹き飛ばすBevyの地雷原です!気を付けましょう!