同じ入力をしたら、同じ出力になるようにしよう
前置き
ゲームを作っていると「再現性」というものが重要になることが多くあります。
例えば「リプレイ機能」は完全な再現性がないと困ります。
他にも「ある場所でエラーが出た」場合、状況を再現できれば再度同じエラーを起こすことができ、直すのも非常に楽になります。
そのためには「同じ入力をしたら、同じ出力になる」のがとても重要です。
具体的に考えていきましょう。
初期設定
電装天使ヴァルフォースは、1対1のバトルものです。
バトルを管理するクラスは独立しており、外部から受けつつけるのは「初期設定」「キー入力」のみとなっています。
「初期設定」とは具体的には何でしょうか?
- 使用キャラクター
- ステージ
- ハンデ内容
- 乱数のシード値
などがそれにあたります。
ここで重要なのは乱数のシード値です。
プログラムで使われる乱数生成関数は、シード値を同じに設定しておけば、出力される乱数内容が完全に一致するように作られています。
つまり、乱数のシード値が同じならば、プログラムは必ず同じ結果を再現してくれるわけです。
再現性を無くす要因
逆に、再現してくれなくなる要因を考えてみましょう。
- 変数の初期化忘れ
- timeGetTimeなどの、時間取得による分岐
- シード値を設定しないで使う、乱数生成器(その場合、大抵現在時刻がシード値として利用されるため)
- 非同期処理を目的としたマルチスレッド処理
となります。
つまり、これらを避けるのが再現性を確保するためのコツです。
特に変数の初期化忘れは厄介です。
Debugビルドでは変数内容が0xcdcdcdcdなどで初期化されます。
おかげでDebugビルド版では初期化忘れをしても再現性があるのですが、逆にReleaseビルド時に再現性がまったくとれなくなります。
とても良くハマる原因なので、変数の初期化忘れはしつこいほどチェックしましょう。
タイミング依存性の排除
説明したとおり、再現性を確保したい場合「現実の時間(timeGetTimeの返り値など)」に左右されてはいけません。
そのため、バトルクラスは
battle->update(key1P, key2P);
このようにキー入力データを受け取ると、内部的に1フレーム分処理が進むようになっています。
ゲームが早く動きすぎないようにタイミングをとっているのは、バトルクラスの外側の仕事です。
逆に、高速でupdateを呼ぶことにより早送りすることもできますし、逆にスローにすることも簡単です。
とにかくゲームプログラムにとっての「時間」は「ゲームが開始されてから何フレーム目か」で管理しましょう。
決して「現実の時間(timeGetTimeの返り値など)」に依存したゲームシステムを作ってはいけません。