Skip to content
第03回 リズムゲーム2

1 / 7 ノートの操作 ​

このステップの目的

判定時刻から発行する対象を選び、残り時間を位置へ変換して、同じ時間軸でノートを動かします。

第02回で、曲の時刻とノートの判定時刻を秒へそろえました。ここでは、その二つの時刻から表示を始める条件と、画面上の位置を求めます。ノートの発行は、表示用のオブジェクトを用意して有効にする処理です。

NoteDataのlaneは左から0〜3、timeは曲の何秒で押すかを表します。次は、左端を曲の開始から3秒で押すノートの説明用の例です。

csharp
new NoteData(0, 3.0)

実習では配布済みの8音の練習譜面を使います。時刻順の配列で、同時押しも含みます。ここで譜面を書き換える必要はありません。

ノートの発行 ​

発行予定時刻に達した対象を選ぶ ​

approachTimeは、判定時刻の何秒前にノートを用意するかを表します。判定時刻が3秒、先読み時間が2.25秒なら、発行する予定時刻は0.75秒です。

text
発行予定時刻 = 判定時刻 - 先読み時間
発行する条件 = 曲の現在時刻 >= 発行予定時刻

画面の更新は一定間隔とは限りません。あるフレームで0.74秒、次のフレームで0.76秒を読んだ場合、0.75秒との完全一致を待つと発行できません。予定時刻に達したかを調べれば、次の更新で対象を取り出せます。

配列は時刻順なので、NextIndexが示す次のノートから調べます。同じ時刻のノートが複数ある場合も、一度の更新で条件を満たすものをすべて発行します。

時刻順の配列からノートを発行する ​

NoteSpawner.csの配布済みのSpawnDueを、次のコードと比べてください。位置を計算するRefreshが使うPositionYは、次の節で確認します。

NoteSpawner.cscsharp
public void SpawnDue(double songTime)
{
    while (NextIndex < notes.Length && notes[NextIndex].time <= songTime + approachTime)
    {
        var note = _available.Count > 0 ? _available.Pop() : Instantiate(prefab, root);
        note.gameObject.SetActive(false);
        note.Prepare(notes[NextIndex], this);
        note.Refresh(songTime, speed);
        activeNotes.Add(note);
        note.gameObject.SetActive(true);
        NextIndex++;
    }
}

time <= songTime + approachTimeは、先ほどの発行条件と同じ意味です。配列の末尾を越えないよう、先にNextIndex < notes.Lengthを確認します。whileで繰り返すので、同時刻の二つのノートも同じフレームで用意できます。

_availableは、使い終えて非表示になったノートを再利用するための入れ物です。あれば取り出し、なければPrefabから生成します。Prepareで前回の判定と見た目を初期化し、Refreshで現在時刻の位置へ置いてから表示します。activeNotesに表示中のノートを登録し、NextIndex++で次の対象へ進みます。

フレームの更新が予定より遅れた場合も、発行後の位置はその時点の曲時刻から求めます。予定時刻へさかのぼって画面を描くのではなく、現在の位置を計算する処理です。

ノートの移動 ​

残り時間を判定線までの距離へ変換する ​

判定時刻から曲の現在時刻を引くと、押すまでの残り時間になります。その値に毎秒の移動速度を掛け、判定線からの距離へ変換します。

text
残り時間 = 判定時刻 - 曲の現在時刻
判定線からの距離 = 残り時間 × 毎秒の移動速度

3秒で押すノートを毎秒270の速さで動かす場合、曲の1秒では残り2秒なので、距離は540です。

曲の現在時刻残り時間判定線からのY距離
1秒2秒540、線より上
3秒0秒0、線に重なる
3.1秒−0.1秒−27、線を通過

曲の2秒での距離を計算してから、NoteMotion.csの配布済みのPositionYを次のコードと比べてください。

NoteMotion.cscsharp
public static float PositionY(double hitTime, double songTime, float speed)
{
    return (float)((hitTime - songTime) * speed);
}

speedの単位はCanvas上の座標単位/秒です。時刻差はdoubleで計算し、UIへ渡す距離をfloatへ変換します。hitTime - songTimeが0になる時刻では、速度がいくつでも距離は0です。

この処理は、前の位置へ移動量を足すのではなく、現在時刻に対応する位置を毎回求めます。フレームの間隔が変わっても、次の更新で曲の現在位置に対応する距離を計算できます。画面更新の遅れがなくなるわけではなく、更新時の位置を音声の時間軸に合わせる方法です。

判定線からの距離をUIの座標へ変換する ​

距離が0でも、親オブジェクト内でのY座標が0とは限りません。ノートの親の領域と、判定線のRectTransformは別だからです。配布済みのRefreshは、距離に判定線の座標を足して配置します。

NoteMotion.cscsharp
var x = _spawner.LaneCenterX(data.lane);
var y = _spawner.JudgmentY + PositionY(data.time, songTime, speed);
rect.anchoredPosition = new Vector2(x, y);

LaneCenterXはレーンの中心、JudgmentYはノートの親の座標で表した判定線の中心です。今回は配布済みの座標変換を使い、時間から距離を求める部分を担当します。

速度と先読み時間を変えて比較する ​

UnityのConsoleにコンパイルエラーがないことを確認します。Chart SourceのUse CsvをオフにしてPlayすると、固定譜面の8音が流れます。入力判定も配布済みなので、この段階から演奏できます。判定の処理は次のステップで確認します。

Note SpawnerのSpeedを270から180へ変える前に、出現時の高さと判定線へ届く時刻を予想してください。Play中に変更して比べ、確認後は270へ戻します。速度を下げると近い位置からゆっくり進みますが、判定時刻は同じです。

次に、速度270のままApproach Timeを2.25から2へ変更してください。発行予定時刻は遅くなり、発行時の距離は607.5から540へ変わります。確認後は2.25へ戻してください。

表示領域の外のノートはRectMask2Dで隠れるため、発行した時刻と、画面で見え始める時刻が一致しない場合があります。発行はオブジェクトを有効にする処理、見え始める時刻は表示領域にも依存する結果として区別します。

チェックリスト
  • 発行予定時刻を、判定時刻と先読み時間から求められる。
  • 完全一致ではなく、予定時刻に達したかを調べる理由を説明できる。
  • 同時刻のノートをwhileで発行する理由を説明できる。
  • 残り時間と速度から距離を計算し、距離0とUIの座標0を区別できる。
  • 速度と先読み時間を一つずつ変え、予想と結果を比べた。
  • Speedを270、Approach Timeを2.25へ戻した。