入力イベント時刻による判定(発展課題)
キー入力が発生した時刻を曲の時刻に換算し、フレームの更新時刻による判定のずれを小さくする
第02回の基本課題を完成させてから取り組みます。編集するのはRhythmInput.cs、SongClock.cs、RhythmGame.csの3ファイルです。教材原本ではAssets/UNSLOW/RhythmGame/Scripts/にあります。HitJudgeの判定幅は変えません。
何を変えるか
基本実装のWasPressedThisFrame()は「このフレームで押されたか」を返します。判定へ渡すSongTimeも、そのフレームの時刻です。キーが押されてからUpdateが動くまでに時間が空くと、その分だけ遅い入力として扱われます。
RhythmGame.inputactionsのLane1〜Lane4はButton型で、Interactionは設定していません。この設定の各Actionのperformedでcontext.timeを読み取り、レーン番号とともに保存します。CallbackContext自体はコールバックの外で使えないため、必要な数値だけを取り出します。Unity公式:CallbackContext
ただし、context.timeはアプリ起動からの実時間を基準とし、曲の開始時刻StartDspTimeは音声システムのDSP時刻を基準とします。異なる時計の値をそのまま引き算しません。現在の実時間とDSP時刻を続けて読み、その差で入力時刻を曲時刻へ換算します。Unity公式:realtimeSinceStartupAsDouble・dspTime
| 値 | 例 |
|---|---|
| 入力イベントの実時間 | 12.340秒 |
| 処理時の実時間 | 12.360秒 |
| 処理時のDSP時刻 | 102.000秒 |
| 曲の開始DSP時刻 | 101.000秒 |
この例では、フレームの曲時刻は102.000 - 101.000 = 1.000秒ですが、入力の曲時刻は12.340 - 12.360 + 102.000 - 101.000 = 0.980秒です。20ミリ秒の差を判定に反映できます。
1. 入力時刻を保存する
RhythmInput.csの先頭へQueue<T>用の名前空間を追加します。
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;同じファイルのprivate InputAction[] laneActions;の直後に、入力1回分の型と待ち行列を追加します。複数レーンの同時押しや、同じフレームに届く複数の入力を順に処理するためです。
private InputAction[] laneActions;
public readonly struct LanePress
{
public readonly int lane;
public readonly double time;
public LanePress(int lane, double time)
{
this.lane = lane;
this.time = time;
}
}
private readonly Queue<LanePress> lanePresses = new Queue<LanePress>();Awake内でレーンActionを探すfor文を、次へ置き換えます。laneをループ内の変数へコピーすると、各コールバックが自分のレーン番号を保存できます。開始と再挑戦のActionは従来どおりです。
laneActions = new InputAction[laneActionNames.Length];
for (int i = 0; i < laneActionNames.Length; i++)
{
int lane = i;
laneActions[i] = map.FindAction(laneActionNames[i], true);
laneActions[i].performed += context =>
lanePresses.Enqueue(new LanePress(lane, context.time));
}
runtimeActions.Enable();WasLanePressedメソッドを削除し、同じ位置へ次の2メソッドを置きます。TryGetLanePressは保存した入力を古い順に1件取り出します。
public bool TryGetLanePress(out LanePress press)
{
if (lanePresses.Count == 0)
{
press = default;
return false;
}
press = lanePresses.Dequeue();
return true;
}
public void ClearLanePresses() { lanePresses.Clear(); }この段階ではRhythmGameがまだWasLanePressedを呼ぶため、続けて手順2・3も編集してからUnityのコンパイル結果を確認します。
2. 入力時刻を曲時刻へ換算する
SongClock.csのStopメソッドの直前へ追加します。現在の2種類の時計を近いタイミングで読み、入力イベントの実時間をDSP時刻へ換算してからStartDspTimeを引きます。
public double InputSongTime(double eventTime)
{
double inputNow = Time.realtimeSinceStartupAsDouble;
double dspNow = AudioSettings.dspTime;
return eventTime - inputNow + dspNow - StartDspTime;
}
public void Stop() { source.Stop(); }これは時計の差を測る簡易換算です。音声出力、画面表示、キーボードの遅延を測定・補正する処理ではありません。アプリが一時停止して時計の進み方が変わった場合に備え、次の手順で古い入力を捨てます。
3. 見逃し判定より先に入力を処理する
RhythmGame.csのBeginRoundで、ラウンドを始める前に保存済みの入力を消します。
LoadChart();
if (State == GameState.Error) return;
input.ClearLanePresses();
PerfectCount = GoodCount = MissCount = 0;Updateメソッドを次へ置き換えます。演奏中以外に押したレーンの入力は、次のラウンドへ持ち越しません。
private void Update()
{
if (input.RestartPressed) BeginRound();
else if (input.StartPressed && State != GameState.Playing) BeginRound();
if (State != GameState.Playing)
{
input.ClearLanePresses();
return;
}
double songTime = clock.SongTime;
Advance(songTime);
}Advance内のspawner.Refresh(songTime);の直後へ、入力を取り出す処理を追加します。ここなら、そのフレームに必要なノーツをSpawnDueで用意した後、IsMissによる見逃し判定より前にキー入力を判定できます。フレームの時刻が受付範囲を過ぎていても、入力イベントの時刻が範囲内なら判定できます。
spawner.SpawnDue(songTime);
spawner.Refresh(songTime);
while (input.TryGetLanePress(out var press))
PressLane(press.lane, clock.InputSongTime(press.time));
// 見逃し処理でリストからノーツが外れるため、末尾から走査する最後にOnApplicationFocusで、フォーカスを失ったときの入力も消します。
if (focused || State != GameState.Playing)
return;
input.ClearLanePresses();
clock.Stop();
State = GameState.Ready;動作を確かめる
- 3ファイルを保存してUnityへ戻り、Consoleにコンパイルエラーがないことを確認します。
- PlayしてD・F・J・Kを押し、対応するレーンが光ることを確かめます。同時押しのノーツも両方判定されることを確認します。
- 演奏中にRestartし、前のラウンドで押したレーンが新しいラウンドの判定に混ざらないことを確認します。別ウィンドウへ移動して戻った場合も同様に試します。
- 上の数値例を使い、
InputSongTimeが返す曲時刻がフレームの曲時刻より20ミリ秒早くなる理由を説明します。必要なら一時的にPressLaneへ渡す直前の両時刻をConsoleへ表示して確かめ、確認後はログを外します。 - 同じ曲と出力機器で、低いフレームレートと通常のフレームレートを比べます。入力時刻を使っても判定にずれが残るなら、まず譜面時刻・音声出力・入力機器を分けて調べます。
context.timeは通常、コールバックが呼ばれた瞬間ではなく、Actionを起こした入力イベントの時刻です。ただし時計の換算は近接した2回の読み取りによる推定であり、機器ごとの遅延も残ります。判定結果が完全に同じになる保証として扱わず、フレーム時刻に丸めていた分を改善する課題として評価してください。