Skip to content

入力イベント時刻による判定(発展課題) ​

このステップの目的

キー入力が発生した時刻を曲の時刻に換算し、フレームの更新時刻による判定のずれを小さくする

第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>用の名前空間を追加します。

cs
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;

同じファイルのprivate InputAction[] laneActions;の直後に、入力1回分の型と待ち行列を追加します。複数レーンの同時押しや、同じフレームに届く複数の入力を順に処理するためです。

cs
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は従来どおりです。

csharp
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件取り出します。

csharp
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を引きます。

cs
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で、ラウンドを始める前に保存済みの入力を消します。

cs
LoadChart();
if (State == GameState.Error) return;
input.ClearLanePresses();
PerfectCount = GoodCount = MissCount = 0;

Updateメソッドを次へ置き換えます。演奏中以外に押したレーンの入力は、次のラウンドへ持ち越しません。

csharp
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による見逃し判定より前にキー入力を判定できます。フレームの時刻が受付範囲を過ぎていても、入力イベントの時刻が範囲内なら判定できます。

cs
spawner.SpawnDue(songTime);
spawner.Refresh(songTime);
while (input.TryGetLanePress(out var press))
    PressLane(press.lane, clock.InputSongTime(press.time));

// 見逃し処理でリストからノーツが外れるため、末尾から走査する

最後にOnApplicationFocusで、フォーカスを失ったときの入力も消します。

cs
if (focused || State != GameState.Playing)
    return;

input.ClearLanePresses();
clock.Stop();
State = GameState.Ready;

動作を確かめる ​

  1. 3ファイルを保存してUnityへ戻り、Consoleにコンパイルエラーがないことを確認します。
  2. PlayしてD・F・J・Kを押し、対応するレーンが光ることを確かめます。同時押しのノーツも両方判定されることを確認します。
  3. 演奏中にRestartし、前のラウンドで押したレーンが新しいラウンドの判定に混ざらないことを確認します。別ウィンドウへ移動して戻った場合も同様に試します。
  4. 上の数値例を使い、InputSongTimeが返す曲時刻がフレームの曲時刻より20ミリ秒早くなる理由を説明します。必要なら一時的にPressLaneへ渡す直前の両時刻をConsoleへ表示して確かめ、確認後はログを外します。
  5. 同じ曲と出力機器で、低いフレームレートと通常のフレームレートを比べます。入力時刻を使っても判定にずれが残るなら、まず譜面時刻・音声出力・入力機器を分けて調べます。

context.timeは通常、コールバックが呼ばれた瞬間ではなく、Actionを起こした入力イベントの時刻です。ただし時計の換算は近接した2回の読み取りによる推定であり、機器ごとの遅延も残ります。判定結果が完全に同じになる保証として扱わず、フレーム時刻に丸めていた分を改善する課題として評価してください。