今回はゲームプログラミングを題材にします。
皆さんは、ゲームの「リプレイ機能」をご存知でしょうか?
リプレイは、「人間が過去に行ったゲームプレイ内容を再現する機能」です。
よく見る例では、ゲームのデモプレイもリプレイ機能の一種です。
例えば、スーパーマリオブラザーズはタイトル画面で放置するとデモプレイが流れます。
これはAIによる操作ではなく、過去の人間のゲームプレイ内容を再現したものです。
今回の記事では、このリプレイ機能の仕組みを解説し、どうすれば実装できるのかを説明します。
また、最近はリアルタイムに同期される、マルチプレイヤーのオンラインゲームが一般的になってきています。
実は、マルチプレイヤーオンラインゲームもリプレイ機能と同じような仕組みで成り立っています。
というわけで、オンラインゲームの仕組みへ応用できる部分についても説明します。
まずは結論から…
長くなるので初めに結論を述べておきます。
リプレイの実装方針については、以下のフローチャートに従って考えればOKです。
リプレイ機能が満たすべき仕様を考える
何が実現できれば、「リプレイ機能」が完成するのか考えましょう。
ここでは3つの仕様を考えます。
過去の状況を再現できる
大前提として、「過去の状況が再現できること」が必要です。
言い換えると、何らかの手段で過去のプレイ内容を記録し、それをゲーム上で再現できることが求められます。
データサイズをなるべく小さくする
ここで言うデータとは、記録される過去のプレイ内容のことです。
プレイ時間が長くなっても、いたずらにデータサイズが大きくならないような仕組みが求められます。
特に、オンラインゲームでは大切になってくる仕様です。
オンラインゲームでは、プレイデータをインターネットを介して送受信します。
近年はスマートフォン用ゲームが増えており、プレイヤーは無制限に通信回線を利用できない環境にあることが多いです。
たとえば、WiMAX を利用しているなど、通信容量に制約のあるユーザーも多く存在します。
そのため、データサイズをできるだけコンパクトにすることが大切になってきます。
オンラインゲーム特有の問題について
一番初めの「デモプレイ」では発生しないのですが、オンラインゲーム特有の問題についても触れておきます。
オンラインゲームでは、プレイデータを毎フレーム記録して同期することは現実的ではありません。
リアルタイムにプレイヤー状態が同期されているように見えても、実は他プレイヤーの状態は少し遅れて反映されています。
また、毎フレーム同期が不可能であることから、厳密な同期はされておらず、多くの「ごまかし」がなされています。
(オンラインゲームの仕組みや「ごまかし」の技術についてもっと色々話したいですが、本題からずれるのでコレ以上はやめて別記事にします…)
このように、オンラインゲームでは毎フレームのプレイデータ同期が不可能であっても破綻しないような仕組みが必要になります。
実装方針を考える
上で述べた、「リプレイ機能が満たすべき仕様」は以下の通りです。
これらを踏まえて、3つの実装方針を紹介します。
いずれも、世に出ているゲームで採用実績のある手段です。
ゲームを動画として保存する
非常に愚直な方針として、ゲームをそのまま動画として保存してしまうという手が考えられます。
たとえば、Nintendo Switch の本体機能である動画保存機能はこの手法ですね。
この方針のメリットは以下のとおりです。
- 確実に過去の状況を再現できる
- どんなタイプのゲームにも通用する
確実性、応用範囲の広さという点では動画保存という手も1つだと考えられます。
そして、この方針のデメリットは以下のとおりです。
- リプレイデータのサイズが非常に大きくなる
- リプレイの検証が困難
- 二次記憶装置への頻繁なアクセスが発生する
- そもそもゲーム内で用意する必要がない
まず、リプレイが長くなるほどデータサイズはハイペースに増加していきます。
動画ですので仕方ないです。
次に、リプレイの検証が困難であるという欠点があります。
プレイヤーが何らかの不正を行っていたとしても、それが動画で確認が難しいものであれば、プレイ後の正当性評価は困難です。
そして、1フレーム辺りに必要なデータサイズが非常に大きいため、メモリにすべてを乗せるのは現実的ではありません。
そのため、SSD/HDD などの二次記憶装置への頻繁なアクセスが発生し、処理負荷がネックになってきます。
最後に、元も子もないのですが、キャプチャーソフトを使う方が処理負荷的にも実装的にも楽なので、わざわざゲーム内で用意する必要が無いです。
よって、この方針については今回は不採用とします。
プレイヤーの入力を記録する
次に、プレイヤーの毎フレームの入力を保存するという手が考えられます。
プレイヤーの動きが同じであれば、同じゲーム内容を再現できるはず、という考えに基づいています。
この方針は、リプレイの実現手段として最も多く用いられている手法です。
メリットは以下のとおりです。
- データサイズがかなり小さくなる
- リプレイの検証を行える
先ほど挙げた「動画保存」の方針と真逆のメリットがあります。
たとえば、ビット演算を上手く用いれば、8ボタンのゲームであっても1時間で僅か211KBのデータ容量で済みます(1byte * 60frame * 3600sec / 1024)。
これがどの程度の大きさかと言うと、SNS で閲覧できる画像1枚分くらいです。
また、プレイ時の入力をそのまま再現するので、不正プレイが行われたかの事後検証を行うことも可能です。
この手法のデメリットは以下のとおりです。
- 一箇所状況再現に失敗するとそれ以降全てが破綻する
- 非決定論的アルゴリズムを用いている場合は採用できない
まず、一箇所状況再現に失敗するとそれ以降全てが破綻するという大きな欠点があります。
たとえばマリオのようなアクションゲームでは、
- プレイヤーがよく分からない動きを始めて死に続ける
- 過去のプレイと獲得スコアがずれている
といった現象が発生します。
そのため、初期状態が同じで、毎フレームの入力が同じであれば確実に同じ状況が再現できるという保証が必要になります。
また、この保証のために非決定論的アルゴリズムを用いている場合は採用できない手法でもあります。
非決定論的アルゴリズムとは、この文脈ではゲームアプリ層からの同じ入力に対して異なる出力を返す可能性のあるアルゴリズムです。
もう少しわかりやすく言うと、ゲーム上で結果を完全にコントロールできないモジュールのことです。
これは、現代のマルチスレッド処理が当たり前になったゲームプログラミングでは割と致命的です。
たとえば、Unity の物理エンジンや Time.deltaTime を使ってるとこの方法は採用不可になります。
乱数は非決定論的アルゴリズム?
ここまで読んだ人の中には、「非決定論的アルゴリズムが採用不可能なら、乱数を使っているとこの手法は使えないのでは?」と思った方もいると思います。
実は、乱数は(通常)決定論的アルゴリズムで、乱数を使っている場合でもこの手法は採用できます。
コンピュータで扱う乱数は擬似乱数と呼ばれていて、ある初期値(種(seed)と呼ばれます)を乱数生成器に与えることで、乱数の生成列を同一にできる仕組みとなっています。
よって、この乱数種を同一にすることで常に同じ結果が得られることが保証できるので、決定論的アルゴリズムとなります。
オブジェクトの状態を記録する
最後に、オブジェクトの状態を記録するという手法を考えてみます。
愚直に考えると、毎フレームのプレイヤー・敵などの状態を記録しておくというものです。
この手法は、プレイヤー入力記録手法の次に、リプレイの実現手段として多く用いられている手法です。
メリットは以下のとおりです。
- 状況再現の起点を複数確保できる
- どこかで破綻が起きてもリカバリー可能
- リプレイの検証を行える
- 非決定論的アルゴリズムを用いている場合も採用可能
まず、状況再現の起点を複数確保できるという利点があります。
これは、たとえばステージ1・ステージ2…といった複数のリプレイ開始地点を用意できるということです。
次に、どこかで状況再現に失敗してもリカバリーができます。
そのために、非決定論的アルゴリズムを用いている場合も採用可能という利点が生じています。
そして、ゲーム内でのオブジェクトの状態を記録しているため、リプレイの正当性検証も可能です。
この手法のデメリットは以下のとおりです。
- 状態を記録するオブジェクトの取捨選択が必要
- オンラインゲームの場合、この手法だけでは不十分なことが多い
ゲーム内に出てくるオブジェクトは無数にあります。
そのため、全てのオブジェクトの状態を記録するとデータサイズが非常に大きくなってしまいます。
状況再現に必要な重要な情報を取捨選択する必要があります。
また、オンラインゲームの場合この手法だけでは不十分なことが多いです。
オンラインゲームは毎フレームの状態同期は現実的ではないため、この手法だけを採用すると動きがとてもカクカクして見えます。
カクカクするのは、データを受け取るタイミングが綺麗に一定間隔にならないためです。
これはインターネットを使う以上避けることのできない不安定性なので、ゲーム側でなんとか「ごまかす」必要があります。
現実的にはどのような実装になっているか?
長くなりましたが、リプレイの実装方針として3つ挙げました。
いずれも長所・短所があるわけですが、では現実的にはどのような実装になっているのか?について述べます。
ゲームジャンルによって最適な実装方法が異なる
適切なリプレイの実装方法は、ゲームジャンルによって異なってきます。
具体的には、ターン制のゲームか、リアルタイム性が求められるかで大別できます。
たとえば、カードゲーム・RPGなどターン制のゲームになってくると、ターン毎のプレイヤーの意思決定でゲームが動きます。
この場合、プレイヤー入力そのものが大切になるわけではないので、「オブジェクト状態を保存する」方針が主体となります。
また、アクションゲーム・シューティングゲームなど毎フレームのプレイヤー入力でゲームが動く場合、基本的には「プレイヤー入力を保存する」方針が主体となります。
ゲームにリアルタイム性が求められる場合、「オフラインのリプレイ機能」か、「オンラインゲームの状態同期」かでさらに実装方針を分けられます。
オフラインのリプレイ機能
オフラインの場合、毎フレームの状態保存が可能です。
よって、「プレイヤー入力を保存する」手法を主体としつつ、重要なポイントで「オブジェクト状態を保存する」を取り入れたハイブリッドな実装になります。
たとえば、ステージ1・ステージ2の初期オブジェクト状態を記録しておくことで、その地点からのリプレイ再生が可能になります。
オンラインゲームの状態同期
オンラインゲームの場合、毎フレームの状態保存は不可能です。
よって、「オブジェクト状態を保存する」手法を主体としつつ、これによって生じる「動きカクカク問題」を解消するため、補助的に「プレイヤー入力を保存する」手法を採用します。
ここでは詳しく述べませんが、相手プレイヤー座標と合わせて入力を記録し、次に状態データを受け取るまでのプレイヤー動作を予測して動かします。
また、オンラインゲームの場合「厳密にオブジェクト同期をとるべき判定を誰が行うか?」という非常に難しい問題もあります。
これはゲームの特性によって適切な解法が異なり、職人技に近い部分があります。
まとめ
今までの話を振り返って、もう一度最初のフローチャートを見てみましょう。
このフローチャートに沿えばよいのですが、プレイヤー入力の保存・オブジェクト状態の保存といった1つの手法だけ用いれば良いのではなく、どちらかを主体としてもう一方を補助的に用いるハイブリッドな実装が現実的な方針です。
リプレイ機能は、何も入力がなくてもゲームが自動で進行していくので、実装できると結構楽しいです。
ぜひチャレンジしてみてください。
コメント