【第50回】UnityのTick処理を徹底検証!FPS低下でも結果が変わらないゲームを目指す

機械設計×ゲーム
0

前回、第49回では、PUZFLOの生産処理を安定させるために、
Tick処理を導入することを決めました。

きっかけは、第37回から続いていた問題です。

同じ設備配置・同じ供給タイミングなのに、
エラーが発生するときと発生しないときがある

第38回でFPS改善を進め、Windowsビルド版では
ほぼ60FPSで安定するようになりました。

しかし、画面録画や他のアプリの影響でFPSが低下する可能性は残っています。
PUZFLOは、プレイヤーが設計した生産ラインの結果を確認するゲームです。

PCが重くなっただけで生産結果が変わってしまうのは、
できるだけ避けたいところです。

そこで今回は、ChatGPTとCodexを使って、
Tick処理を実際に実装し、FPSを変えながら検証していきます。

ただ、ゲーム内部の仕組みを変更するので、少し不安もありました。

「本当に今までの設備は正常に動くのか?」
「Tick処理にしたことで、ゲームが重くならないのか?」
「20FPSまで落ちても、同じ結果になるのか?」

こうした疑問を、一つずつ確認していきます。


Tick処理の導入で一番心配だったこと

Tick処理を導入する前に、特に気になっていたのが、
これまで作ってきた設備への影響です。

PUZFLOには、すでに多くの設備があります。
投入機、コンベア、加工機、ストッパ、分岐・合流、組立機、ロボットアームなど。

それぞれが独立して動くだけでなく、設備同士でワークを受け渡しています。
例えば、投入機の供給タイミングが少し変わるだけでも、加工機への到着時間が変わります。

加工機の排出タイミングが変われば、後ろのワークが詰まるかもしれません。

つまり、一つの処理を変更すると、ライン全体に影響する可能性があるということです。

第37回でTick処理を見送った理由の一つも、ここにありました。

今回はSteamで販売するゲームとして安定性を高めるため、
改めて導入することにしましたが、できる限り既存の仕様を維持したいと考えました。


調査して分かった「ワークだけTick化してもダメ」という問題

最初は、「ワークの移動処理だけ一定間隔にすればいいのでは?」と思っていました。

しかし、ChatGPTとコードの構造を調べていくと、
それだけでは不十分だと分かりました。

例えば、ワークの移動だけをTick処理にしても、
投入機がFPSに影響されていたらどうでしょうか。

ワークが供給されるタイミングが変わってしまいます。

同じように、加工機やストッパ、出荷判定なども
別々のタイミングで動いていたら、結果が変わる可能性があります。

そこで、生産結果に影響する処理を共通のTickで管理する方針にしました。

今回Tick化の対象にした処理

対象Tick処理で管理したい内容
シミュレーション時間ゲーム内部の経過時間
投入機ワークの供給タイミング
ワーク移動・間隔・接触判定
加工機・組立機処理時間・受け渡し
ストッパ停止・開放タイミング
分岐・合流ワークの進行方向・優先順位
クロスコンベア交差時の搬送判定
ロボットアームピック・搬送・排出
出荷場出荷数・NG判定
クリア判定成功・失敗の結果

一方で、UIやカメラ、BGMなど、ゲーム結果に直接影響しない部分は、従来の画面更新処理を利用します。

生産結果を決める処理と、見た目を表示する処理を分ける。
これが今回の基本方針です。


Tick処理にするとゲームが重くなるのでは?

ここで、自分がChatGPTへ質問したのが、

はじめ
はじめ

「これって、Tick化するとゲームもだいぶ重くなるんじゃ?」

ということでした。

PUZFLOは以前、後半ステージでFPSが大きく低下する問題がありました。

ようやくWindows版で60FPS前後を維持できるようになったので、
また重くなるのは避けたいところです。

するとChatGPTから、意外と分かりやすい説明がありました。

通常60FPSで動いている場合、従来の生産処理も
1秒間に約60回実行されているものが多いとのこと。

そこで、Tick処理も60Hz(1秒間に60回)に設定します。

つまり、

  • 従来:1秒間に約60回処理
  • Tick導入後:1秒間に60回処理

となるため、正しく移行できれば通常時の処理回数は大きく変わりません。
ただし、FPSが低下した場合は違います。

30FPSや20FPSではどうなる?

画面のFPS画面1回の更新に対するTickの目安
60FPS1Tick
30FPS2Tick
20FPS3Tick

例えば、30FPSまで低下しても、ゲーム内部では1秒間に60回の生産処理を進める仕組みです。
これなら、画面が少しカクついても、生産ラインの判定タイミングは維持しやすくなります。

ただし、FPSが低い状態では、1画面更新あたりに実行する生産処理が増えることになります。
そのため、極端にPCが重くなった場合には、別の対策が必要でした。


Tick処理は17ステップに分けて慎重に実装

今回、ChatGPTと相談して決めたのが、
「一気にすべての設備をTick化しない」という方針です。

Codexへゲーム全体の変更を一度に依頼すると、
途中で不具合が発生しても、何が原因なのか分かりにくくなります。

そこで、共通のTick処理の土台を作り、
ゲーム結果に関わる設備や判定を段階的に移行しました。

最終的には、STEP1〜STEP17まで、
17段階に分けて慎重に実装・確認する形になりました。

基本的な進め方は、

  1. 共通の60Hz Tick処理を用意する
  2. 生産結果に関わる処理を段階的に移す
  3. 設備の動作を確認する
  4. FPSを変えて結果を比較する
  5. 過負荷時の安全対策を追加する
  6. 実際のゲーム環境で再確認する

という流れです。

なお、各STEPですべて異なる設備を追加したわけではありません。
実装だけでなく、調査・確認・不具合修正なども挟みながら進めています。

自分はコードを理解しているわけではありませんが、
「何を変更したか」「何が正常に動いていれば合格か」
を確認しながら進めることで、大きな変更にも取り組みやすくなりました。


Tick処理の実装後に検証してみた

実装が進んだところで、いよいよ動作確認です。

今回特に確認したいのは、
「FPSが変わっても、本当に同じ結果になるのか?」ということです。

通常の60FPSだけで確認しても意味がありません。
そこで、30FPSと20FPSという、意図的にFPSを下げた条件で比較しました。

ただし、普通に画面を見比べるだけでは、細かい違いを判断できません。
そこで使用したのが、Trace(トレース)という記録です。


Traceとは?ゲーム内部の動きを記録する仕組み

Traceという言葉も、ゲーム制作を始めるまで知りませんでした。
かなり簡単に説明すると、ゲーム内部で何が起こったのかを順番に記録する仕組みです。

例えば、

  • 何番目のTickでワークが供給されたか
  • どのワークが出荷されたか
  • いつNGが発生したか
  • どの設備でイベントが起こったか

といった情報を記録します。

PUZFLOの場合、画面上で同じように見えても、
内部ではほんの少しタイミングが違う可能性があります。

Traceを使えば、そうした違いまで確認できます。
今回は30FPSと20FPSで動かした結果を保存し、
ChatGPTに比較してもらいました。


結果は30FPSと20FPSで完全一致!

ここは、今回の開発でかなり嬉しかったところです。
同じSeed・同じライン構成で、30FPSと20FPSの条件を比較した結果がこちらです。

検証項目30FPS20FPS
最終Tick番号7,8367,836
Trace記録数3,767件3,767件
ワーク供給イベント2,102件2,102件
出荷イベント1,664件1,664件
NGイベント1件1件
最終NG発生Tick7,8367,836

すべて同じ数値になりました。

ただ、件数が同じだけなら偶然の可能性もあります。
そこで、さらに詳しく確認しました。

Traceに記録された、

  • Tick番号
  • イベントの種類
  • ワークID
  • 設備ID
  • 詳細情報
  • イベントの並び順

まで比較したところ、3,767イベントすべてが完全一致。

差は1件もありませんでした。
これはかなり大きな結果です。


2分以上の生産でも同じ結果になった

今回の最終Tick番号は、7,836です。

60Hzなので、ゲーム内部では、7,836 ÷ 60 = 約130.6秒となります。

つまり、約2分10秒にわたる生産処理が、
30FPSでも20FPSでも同じ結果になったということです。

さらに、最後は両方とも同じTickでNGが発生しました。

ここも重要です。

今回の検証では、NGが発生しなかったから成功というわけではありません。
同じラインなら、失敗する場合も同じタイミングで失敗する。

つまり、成功・失敗の結果が再現できることが大切です。

PUZFLOでは、プレイヤー自身のライン設計によって
結果が決まってほしいので、これはかなり嬉しい検証結果でした。


途中で結果が違ったこともあった

実は、検証中に最初からすべて一致していたわけではありません。
途中で30FPSと20FPSの結果に違いが出たこともありました。

ただ、確認してみると、テストの途中でラインの設備配置を変更していました。

これでは、正しい比較になりません。

Tick処理の再現性を確認するには、
同じSeedだけでなく、設備配置や各設備の設定も同じである必要があります。

そこで条件を揃え直して再検証した結果、先ほどの3,767イベント完全一致につながりました。
ゲームをテストするときも、比較する条件を正しく揃えることが重要だと分かりました。


次の問題は「PCが極端に重くなった場合」

30FPSと20FPSで同じ結果になることは確認できました。
ただし、これで終わりではありません。

ChatGPTから、次に注意した方がいいと言われたのが、
Catch-up(キャッチアップ)処理の過負荷です。

Tick処理では、例えば20FPSなら画面1回の更新中に約3Tickを処理します。
しかし、PCが一瞬固まったらどうなるでしょうか。

例えば、処理が長時間止まると、

10Tick。
20Tick。
30Tick。

と、実行すべきTickが大量に溜まる可能性があります。

それを一気に実行しようとすると、

PCが重い → Tickが溜まる → 一気に処理する → さらに重くなる

という悪循環が発生するかもしれません。

そこで、STEP14では過負荷への安全対策を追加することにしました。


「自動Pause」を導入

今回考えたのは、処理が極端に遅れた場合は、無理に追いつかず生産を一時停止する仕組みです。

PUZFLOは、速く動かすことよりも、正しい生産結果を維持することが重要です。
そのため、大きな負荷が発生したときは、無理に処理を続けるのではなく、安全側へ停止させます。

今回設定したCatch-upの上限は、1フレームあたり最大8Tickです。

具体的には、次のような考え方です。

必要なTick数動作
1Tick通常処理
2Tick通常処理
3Tick通常処理
4Tick続行
8Tick続行
9Tick以上Auto Pause

通常の20FPSなら約3Tickなので、問題なく処理できます。

一方、9Tick以上の処理が必要になるほど大きな遅れが発生した場合は、
無理に追いつこうとせず、自動でPauseします。

少しのFPS低下なら吸収する。
極端な負荷なら安全に止める。

という考え方です。


なぜ8Tickにしたのか?

8Tickという数値は、最初から正解が決まっているわけではありません。

PUZFLOでは、20〜30FPS程度まで低下する可能性も想定しています。

そのため、上限を3Tickや4Tickにすると、少しFPSが下がっただけでもPauseが発生するかもしれません。

一方で、上限を大きくしすぎると、今度は一気に大量の処理を実行してしまいます。

そこで今回は、8Tickを初期値として検証することにしました。

60Hzでは約0.133秒分に相当します。

ただし、これは「0.133秒以上重くなったら必ず止まる」という単純な意味ではなく、
実際にそのフレームで必要になったTick数によって判定します。

このあたりの仕組みは、自分だけではなかなか思いつかなかった部分です。


Auto Pauseではシミュレーション時間を飛ばさない

もう一つ重要なのが、Pauseしたあとの動作です。
例えば、PCが一瞬固まって大量のTickが溜まったとします。

そこでPauseしても、再開した瞬間にその溜まったTickを全部処理したら意味がありません。

そのため今回は、過負荷でPauseした場合、
遅れていた現実時間を無理に取り戻さないという設計にしました。

ゲーム内部のTick番号やシミュレーション時間を勝手に進めるのではなく、
一時停止した状態から通常の処理を再開します。

これなら、PCが一瞬重くなっただけで、ワークが突然大量に移動したり、
生産時間が飛んだりすることを防ぎやすくなります。

また、過負荷が解消されたからといって、
勝手にゲームを再開しない方針にしました。

再開するかどうかはプレイヤーが決める。

ここも大切にした部分です。


既存のPause機能もそのまま利用する

PUZFLOには、もともとPause機能があります。

例えば、

  • 別のアプリへ画面を切り替えた場合
  • 設定画面を開いた場合
  • 解像度や画面表示を変更した場合
  • ウィンドウサイズを変更した場合

などです。

今回の過負荷対策では、新しい停止システムを別に作るのではなく、既存のPause機能を利用する方針にしました。
同じような機能を複数作ると、逆に不具合の原因になる可能性があるからです。

また、過負荷でPauseしている最中に別のPause条件が発生しても、
状態がおかしくならないように考慮しています。

こういう部分は、プレイヤーから見えにくいですが、ゲームを安定させるには重要だと思います。


「Tick処理ができた」と「製品版で安心」は別

今回のTrace比較では、30FPSと20FPSで非常に良い結果が出ました。

ただし、この結果だけで、あらゆるPC環境で
完全に同じ動作になると証明できたわけではありません。

特に確認が必要なのは、Windowsビルド版での動作です。

Unity Editor上で正常でも、実際に配布するゲームで同じになるとは限りません。

そのため、次の確認では、通常の60FPS前後と、
負荷をかけた20〜30FPSを比較することが重要になります。

さらに、通常時のCPU負荷も確認したいところです。

Tick処理を導入した結果、普段のゲーム動作まで
大幅に重くなってしまっては困ります。

今回の目的は、ゲームを重くしてでも無理やり結果を揃えることではなく、
通常時の快適さを維持しながら、生産結果の再現性を高めることだからです。


第50回で感じたAIゲーム開発の難しさ

ゲーム制作を始めたころは、ChatGPTやCodexへお願いして、

「コンベアを動かして」
「加工機を追加して」
「ワークを出荷できるようにして」

といった実装が中心でした。

でも今回のTick処理は、少し違います。

画面上では、ほとんど変化がありません。
新しい設備が増えるわけでもありません。
派手なアニメーションが追加されるわけでもありません。

それでも、ゲームの完成度を高めるうえでは、かなり大切な改善です。

そして今回は、AIに実装してもらうだけではなく、
結果が正しいかを確認することの重要性を強く感じました。

自分は相変わらずコードを理解しているわけではありません。

それでも、

「同じ条件なのに結果が違うのはおかしい」
「FPSが下がっても正常に動いてほしい」
「無理な処理でゲームが止まるなら、安全にPauseしてほしい」

という要求は決められます。

その要求をChatGPTと整理し、Codexで実装し、実際の結果を確認する。
この進め方が、以前より少しずつ身についてきた気がします。


まとめ|Tick処理の導入で生産結果の再現性が大きく改善

今回、第50回では、前回から取り組んできたTick処理について、
実装と検証の内容をまとめました。

特に大きかったのは、次の結果です。

  • 60Hzの共通Tick処理を採用
    • 生産結果に関係する処理を、画面のFPSとは分けて管理する仕組みを目指しました。
  • 17ステップに分けて慎重に実装
    • 既存の設備仕様をできる限り維持しながら、段階的に変更しました。
  • 30FPSと20FPSでTraceが完全一致
    • 約130.6秒、3,767イベントの比較で、Tick番号・ワークID・設備ID・イベント順まで一致しました。
  • 過負荷時の安全対策を追加
    • 極端にTickが溜まった場合は、無理に処理せずAuto Pauseする仕組みを取り入れました。

今回のTick処理は、ゲーム画面の見た目を変える改善ではありません。

しかし、PUZFLOにとっては非常に重要です。
なぜならPUZFLOは、プレイヤーが設計した生産ラインの結果を楽しむゲームだからです。

同じ条件で作ったラインなら、できるだけ同じ結果になってほしい。
PCの一時的な負荷によって、成功・失敗が左右されることは避けたい。
今回の検証によって、その目標へ大きく前進できたと思います。

「動くゲーム」から「安心して試せるゲーム」へ。

Steamで販売するために、こうした見えない部分も少しずつ仕上げていきます。


次回は「ワークが増えるとゲームが重くなる問題」を改善する

今回のTick処理によって、FPSが低下しても、
生産結果が変わりにくい仕組みを作ることができました。

これでひと安心……と思っていたのですが、今度は別の問題が発生しました。

ゲーム終盤の大きな工場で生産を開始すると、最初は60FPSで動いていたのに、
約10秒後には40FPS前後まで低下するようになったのです。

さらに、ゲームを進めると、PCへの負荷が大きくなり、
ゲームが自動で一時停止することもありました。

そこでChatGPTとCodexを使って原因を調査したところ、
ワーク同士が重ならないようにする処理が、
大きな負担になっていることが分かりました。

PUZFLOでは、コンベア上のワークがぶつかったり、
重なったりしないように、ワーク同士の間隔を確認しています。

ところが、ワークの数が増えるほど、
確認しなければならない組み合わせも増えてしまいます。

つまり、工場が大きくなるほど、ワークの間隔を確認する計算が増えて、
ゲームが重くなっていたというわけです。

次回、第51回では、この処理を軽くすることに挑戦します。

ただし、ワーク同士が重ならないという大切なルールは、そのまま残したいところです。

そこで、「ゲームの動きは変えずに、無駄な計算だけを減らせないか?」
という方針で改善を進めていきます。

Tick処理で生産結果を安定させた次は、
ワークが大量に流れる大規模な工場でも、快適に遊べるPUZFLOを目指します。

コメント

タイトルとURLをコピーしました