同じ入力をしたら、同じ出力になるようにしよう

前置き

ゲームを作っていると「再現性」というものが重要になることが多くあります。
例えば「リプレイ機能」は完全な再現性がないと困ります。
他にも「ある場所でエラーが出た」場合、状況を再現できれば再度同じエラーを起こすことができ、直すのも非常に楽になります。
そのためには「同じ入力をしたら、同じ出力になる」のがとても重要です。
具体的に考えていきましょう。

初期設定

電装天使ヴァルフォースは、1対1のバトルものです。
バトルを管理するクラスは独立しており、外部から受けつつけるのは「初期設定」「キー入力」のみとなっています。
 
「初期設定」とは具体的には何でしょうか?

  • 使用キャラクター
  • ステージ
  • ハンデ内容
  • 乱数のシード値

などがそれにあたります。
ここで重要なのは乱数のシード値です。
プログラムで使われる乱数生成関数は、シード値を同じに設定しておけば、出力される乱数内容が完全に一致するように作られています。
 
つまり、乱数のシード値が同じならば、プログラムは必ず同じ結果を再現してくれるわけです。

再現性を無くす要因

逆に、再現してくれなくなる要因を考えてみましょう。

  • 変数の初期化忘れ
  • timeGetTimeなどの、時間取得による分岐
  • シード値を設定しないで使う、乱数生成器(その場合、大抵現在時刻がシード値として利用されるため)
  • 非同期処理を目的としたマルチスレッド処理

となります。
つまり、これらを避けるのが再現性を確保するためのコツです。
特に変数の初期化忘れは厄介です。
Debugビルドでは変数内容が0xcdcdcdcdなどで初期化されます。
おかげでDebugビルド版では初期化忘れをしても再現性があるのですが、逆にReleaseビルド時に再現性がまったくとれなくなります。
とても良くハマる原因なので、変数の初期化忘れはしつこいほどチェックしましょう。

タイミング依存性の排除

説明したとおり、再現性を確保したい場合「現実の時間(timeGetTimeの返り値など)」に左右されてはいけません。
そのため、バトルクラスは

battle->update(key1P, key2P);

このようにキー入力データを受け取ると、内部的に1フレーム分処理が進むようになっています。
 
ゲームが早く動きすぎないようにタイミングをとっているのは、バトルクラスの外側の仕事です。
逆に、高速でupdateを呼ぶことにより早送りすることもできますし、逆にスローにすることも簡単です。
 
とにかくゲームプログラムにとっての「時間」は「ゲームが開始されてから何フレーム目か」で管理しましょう。
決して「現実の時間(timeGetTimeの返り値など)」に依存したゲームシステムを作ってはいけません。

やってて良かった「完全再現性」

電装天使ヴァルフォースで「斑鳩セツナがレーザーを撃つと稀に強制終了する」というバグが一時出たのをご存じでしょうか?
(実際はセツナ以外でも起こりえたのですが)
この際私は、クライアントに「強制終了しても、そこまでのリプレイファイルを出力する処理」を追加しました。
 
結果、数日後に届いた「強制終了するリプレイファイル」を元に、手元でエラーを再現。
デバッグビルドで原因を一発で突き止めたのでした。
 
もしリプレイ機能を実装する気が無かったとしても、リプレイファイル出力機能はつけておくとデバッグにとても役立ちますよ。