読者です 読者をやめる 読者になる 読者になる

趣味プログラマによるOSS開発日誌

趣味で作っているOSSソフトウェアの紹介や関連技術の紹介、楽曲製作、Webデザイン勉強状況を紹介します。

リプレイバグの対処法

STGやレースゲームなどにある「リプレイ機能」は、実装方法は割と簡単な方であるが、"正しく動かす"にはそれなりにきちんと考えないと、ちょっとした環境の変化でいわゆる「リプレイずれ」を引き起こす。
自作ゲーム「eriKs」を試しに、VC++のDebugビルドからReleaseビルドに変えてリプレイを実行したところ、見事にずれたので修正までの流れをここに書いておこうと思う。

リプレイずれの原因としては、以下のことが挙げられる。

1. C言語標準ライブラリで提供されている、乱数生成関数rand()をシードを与えないで使用
2. 浮動小数点計算の誤差
3. マルチスレッドによる非同期処理
4. 描画処理の中で更新処理を行っている
5. オブジェクトの初期化が行われていない

1に関しては、今回のプログラムでは問題ないと思う。
「eriKs」はC言語の標準ライブラリで提供されている、乱数生成関数rand()を使用していない。
その代わりに、自機の位置などゲーム情報を足したり掛けたりする擬似乱数を使って、ランダムに見せかけている。
従って、リプレイずれの原因にはならない。

2に関しては、正直言うとかなり怪しい。
とりあえず以下のコードを、毎フレーム呼ばれる更新関数に書いて様子を見ることにした。

__asm{
    finit    # 浮動小数演算器のリセット
}

このコードは浮動小数点演算器をリセットするアセンブリコードで、CPUに依存する。
1フレーム中に誤差が蓄積されて誤った結果が得られる可能性もあるし、別のマシンではこの記述が意味なくなる可能性もあるが、無いよりは良いだろう。
今後テストを行っていくうちにまたずれが生じたら、今度は浮動小数点を使わずに固定小数点を使うようにしたいと思う。
以上の処置を行っても、リプレイバグは依然として消えなかった。

3に関してはおそらく問題ないだろう。
ゲーム実行中にファイルの読み込みは行っていなく、ファイルの読み込みはシーンの切り替え時に行われるようになっている。
書いたコードに問題なければ、同期も取れているのでリプレイずれとは関係なさそう。
しかもフレームごとのログからは、リプレイずれが起こったのが、マルチスレッド処理を行っていないゲームの中盤で起こっていた。

4は原因としてあり得ない。
リプレイの高速再生を行いたいと思い、描画処理内ではオブジェクトの変更は行わないように、最初から設計しているからである。

残ったのは5である。
調べるまで、初期化していないことでリプレイずれを生じさせるということを、あまり意識していなかった。
この項目が気になったのは、Debugビルドでは正常に動いているのに、ReleaseビルドになるとDebugビルドで作成したリプレイがずれるということと、1回目と2回目でリプレイ結果がずれたからである。
実際DebugビルドとReleaseビルドでは、初期化を行っていない領域の値の埋め方が異なるとのこと。
しかし初期化を行っていないところは、ゲーム中に関係ないはずと思っていたが、メモリプール(「eriKs」は高速化のためにシステムのメモリ割り当て機構、いわゆるnew/mallocを使っていない。)からメモリを割り当てる際に、領域のゼロクリアを行ったところ、リプレイずれが解消した。


とりあえずリプレイずれを解決できたのはよいが、これは開発PC内で閉じているテストである。
PCが異なれば、浮動小数点演算による誤差の影響が出てくるかもしれない。


[参考記事]
http://heppoko-net.jp/blog/heppoko/2008/01/_7.html
http://d.hatena.ne.jp/ABA/20080120
http://d.hatena.ne.jp/yumesoft/20100316/1268722387
http://d.hatena.ne.jp/nokuno/20060722/1153534429