Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: fix broken targetTime control for long loading time #177

Merged
merged 5 commits into from
Jan 17, 2023
Merged

Conversation

xnv
Copy link
Member

@xnv xnv commented Dec 23, 2022

目標時刻 (targetTime) ありでのリプレイ実行において、経過時間の計算にローディングシーンなどの時間が含まれてしまう問題を修正します。

従来、例えば目標時刻が (ゲーム開始から) 60 秒 の時、アセットロードに 10 秒かかってしまうと、ゲーム内時間が 50 秒経過した時点で「targetTime に追いついた」判定になってしまっていました。目標時刻が (実時間経過などで) 進んでいく間は顕在化しませんが、終了済みのプレイの終端まで実行しようとする時に「終端に到達できない」問題になります。

影響はリプレイ実行のみです。リアルタイムのゲーム実行中の問題ではありません。

対応

実行開始からの "経過ゲーム内時間" を表す値 GameLoop#_currentTickTime を追加します。

従来、 GameLoop_currentTime に現在の時間を保持していました。この値は "経過時間" 、すなわちローディングシーンの表示中など「ゲーム内時間が進まない状態」でも進行する時間でした。 "経過時間" は、timestamp つき tick の消化タイミングを知るために必要な値です。

問題はこの "経過時間" を、目標時刻の到達判定にも用いていたことです。目標時刻は明らかに「ゲーム内の時間」で指定されるものなので (実行の度に変動するローディング込みの時間など指定できません) 、これは単位の違う数の大小を比較しているような状況でした。

これを解消するには、目標時刻と比較可能な「現在のゲーム内時間」を把握する必要があります。このような値は現在の GameLoop にありません。ティックの消化タイミングは完全に把握しているので、消化のたびに都度加算する値として新設し、これを _currentTickTime と呼ぶことにします。元々 timestamp つき tick の時刻を「tick の時刻/tick time」と呼んでいるので、それに沿った命名になっています。

動作確認

ユニットテストの他、意図的にアセットロードを遅らせる改造をした akashic serve で問題の再現と修正を確認しています。

@@ -479,7 +492,7 @@ export class GameLoop {
}
if (this._omitInterpolatedTickOnReplay && this._sceneLocalMode === "interpolate-local") {
if (this._foundLatestTick) {
// 最新のティックが存在しない場合は現在時刻を目標時刻に合わせる
// これ以上新しいティックが存在しない場合は現在時刻を目標時刻に合わせる
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

別件: コメントがわかりにくかったので調整。

// 特殊ケース: 目標時刻より手前に次ティックがあるので、目標時刻までは進んで次ティックは消化してしまう。
// (この処理がないと、特にリプレイで「最後のティックの0.1フレーム時間前」などに来たときに進めなくなってしまう。)
nextFrameTime = targetTime;
const nextTickTime = this._tickBuffer.readNextTickTime() ?? (this._currentTickTime + this._frameTime);
Copy link
Member Author

@xnv xnv Dec 26, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

修正のキモ1: tick に timestamp がない時の「次のティック時刻」 nextTickTime を正しく求めます。

これは「現在のティック時刻」 _currenTickTime を保持することで可能になりました。これまでは nextFrameTime (current time + 1/FPS) で求めており、これはロード時間を含めて進むゲーム外時間なので正しくありませんでした。

// (この処理がないと、特にリプレイで「最後のティックの0.1フレーム時間前」などに来たときに進めなくなってしまう。)
nextFrameTime = targetTime;
const nextTickTime = this._tickBuffer.readNextTickTime() ?? (this._currentTickTime + this._frameTime);

Copy link
Member Author

@xnv xnv Dec 26, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

修正のキモ2: 条件式を変更しています。ややこしいので整理します。

  • f': nextFrameTime (_currenTime + 1/FPS)
  • t': nextTickTime (次ティックのタイムスタンプ時刻、なければ _currenTickTime + 1/FPS)
  • g: targetTime

と置きます。f', t', g の大小関係は次の6通りで、それに応じた処理は概ね 3 種類です:

A) g  ≦ t' ≦ f' (目標時刻に到達している → 何もしない)
B) g  ≦ f' < t' (同上)
C) t' ≦ f' < g  (次ティック時刻に到達していて、次ティックも目標時刻より手前 → 次ティックを消化する)
D) t' < g  ≦ f' (同上)
E) f' < t' < g  (次ティック時刻の消化タイミング待ち → ローカルティックを消化する (か、次ティック時刻まで飛ぶ))
F) f' < g  ≦ t' (次ティック手前の目標時刻まで進みたい → ローカルティックを消化する (か、目標時刻まで飛ぶ))

if-then 式に整理すると

1. (g ≦ t') かつ (g ≦ f') ならば、A or B
2. そうでなく、(f' < t') ならば、 E or F (B は上で除外済みのため)
3. そうでなければ、 C or D

となり、これをコードにしたのがここから下の部分です。

修正前のコードと条件式が異なりますが、修正前はそもそも nextFrameTime (f') を t' と混同している、 ティックにタイムスタンプがない場合 nextTickTime (t') に f' が入る、という具合で、今にして思うとどうやって辻褄を合わせていたのか首を傾げる様相なので、比較しないでください。

@@ -400,7 +413,7 @@ export class GameLoop {
_doLocalTick(): void {
const game = this._game;
const pevs = this._eventBuffer.readLocalEvents();
this._currentTime += this._frameTime;
this._currentTime += this._frameTime; // ここでは _currenTickTime は進まないことに注意 (ローカルティック消化では進まない)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

修正のキモ4: ローカルティック消化では _currentTickTime を進めません。

コメント通り。他修正箇所多数ですが、大半は _currentTime の代わりに _currenTtickTime を使うことによる変更です。

Comment on lines +875 to +882
_isImmediateBeforeOf(targetTime: number): boolean {
// 目標時刻への到達判定。次ティックがない場合は _foundLatestTick に委ねる、
// すなわち既存全ティックを消化した時は到達とみなす点に注意。あまり直観的でないが、こうでないと永久に
// rawTargetTimeReachedTrigger を fire できない可能性があり、後方互換性に影響がありうる。
return this._tickBuffer.hasNextTick() ?
(targetTime < (this._tickBuffer.readNextTickTime() ?? (this._currentTickTime + this._frameTime))) :
this._foundLatestTick;
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

修正のキモ3: 目標時刻への到達 (直前かどうか) 判定用の関数を新設しています。

ゲーム内時間 (ティック時間) を目標時刻と比べる関係上、「次ティックがない」=次ティック時刻が分からない状況を考慮する必要が生じました。この場合は「最新ティックを見つけている (=最新に到達している)」ならば、その時点で目標時刻への到達 (直前) であるとして扱います。(コメントどおり)

@xnv xnv merged commit 2e52d98 into main Jan 17, 2023
@xnv xnv deleted the fix-long-load branch January 17, 2023 10:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants