ビデオゲームの構造
この記事は、メインループがどのように実行されるべきかという点に焦点を当て、技術的な観点から一般的なビデオゲームの構造とワークフローについて解説します。これにより、現行のゲーム開発を始めたばかりの初心者が、ゲーム制作で要求される要素や、JavaScript などのウェブ標準がどのようにツールとして活用できるかを理解する助けとなります。経験豊富なゲームプログラマーだが、ウェブ開発が初めてという方にとっても役立つでしょう。
表示し、受け入れ、解釈し、計算し、繰り返す
あらゆるビデオゲームのゴールはユーザーに状況を表示して入力を受け入れてこの信号を解釈して行動し、行動から得られる新しい状況を計算することです。ゲームはこうした段階を常に何度も何度も繰り返して、何かの条件(例えば勝利や敗退や、寝るために終了すること)が起きるまで繰り返します。当然のことながら、このパターンはゲームエンジンのプログラミング方法と一致しています。
詳細な仕様はゲームに依存します。
ゲームによっては、このサイクルをユーザー入力によって駆動しているものもあります。例えば、「2 枚のよく似た絵のまちがい探し」という種のゲームを開発していると想像してみてください。このようなゲームでは、ユーザーに2枚の画像が表示され、ユーザーのクリック(またはタッチ)が受け入れられ、その入力が「成功」「失敗」「一時停止」「メニュー操作」などとして解釈され、最後にその入力に基づいて更新されたシーンが計算されます。ゲームループはユーザーの入力によって進行し、指定された次の入力が提供されるまで待機します。これは、フレームごとの絶え間ない更新を必要とせず、プレイヤーが反応したときのみ更新を行う、いわばターン制の手法と言えます。
一方、その他のゲームでは、可能な限り小さな個々のタイムスライスを一つひとつ制御することが求められます。ここでも上記とほぼ同じ原則が適用されますが、少し異なる点があります。アニメーションの各フレームごとにサイクルが進み、ユーザー入力による変更は、まず処理ターンで捕捉されます。この 1 フレームごとというモデルは、メインループと呼ばれるもので実装されています。もしゲームが時間に基づいてループする場合、これがシミュレーションが遵守すべき基準となります。
しかしフレーム単位の管理は不要かもしれません。ゲームループは、「まちがい探し」の例のように、入力イベントに基づいて動作することがあります。あるいは、入力とシミュレーション時間の両方が要求される場合もあります。あるいは、まったく別の要素に基づいてループするかもしれません。
次の節で説明するように、現行の JavaScript では、幸いなことに、1 フレームにつき 1 回実行される効率的なメインループを開発することが以前より容易になっています。もちろん、ゲームの最適化度は開発者次第です。もし、ある処理がより頻度の低いイベントに紐づけるべきと思われる場合は、その処理をメインループから切り離すのがよい考えであることが多いでしょう(ただし、常にそうとは限りません)。
JavaScript でメインループを作る
JavaScript は、イベントやコールバック関数と組み合わせることで最も効果を発揮します。現行ブラウザーは、メソッドを必要なタイミングで即座に呼び出し、その合間はアイドル状態になる(あるいはそれ以外のタスクを実行する)よう設計されています。コードを、その実行に適したタイミングに紐付けることはよい考えです。関数を本当に厳密な間隔時間で呼び出す必要があるのか、フレームごとに呼び出す必要があるのか、それとも何か他のことが発生した後にのみ呼び出すべきなのかを考えてみてください。関数をいつ呼び出すべきかをブラウザーに具体的に伝えることで、ブラウザーはその呼び出しタイミングを最適化することができる。また、おそらく開発作業も楽になるでしょう。
一部のコードはフレームごとに実行する必要があるため、その関数をブラウザーの再描画スケジュール以外にも関連付ける必要はあるのでしょうか?ウェブでは、window.requestAnimationFrame() が、適切に実装されたフレーム単位のメインループの基盤となります。コールバック関数はそれ自身が呼び出されたときそこに渡されなければなりません。そのコールバック関数は次の再描画までの適切な時間で実行されます。単純なメインループの例を見てみましょう。
window.main = () => {
window.requestAnimationFrame(main);
// メインループで必要なあらゆること
};
main(); // メインループ開始
メモ:
ここで説明している main() 関数はそれぞれ、ループの中身を実行する前に新しい requestAnimationFrame をスケジュールしています。これは間違っているのではなく、ベストプラクティスとみなされています。次の requestAnimationFrame を早めに呼び出すことで、たとえ現在のフレームが VSync ウィンドウに間に合わなくても、ブラウザーがそれを時間通りに受け取り、それに応じて処理を計画できるようになります。
上記のコードブロックには、2 つの文があります。1 つ目の文では、main() という名前の関数をグローバル変数として生成しています。この関数は処理を実行すると同時に、window.requestAnimationFrame() を使用して、次のフレームで自身を呼び出すようブラウザに指示します。2 つ目の文では、1 つ目の文で定義された main() 関数を呼び出しています。main() は 2 つ目の文で 1 回呼び出され、その呼び出しのたびに次のフレームで実行されるタスクのキューに自身を追加するため、main() はフレームレートに同期されます。
もちろん、このループは完璧というわけではありません。これを改善する方法について考える前に、まず、このループがすでにうまく機能している点について見ていきましょう。
メインループの実行タイミングを、ブラウザーが画面に描画を行うタイミングに合わせることで、ブラウザーが描画を行う頻度と同じ頻度でループを実行できるようになります。これにより、アニメーションのそれぞれのフレームを細かく制御できるようになります。また、ループされる関数が main() だけであるため、実装も非常に簡単です。一人称視点のシューティングゲーム(や類似のゲーム)では、1 フレームごとに新しいシーンが表示されます。これ以上にスムーズで反応の良いものはないでしょう。
しかし、アニメーションは必ずフレームごとの制御が要求されるとすぐに思い込まないでください。CSS アニメーションやブラウザーに組み込まれたその他ツールを使えば、単純なアニメーションは、GPU アクセラレーションを活用してでも簡単に実現できます。こうしたツールは数多く存在し、作業を格段に楽にしてくれるでしょう。
Javascript でより良いメインループを作る
前回の実行時に使用したメインループには、2 つの明らかな問題が挙げられます。1 つ目は、main() が window オブジェクト(すべてのグローバル変数が格納されている場所)を汚染してしまうことです。2 つ目は、サンプルコードでは、タブ全体を閉じたり再読み込みしたりしない限り、ループを停止する方法が提供されていなかったことです。1 つ目の問題については、メインループを単に実行させたいだけで、そこに直接アクセスする必要がない場合は、即時実行関数式 (IIFE) として作成することができます。
/*
* セミコロンで始めているのは、この例の直前のコード行で自動セミコロン挿入 (ASI) が行われていた場合に備えているためです。
* ブラウザーが、この例全体が前回の行の続きであると誤って判断してしまう可能性があるからです。
* 前回の行が空行でなかったり、終了していなかったりする場合、先頭のセミコロンは新しい行の始まりを示します。
*/
;(() => {
function main() {
window.requestAnimationFrame(main);
// メインループの中身
}
main(); // ループを開始する
})();
ブラウザーがこの IIFE を検出すると、メインループが定義され、直ちに次のフレーム用にキューに登録されます。このループはどのオブジェクトにも関連付けられず、main(メソッドの場合は main())は、アプリケーションの他の部分では有効な未使用の名前として扱われるため、別の名前として自由に定義することができます。
メモ:
実際は、次の requestAnimationFrame() を if 構文で防ぐほうがより一般的で、cancelAnimationFrame() を呼ぶことはそれよりも一般的ではありません。
2 つ目の問題であるメインループの停止については、window.cancelAnimationFrame() を使用して main() の呼び出しをキャンセルする必要があります。その際、cancelAnimationFrame() には、以前の requestAnimationFrame() の呼び出し時に指定された ID トークンを引数として渡す必要があります。ゲームの関数や変数が、MyGame という名前空間に基づいて構築されていると想定しましょう。前回の例を展開すると、メインループは次のようになります。
/*
* セミコロンで始めているのは、この例の直前のコード行で自動セミコロン挿入 (ASI) が行われていた場合に備えているためです。
* ブラウザーが、この例全体が前回の行の続きであると誤って判断してしまう可能性があるからです。
* 前回の行が空行でなかったり、終了していなかったりする場合、先頭のセミコロンは新しい行の始まりを示します。
*
* MyGame は前もって定義されているものとします。
*/
;(() => {
function main() {
MyGame.stopMain = window.requestAnimationFrame(main);
// メインループの中身
}
main(); // ループ開始
})();
MyGame 名前空間に宣言された変数があり、それを stopMain と名付けます。それはメインループで最後に requestAnimationFrame() を呼び出した時に返された ID を保持します。メインループは、ブラウザーにこのトークンと関連するリクエストをキャンセルすることで、いつでも止めることができます。
window.cancelAnimationFrame(MyGame.stopMain);
JavaScript でメインループをプログラミングする際の鍵は、アクションを駆動させるべきイベントにそれを添付し、関与するさまざまなシステムの相互作用に注意を払うことです。複数の異なる種類のイベントによって駆動される要素が複数ある場合もあります。これは不必要な複雑さのように感じられるかもしれませんが、単に良く最適化されている可能性もあります(もちろん、必ずしもそうとは限りませんが)。問題は、典型的ないわゆる「メインループ」をプログラミングしているわけではないということです。JavaScript で、ブラウザーのメインループを使用しており、それを効果的に活用しようとしているのです。
JavaScript でもっと最適化されたメインループを作る
結局のところ、JavaScript では、ブラウザーが独自のメインループを実行しており、自分のコードはそのループのいくつかの段階で実行されます。上記の節では、ブラウザーから制御権を奪わないメインループについて記述しています。これらのメインメソッドは window.requestAnimationFrame() に紐付けられており、このメソッドはブラウザーに対して、次のフレームでの制御権をリクエストします。これらのリクエストをメインループにどのように関連付けるかは、ブラウザー次第です。実は、HTML 仕様書では、ブラウザーが requestAnimationFrame のコールバックをいつ実行しなければならないかについて、厳密に定義していません。これは、ブラウザーベンダーが最善と考える解決策を自由に試し、時間経過に伴う微調整が可能であるという点で、良いことであるといえます。
Firefox と Google Chrome (およびおそらく他のブラウザー)の最近のバーションでは、フレームのタイムスライスの初めで requestAnimationFrame コールバックをメインスレッドに接続しようとします。ブラウザーのメインスレッドはしたがって次のように見せようとします。
- 新しいフレームを始めます(前のフレームをディスプレイが処理中である間に)。
requestAnimationFrameコールバックのリストを通ってそれらを呼び出します。- 上記のコールバックがメインスレッドの制御をやめた時、ガベージコレクターを実行して、フレームごとのタスクを実行します。
- モニターが画像を表示できる状態になるまで(イベントによってブラウザーの「仮眠」が中断されない限り)待機し(VSync)、この処理を繰り返す。
リアルタイムアプリケーションの開発では、処理する際に時間の「予算」があると考えることもできます。上のすべてのステップは実行を 60Hz のディスプレイの描画に追いつくように 16.5ms 以内に終わらせなければなりません。ブラウザーは、コードに最大限の計算時間を確保するため、可能な限り早い段階でコードを実行します。メインスレッドは、多くの場合、メインスレッド上には存在しない処理(WebGL におけるラスタライズやシェーダーなど)を開始します。ブラウザーがメインスレッドを使用してガベージコレクションやその他のタスクを管理したり、非同期イベントを処理したりしている間も、ウェブワーカーや GPU 上で長時間の計算を並行して実行することができます。
時間の予算管理について触れたついでに、多くのウェブブラウザーには高精度タイマー (High Resolution Time) と呼ばれるツールが搭載されています。Dateオブジェクトは、精度がとても低く、システムクロックによって変更される可能性があるため、イベントのタイミング測定における標準的な手法として使用できなくなりました。一方、高精度タイマーは、navigationStart(前の文書がアンロードされた時点)からの経過ミリ秒数をカウントします。この値は、1000 分の 1 ミリ秒単位の正確さを持つ実数として返されます。これは DOMHighResTimeStamp として知られていますが、実質的には浮動小数点数として考えてください。
メモ: マイクロ秒単位の精度を実現できないシステム(ハードウェアまたはソフトウェア)については、少なくともミリ秒単位の精度を提供することが可能です。ただし、可能であれば、0.001ms の精度を提供するでしょう。
この値は、あまり重要ではないイベントを基準としているため、単独ではあまり有用ではありませんが、別のタイムスタンプから差し引くことで、その 2 つの時点の間でどれだけの時間が経過したかを、正確かつ精密に算出することができます。このようなタイムスタンプを取得するには、window.performance.now() を呼び出し、その結果を変数に格納します。
const tNow = window.performance.now();
メインループの話に戻りましょう。main 関数がいつ呼び出されたかを知りたいことがよくあるでしょう。これはよくあることであるため、window.requestAnimationFrame() は、コールバックが実行される際に、常に引数として DOMHighResTimeStamp を提供します。これにより、前回の手法にさらなる改良が加わることになります。
/*
* セミコロンで始めているのは、この例の直前のコード行で自動セミコロン挿入 (ASI) が行われていた場合に備えているためです。
* ブラウザーが、この例全体が前回の行の続きであると誤って判断してしまう可能性があるからです。
* 前回の行が空行でなかったり、終了していなかったりする場合、先頭のセミコロンは新しい行の始まりを示します。
*
* MyGame は前もって定義されているものとします。
*/
;(() => {
function main(tFrame) {
MyGame.stopMain = window.requestAnimationFrame(main);
// メインループの中身
// tFrame は "function main(tFrame)"から来て、rAF によって提供され、ここで DOMHighResTimeStamp になります
}
main(); // ループ開始
})();
いくつか他に最適化することもできますが、ゲームが何を達成しようとしているかによって全然違います。ゲームのジャンルによる違いはありますが、それ以上に微妙な違いさえあるかもしれません。キャンバスに 1 ピクセルごとに描画することもできますし、DOM 要素を(複数の WebGL キャンバスで透明な背景を持ったものとかでもやろうと思えば)複雑な階層へとレイヤー化することもできます。それぞれの道程に一つずつの条件と制約があるでしょう。
さあ決断の時は...来たれり
メインループに関しては難しい決断をしなくてはなりません。どんな決断かというとどうやって正確な時間の経過をシミュレーションしようかということです。もしフレームごとの制御が必要なら、どれくらいの頻度でゲームが更新され、描画されるかを決定しなければなりません。ひょっとしたら更新と描画を別の頻度でしたいとさえ考えるかもしれません。またもう一つ考えないといけないのは、ゲームがユーザーのシステムがゲームの仕事量に及ばない場合にどうやったらいい感じに失敗するだろうかということです。こういう場合を考えてみましょう - ユーザーの入力を処理してゲームの状態を描画するたびに更新するとします。以下に展開していきましょう。
メモ: メインループの時間処理の仕方を変更するのは、どこであってもデバッグの悪夢となります。メインループに作業する前に、自分のニーズを慎重に検討してください。
ほとんどのブラウザーゲームがなるべき姿
対応ハードウェアの最大リフレッシュレートにゲームが追従することが可能であれば、作業はかなり簡単です。更新とレンダリングを行った後、VSync が動作するまで何もする必要はありません。
/*
* セミコロンで始めているのは、この例の直前のコード行で自動セミコロン挿入 (ASI) が行われていた場合に備えているためです。
* ブラウザーが、この例全体が前回の行の続きであると誤って判断してしまう可能性があるからです。
* 前回の行が空行でなかったり、終了していなかったりする場合、先頭のセミコロンは新しい行の始まりを示します。
*
* MyGame は前もって定義されているものとします。
*/
;(() => {
function main(tFrame) {
MyGame.stopMain = window.requestAnimationFrame(main);
update(tFrame); // update メソッドを呼び出す。この場合は rAF のタイムスタンプを渡す。
render();
}
main(); // ループ開始
})();
もし最大リフレッシュレートに対応できないのであれば、品質設定を調整して決められた時間内に実行できるようにします。これのもっとも有名な例は id Software の RAGE です。このゲームでは、計算時間をおよそ 16ms(つまりおよそ 60fps)に抑えるため、ユーザーからの操作を制限していました。計算に時間がかかりすぎると、レンダリング解像度が低下したり、テクスチャやその他のゲーム資産が読み込まれなかったり、描画されなかったりするなどの問題が発生するからです。この(ウェブ以外での)ケーススタディでは、いくつかの前提条件とトレードオフが設けられました。
- アニメーションのそれぞれのフレームで、ユーザーの入力が反映されます。
- それぞれの描画には固有の更新内容があるため、フレームを補間(推測)する必要はありません。
- シミュレーションシステムでは、基本的に、それぞれのフル更新の間隔は 16ms 以下であると仮定できます。
- ユーザーに品質設定の制御権を与えると、悪夢のようになるでしょう。
- モニターの入力は 30 FPS, 75 FPS, 100 FPS, 120 FPS, 144 FPS, など、様々なレートで行われます。
- 60 FPS を維持できないシステムでは、ゲームを最適な速度で実行する際に画質が低下します(画質が低くなりすぎると、最終的には完全に実行できなくなります)。
様々なリフレッシュレートを制御するためにできる他の方法
他にも問題を追跡するための方法が存在します。
一般的な手法の一つは、シミュレーションを一定の頻度で更新し、実際のフレームを可能な限り多く(あるいは少なく)描画することです。更新メソッドは、ユーザーに何が表示されているかを気にせずにループを続けることができます。描画メソッドは、最新の更新内容とその現れる時刻を確認できます。描画側は、どの時点を表しているか、および前回の更新時のシミュレーション時刻を把握しているため、ユーザーに表示する妥当なフレームを予測することができます。これが公式の更新ループよりも頻繁であるか(あるいはそれよりも少ない頻度であるか)は問題ではありません。更新メソッドはチェックポイントを設定し、システムが許す限り頻繁に、レンダリングメソッドはそれらのチェックポイント周辺の時点を描画します。ウェブ標準において、更新メソッドを別個のメソッドとして扱う方法はいくつもあります。
-
requestAnimationFrame()で描画し、setInterval()またはsetTimeout()で更新する。- これはフォーカスされていないときや最小化されている時でも処理時間を使い、メインスレッドを占有して、ひょっとしたら昔のゲームのループの遺物なのかもしれません(が、簡単です)
-
requestAnimationFrame()で描画しウェブワーカー のsetInterval()からsetTimeout()で更新する。- 上述のものと同じですが、メインスレッドを占有しない (または、メインスレッドがこれ自体を占有しない) 点で違います。これはより複雑な解決策であり、単純な更新に対してはオーバーヘッドが大きすぎるかもしれません。
-
requestAnimationFrame()を活用し、これを使用して、更新メソッドが含まれているウェブワーカーのみに、計算に必要となるティック数を(ある場合)通知します。- これは
requestAnimationFrame()が呼び出されるまで待機し、メインスレッドを妨げないうえ、旧来の手法に頼ることにもなりません。ただし、これも前述の 2 つの方法よりもやや複雑であり、それぞれの更新の開始は、ブラウザーが rAF コールバックを発生させるまでブロックされてしまいます。
- これは
これらの方法はそれぞれ似たようなトレードオフがあります。
- ユーザーはフレームのレンダリングをスキップするか、もしくはパフォーマンスによっては臨時の 1 フレームを補完することができます。
- 一時的な不具合を除けば、すべてのユーザーが外観に直接影響しない変数を一定の頻度更新する可能性があります。
- 今まで見てきたような基本的なループよりもプログラムがずっと複雑になります。
- ユーザーの入力が次の更新まで完全に無視されます(たとえユーザーが高速な端末を持っていても)。
- 補間処理を必須とすると、パフォーマンスの低下を招きます。
更新と描画をばらばらにした例は次のようになるはずです。デモンストレーションのために、この例は三番目の項目をもとにしていますが、読みやすくするためにウェブワーカーを使わないようにしています(あと、正直に言うと、書きやすくするためでもあるんですけどね)。
警告: この例は特に技術的なレビューが必要です。
/*
* セミコロンで始めているのは、この例の直前のコード行で自動セミコロン挿入 (ASI) が行われていた場合に備えているためです。
* ブラウザーが、この例全体が前回の行の続きであると誤って判断してしまう可能性があるからです。
* 前回の行が空行でなかったり、終了していなかったりする場合、先頭のセミコロンは新しい行の始まりを示します。
*
* MyGame は前もって定義されているものとします。
*
* MyGame.lastRender は、最後に指定された requestAnimationFrame のタイムスタンプを追跡します。
* MyGame.lastTick は、最終更新時刻を追跡しています。常に tickLength だけ増加します。
* MyGame.tickLength は、ゲームの状態が更新される頻度を表します。ここでは 20Hz (50ms) に設定されています。
*
* timeSinceTick は、requestAnimationFrame のコールバックと前回の更新との間の時間を表します。
* numTicks は、これら 2 つのレンダリングフレームの間に現れるべきだった更新の回数です。
*
* render() には tFrame が渡されます。これは、render メソッドが、外挿処理(高速な端末では
* あくまで見た目の調整用)のために、直前に渡された更新ティックからどれだけの時間が経過したかを
* 計算されることを想定しているためです。このメソッドはシーンを描画します。
*
* update() は、指定された時点におけるゲームの状態を計算されます。この関数は常に
* tickLength だけ時間を増加する必要があります。これはゲームの状態に関する権限を持つ関数です。
* この関数には、その時点を表す DOMHighResTimeStamp が引数として渡されます(繰り返しになりますが、
* 一時停止機能などが追加されていない限り、これは常に前回の更新時刻 + MyGame.tickLength となります)。
*
* setInitialState() メインループが実行される前に、残っているタスクをすべて実行します。
* これは、皆さんが追加したかもしれない、ごく一般的な例の関数に過ぎません。
*/
;(() => {
function main(tFrame) {
MyGame.stopMain = window.requestAnimationFrame(main);
const nextTick = MyGame.lastTick + MyGame.tickLength;
let numTicks = 0;
// If tFrame < nextTick then 0 ticks need to be updated (0 is default for numTicks).
// If tFrame = nextTick then 1 tick needs to be updated (and so forth).
// Note: As we mention in summary, you should keep track of how large numTicks is.
// If it is large, then either your game was asleep, or the machine cannot keep up.
if (tFrame > nextTick) {
const timeSinceTick = tFrame - MyGame.lastTick;
numTicks = Math.floor(timeSinceTick / MyGame.tickLength);
}
queueUpdates(numTicks);
render(tFrame);
MyGame.lastRender = tFrame;
}
function queueUpdates(numTicks) {
for (let i = 0; i < numTicks; i++) {
MyGame.lastTick += MyGame.tickLength; // Now lastTick is this tick.
update(MyGame.lastTick);
}
}
MyGame.lastTick = performance.now();
MyGame.lastRender = MyGame.lastTick; // Pretend the first draw was on first update.
MyGame.tickLength = 50; // This sets your simulation to run at 20Hz (50ms)
setInitialState();
main(performance.now()); // Start the cycle
})();
もう一つの選択肢は、特定の処理の頻度を下げることです。更新ループの一部で、計算に時間がかかるものの、時間に対する感度が高くない処理がある場合は、その実行頻度を下げ、理想的には、その処理を延長された期間全体に小分けにして分散させることを考えてみるといいでしょう。これが暗に使われている例は The Artillery Blog for Artillery Games でみることができます。そこで彼らはガベージコレクションの最適化のためにガベージジェネレーションのランクを調整しています 。明らかにリソースのクリーンアップは時間に制約がありません(とりわけ整理整頓をすることがゴミそれ自体よりも混乱を招くようなときは特にです)。
これはまたあなた自身のタスクにも適用できるかもしれません。それらは利用可能なリソースが問題になった時によき抑制をしてくれます。
まとめ
はっきりさせておきたいのは、以上のいずれかがあなたのゲームにとって最適である可能性もあれば、どれも最適ではない可能性もあるということです。正しい判断は、あなたがどのトレードオフを受け入れる(あるいは受け入れない)かによって完全に決まります。懸念されるのは、主に別の選択肢への切り替えです。幸い、私自身はその経験はありませんが、リグレッションの追跡に追われる、非常に骨の折れる作業だと聞いています。
ウェブのような管理型プラットフォームにおいて覚えておくべき重要な点は、ループの実行が相当な時間停止してしまうことがあるということです。これは、ユーザーがタブの選択を解除し、ブラウザーが requestAnimationFrame のコールバック間隔を休止(または遅延)させた場合に発生する可能性があります。この状況に対処する方法はいくつかあり、ゲームがシングルプレイヤーかマルチプレイヤーかによって異なる場合があります。主な選択肢としては、次のようなものがあります。
-
途切れを「ポーズ」として考え、時間をとばす。
- ほとんどのマルチプレーヤーゲームでは問題だと思うかもしれませんが。
-
途切れに追いつくようシミュレートする。
- これは、長時間の途切れや複雑な更新がある場合に、問題になる可能性があります。
-
ゲームの状態をピアーもしくはサーバーから回復させる。
- ピアーやサーバーも最新版でない場合や、ゲームがシングルプレイヤーでサーバーを保有していないためにピアーやサーバー自体が存在しない場合は、この方法は効果がありません。
メインループの開発が完了し、ゲームに適した一連の仮定やトレードオフを決定したら、これでその決定に基づいて物理演算、AI、サウンド、ネットワーク同期、その他ゲームに必要なあらゆる要素を計算します。