メインコンテンツへスキップ

【リズムゲームの作り方】 #7 判定対象となるノーツを探す

目次
リズムゲームの作り方 - シリーズ
Part 7: この記事

前回は、NoteInfoクラスを作り、それぞれのノーツが判定線を通過する時刻とレーンを記録し、リストにまとめていました。

これを使って、今回はキーが押されたときに判定対象となる「タイミングが最も近いノーツ」を調べ、それについて判定を行う処理を作っていきます。

判定対象となるノーツの探し方
#

各ノーツに対してベストな判定タイミングである時刻を保存するところまではできました。では、いざレーンが押されたときに判定対象となるノーツはどう求めればいいでしょうか。

まず、押されたレーンに対応するレーンのノーツだけを判定対象にしたいですよね。押したレーンと別のレーンのノーツが判定されたら困ります。

そして、押したタイミングが最も近いノーツに対して判定することが必要になります。「ゲーム開始からの時間」と「各ノーツのベストタイミング」を比較して一番時刻が近いノーツに対して判定を行います。

「ゲーム開始からの時間」については、「ゲームを開始した時刻」と「現在の時刻」の差を求めればよいです。「ゲームを開始した時刻」はNotesMoverstartTimeですでに求めていたので、これをそのまま使いたいと思います。「現在の時刻」については、Time.timeで求められます。

また、「ゲーム開始からの時間」と「ノーツのベストタイミング」を比較する際、キーを押すのが理想より「少し早い場合」と「少し遅い場合」の差を正しく比べるため、時間のズレはマイナスをなくして絶対値にする必要があります。

これまでの探し方の手順をまとめると、

  1. リストに入っているすべてのノーツを順番に調べる
  2. 違うレーンのノーツは無視する
  3. レーンが同じなら、時間のズレ(絶対値)を計算する
  4. 今まで調べた中で一番ズレが小さいノーツを「最も時刻が近いノーツ」として記憶を更新していく

のようになります。これをコードにしていきます。

ゲームの開始時刻(startTime)を求められるようにする
#

まずはNotesMoverstartTimeをpublicにしておきましょう。publicにすることでJudgementManagerから値を取得することができるようになります。

NotesMover.cs
1using UnityEngine;
2
3public class NotesMover : MonoBehaviour
4{
5    public float noteSpeed;
6    public float startTime;
7
8    // ...(以下略)
9}

次にJudgementManagerクラスを開き、最も近いノーツを調べる処理を書いていきましょう。

JudgementManagerの上部とStart内に、NotesMoverからstartTimeを取得できるようにNotesMoverを記録しておく処理を作ります。

JudgementManager.cs
 1using System.Collections.Generic;
 2using UnityEngine;
 3using UnityEngine.InputSystem;
 4
 5public class JudgementManager : MonoBehaviour
 6{
 7    List<NoteInfo> noteInfoList = new();
 8    NotesMover notesMover;
 9
10    void Start()
11    {
12        noteInfoList = GetComponent<NotesGenerator>().noteInfoList;
13        notesMover = GetComponent<NotesMover>();
14    }
15
16    // ...(以下略)
17}

NotesMoverを保存しておくnotesMoverと、notesMoverを取得するGetComponentを追加しました。

これで、JudgementManagernotesMover.startTimeとすればNotesMoverstartTimeを取得できるようになりました。

リストから一番近いノーツを探し出す(FindNearestNote
#

それでは、リストから一番近いノーツを探し出すFindNearestNoteメソッドを作っていきます。

先ほど整理した4つの手順を使って作っていきます。

  1. リストに入っているすべてのノーツを順番に調べる
  2. 違うレーンのノーツは無視する
  3. レーンが同じなら、時間のズレ(絶対値)を計算する
  4. 今まで調べた中で一番ズレが小さいノーツを「最も近いノーツ」として記憶を更新していく
JudgementManager.cs
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;

public class JudgementManager : MonoBehaviour
{
    // ...(省略)

    void Judge(int lane)
    {
        // 判定処理
    }

    NoteInfo FindNearestNote(int lane)
    {
        // リズムゲーム開始からの経過時間を求める
        float elapsedTime = Time.time - notesMover.startTime;

        NoteInfo nearestNote = null;
        float minDiff = float.MaxValue;

        // 1. リストに入っているすべてのノーツに対して処理
        foreach (NoteInfo noteInfo in noteInfoList)
        {
            // 2. レーンが一致していなければ無視
            if (noteInfo.lane != lane)
            {
                continue;
            }

            // 3. 時間のズレを計算
            float currentDiffTime = Mathf.Abs(elapsedTime - noteInfo.time);

            // 4. 今までの最小のズレより小さければ更新
            if (currentDiffTime < minDiff)
            {
                minDiff = currentDiffTime;
                nearestNote = noteInfo;
            }
        }

        return nearestNote;
    }
}

まず「ゲーム開始からの時間」をelapsedTimeとして、Time.time - notesMover.startTimeで求めます。この時間を基準に、各ノーツについて時間のズレを計算していきます。

foreach文でリストを順番に調べる前に、結果を記憶しておくための初期値を2つ用意しておきます。

  • nearestNote(一番近いノーツを入れる箱)

    最終的に見つかったノーツを入れるための変数です。最初はまだ探し始めていないので、null(空っぽ)にしておきます。対象のレーンにノーツが一つもなかった場合は、このnullがそのまま返されることになります。

  • minDiff(最も小さい時間のズレ)

    今まで調べた中で「一番小さかった時間のズレ」を記憶しておく変数です。わざと大きい数字(float.MaxValue≒約340澗!)を入れておくのがポイントです。こうすることで、1つ目のノーツを調べたときに必ず「今までの最小記録より小さい」と判定され、最初の基準として正しく記録されるようになります。

foreach文によって各ノーツについて手順に沿って処理しています。

  • 2.の「違うレーンのノーツは無視する」

    if (noteInfo.lane != lane) でレーンが一致するかどうかを確認します。違うレーンの場合は continue; を使ってこの後の処理を飛ばし、次のノーツへ進みます。

  • 3.の「時間のズレを絶対値で計算する」

    レーンが同じだった場合は、Mathf.Absメソッドを使って、現在の経過時間とノーツが叩かれる理想の時間との差の絶対値を求め、currentDiffTime に入れます。

  • 4.の「より近いノーツに記録を更新する」

    今計算したズレ(currentDiffTime)が、これまでの最小のズレ(minDiff)よりも小さければ記録を更新します。最も近いノーツとして nearestNoteminDiff の両方を上書きします。

そして、すべてのノーツに対して処理を行ったあとに出てくるnearestNoteが、「レーンが一致する」「最も今の時間に近い」ノーツということになります。

最も近いノーツが取得できたか確認する
#

最後に、本当に一番近いノーツが見つかっているか、試しに画面(Console)に出力して確認してみましょう。 空になっているJudgeメソッドの中身に、処理を追加します。

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

public class JudgementManager : MonoBehaviour
{
    // ...(省略)

    void Judge(int lane)
    {
        // 最も近いノーツを探す
        NoteInfo nearestNote = FindNearestNote(lane);

        // もしノーツが見つからなかったら(nullなら)何もしない
        if (nearestNote == null)
        {
            return;
        }

        // 見つかったノーツの情報を表示してみる
        Debug.Log($"レーン{lane}でキーが押されました。一番近いノーツの時間は {nearestNote.time} です");
    }

    // ...(省略)
}

ここまで書き終わったら、JudgementManagerNotesParentにアタッチしましょう。

Projectウィンドウで、Assets > Scriptフォルダから、JudgementManagerをHierarchyウィンドウのNotesParentにドラッグ&ドロップします。

Consoleウィンドウを開き、どんな内容が出てくるか確認できるようにします。

これでゲームを再生してキーを押してみて、いい感じのタイミングの数字が表示されれば成功です!

ここまでの処理で、「キーを押したときに、一番近いノーツを見つけ出す」ことができるようになりました。次回は、いよいよこのノーツとの「時間のズレ」を使って、PerfectやGreatなどの判定を出していく処理を作っていきます。

現時点でのJudgementManager
#

JudgementManagerが長くなってきましたので、現時点でのコードを載せておきます。

JudgementManager.cs
 1using System.Collections.Generic;
 2using UnityEngine;
 3using UnityEngine.InputSystem;
 4
 5public class JudgementManager : MonoBehaviour
 6{
 7    List<NoteInfo> noteInfoList = new();
 8    NotesMover notesMover;
 9
10    void Start()
11    {
12        noteInfoList = GetComponent<NotesGenerator>().noteInfoList;
13        notesMover = GetComponent<NotesMover>();
14    }
15
16    void Update()
17    {
18        // 現在のキーボード情報
19        var current = Keyboard.current;
20
21        // キーボードが接続されていなければ無視
22        if (current == null)
23        {
24            return;
25        }
26
27        // Dキーが押された瞬間なら
28        if (current.dKey.wasPressedThisFrame)
29        {
30            Judge(1);
31        }
32
33        // Fキーが押された瞬間なら
34        if (current.fKey.wasPressedThisFrame)
35        {
36            Judge(2);
37        }
38
39        // Jキーが押された瞬間なら
40        if (current.jKey.wasPressedThisFrame)
41        {
42            Judge(3);
43        }
44
45        // Kキーが押された瞬間なら
46        if (current.kKey.wasPressedThisFrame)
47        {
48            Judge(4);
49        }
50    }
51
52    void Judge(int lane)
53    {
54        // 最も近いノーツを探す
55        NoteInfo nearestNote = FindNearestNote(lane);
56
57        // もしノーツが見つからなかったら何もしない
58        if (nearestNote == null)
59        {
60            return;
61        }
62
63        // 見つかったノーツの情報を表示してみる
64        Debug.Log($"レーン{lane}でキーが押されました。一番近いノーツの時間は {nearestNote.time} です");
65    }
66
67    NoteInfo FindNearestNote(int lane)
68    {
69        // リズムゲーム開始からの経過時間を求める
70        float elapsedTime = Time.time - notesMover.startTime;
71
72        NoteInfo nearestNote = null;
73        float minDiff = float.MaxValue;
74
75        // 1. リストに入っているすべてのノーツに対して処理
76        foreach (NoteInfo noteInfo in noteInfoList)
77        {
78            // 2. レーンが一致していなければ無視
79            if (noteInfo.lane != lane)
80            {
81                continue;
82            }
83
84            // 3. 時間のズレを計算
85            float currentDiffTime = Mathf.Abs(elapsedTime - noteInfo.time);
86
87            // 4. 今までの最小のズレより小さければ更新
88            if (currentDiffTime < minDiff)
89            {
90                minDiff = currentDiffTime;
91                nearestNote = noteInfo;
92            }
93        }
94
95        return nearestNote;
96    }
97}
リズムゲームの作り方 - シリーズ
Part 7: この記事