前回は、ノーツを叩いたときに判定結果を求めてコンソール上に表示する機能を作りました。
判定が終わったノーツを消す #
前回シーンを再生したときに、ノーツを叩いても叩いたノーツが消えなかったことに気づいたでしょうか。これは、本来必要な「ノーツが判定されたあとにそのノーツを消す処理」がないために起こっています。
実はこれまで、見た目だけでなく内部的(データ上)のノーツを消す処理も入れていませんでした。そのため、連続でキーを押すと同じノーツに対して何度も判定されてしまう状態になっていました。
これらを解消するために、
- 判定されたノーツと対応するゲームオブジェクトを消す
- 判定されたノーツを内部の
NotesListから消して判定対象から外す
という2つのことを行う必要があります。
NoteInfoに対応するゲームオブジェクトの情報を追加する
#
「1. 判定されたノーツと対応するゲームオブジェクトを消す」という機能を実装するには、NoteInfoをもとにして画面のオブジェクトを消す必要があります。そのため、まずはNoteInfoとそのNoteInfoに対応したノーツのゲームオブジェクトを紐づける必要があります。
まず、NoteInfoの内容を書き換えて、GameObjectを保存できるようにしておきます。
1using UnityEngine;
2
3public class NoteInfo
4{
5 public float time;
6 public int lane;
7 public GameObject gameObject;
8}変数gameObjectの定義に加え、GameObjectを使うのに必要なusing UnityEngine;も追加しました。
次に、NoteInfoの作成時にgameObjectを追加する処理をNotesGeneratorクラスのGenerateNotesメソッドに追加します。
1using System;
2using System.Collections.Generic;
3using UnityEngine;
4
5public class NotesGenerator : MonoBehaviour
6{
7 // ...(省略)
8
9 void GenerateNotes(string[] chart)
10 {
11 for (int line = 0; line < chart.Length; line++)
12 {
13 string currentLine = chart[line];
14 for (int laneIndex = 0; laneIndex < 4; laneIndex++)
15 {
16 if (currentLine[laneIndex] == '1')
17 {
18 // 座標を計算
19 float time = 60.0f / bpm * line;
20 float posZ = time * noteSpeed;
21 float posX = laneIndex - 1.5f;
22 Vector3 notePosition = new(posX, 0.0f, posZ);
23
24 // ノーツを生成
25 GameObject noteObj = Instantiate(note, notePosition, Quaternion.identity, transform);
26
27 // NoteInfoを作成
28 NoteInfo noteInfo = new() { time = time, lane = laneIndex + 1, gameObject = noteObj };
29 noteInfoList.Add(noteInfo);
30 }
31 }
32 }
33 }
34}これでNoteInfoとゲームオブジェクトを紐づけることができました。
対応するゲームオブジェクトの削除と、リストからの削除 #
次は、JudgementManagerクラスのJudgeメソッドを修正し、ノーツの判定が行われたときに以下の2つの処理を行うようにします。
- 判定されたノーツと対応するゲームオブジェクトを消す
ノーツのゲームオブジェクトを非表示(
SetActive(false))にする - 判定されたノーツを内部の
NotesListから消して判定対象から外す 判定されたノーツをnoteInfoListから削除(Remove)する
処理を追加してみましょう。
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;
public class JudgementManager : MonoBehaviour
{
// ...(省略)
void Judge(int lane)
{
// 最も近いノーツを探す
NoteInfo nearestNote = FindNearestNote(lane);
// もしノーツが見つからなかったら何もしない
if (nearestNote == null)
{
return;
}
// ノーツのベストタイミングを渡して、判定評価を取得する
string evaluation = GetNoteEvaluation(nearestNote.time);
// 判定対象外(NonTarget)でなければ、判定結果をコンソールに表示し、対応するノーツを削除する
if (evaluation != "NonTarget")
{
Debug.Log($"レーン{lane}: {evaluation}!");
nearestNote.gameObject.SetActive(false);
noteInfoList.Remove(nearestNote);
}
}
// ...(省略)
}以下の2行を追加しました。
-
nearestNote.gameObject.SetActive(false);Unityの機能で、オブジェクトを「非アクティブ(無効)」にして画面から見えなくしています。
-
noteInfoList.Remove(nearestNote);リストから判定が終わったノーツのデータを取り除く処理です。もしこれを忘れると、「画面からは見えないが、データ上はそこにノーツが存在し続ける」という状態になってしまいます。連続で何度もキーを押すと、見えないノーツが何度も判定されてしまうバグ(2重判定など)を防ぐために必要な処理です。
保存してUnityエディタからシーンを再生してみましょう。ノーツを叩くと同時に画面から消え、連打しても1回しか判定されなくなっていれば成功です!
ロストノートの削除 #
今の状態では、キーを押したときにだけ判定をするようになっているので、キーを何も押さずにノーツが判定線を通過した場合は、ノーツが判定されないまま残ってしまっています。これは本来であればMissとして処理する必要があります。
そこで、判定線を過ぎ、Miss判定よりも離れた場合はMissとして判定してノーツを削除できるようにしていきます。
実装 #
ノーツがMiss判定を超えて通り過ぎてしまった場合にMissにする処理を書いていきます。
これは、Updateメソッドで毎フレーム
- Missになるノーツがないか確認し、
- Miss判定の0.15秒を超えているノーツがあればMissとして判定する
とすれば良さそうです。
JudgementManagerクラスのGetNoteEvaluationメソッドの下にJudgeLostNoteメソッドを作ります。
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;
public class JudgementManager : MonoBehaviour
{
List<NoteInfo> noteInfoList = new();
NotesMover notesMover;
void Start()
{
noteInfoList = GetComponent<NotesGenerator>().noteInfoList;
notesMover = GetComponent<NotesMover>();
}
void Update()
{
JudgeLostNote();
// 現在のキーボード情報
var current = Keyboard.current;
// キーボードが接続されていなければ無視
if (current == null)
{
return;
}
// Dキーが押された瞬間なら
if (current.dKey.wasPressedThisFrame)
{
Judge(1);
}
// Fキーが押された瞬間なら
if (current.fKey.wasPressedThisFrame)
{
Judge(2);
}
// Jキーが押された瞬間なら
if (current.jKey.wasPressedThisFrame)
{
Judge(3);
}
// Kキーが押された瞬間なら
if (current.kKey.wasPressedThisFrame)
{
Judge(4);
}
}
// ...(省略)
// (JudgeLostNoteはGetNoteEvaluationメソッドの下に追加)
void JudgeLostNote()
{
// 1. スルーされた(Missになった)ノーツを探してリストに一時保存する
List<NoteInfo> lostNotes = new();
float elapsedTime = Time.time - notesMover.startTime;
foreach (NoteInfo noteInfo in noteInfoList)
{
float diffTime = elapsedTime - noteInfo.time;
// 判定線を過ぎていて、かつ判定範囲外まで離れてしまったノーツ
if (diffTime > 0.0f && GetNoteEvaluation(noteInfo.time) == "NonTarget")
{
lostNotes.Add(noteInfo);
}
}
// 2. 見つかったノーツをMiss扱いにして画面とリストから消す
foreach (NoteInfo lostNote in lostNotes)
{
Debug.Log($"レーン{lostNote.lane}: Miss!");
lostNote.gameObject.SetActive(false);
noteInfoList.Remove(lostNote);
}
}
}まず各ノーツに対してベストタイミングとのズレを計算して、判定線を過ぎていてかつ判定範囲外まで離れたノーツを調べています。
diffTime > 0.0f とすることで、「まだ届いていないノーツ」ではなく「すでに通り過ぎてしまったノーツ」だけを調べるようにしています。
対象となるノーツ(ロストノーツ)を見つけたら、とりあえず lostNotes というリストに一時保存して追加しています。
そして後半では、見つかったロストノーツに対して、これらをMissと判定してノーツを消す処理を行います。コンソールに表示するメッセージに Miss! と直接書き込んでいる以外は、記事前半で書いた処理とほとんど同じですね。
Q. なぜわざわざ別のリスト(lostNotes)に入れ直しているのか?
lostNotes)に入れ直しているのか?
「1つ目のforeachの中で見つけたら、その場ですぐにRemoveすればいいのでは?」と思うかもしれません。
しかし、C#のルールとして「foreachでリストを順番に調べている最中に、そのリスト自体から要素を削除(Remove)してはいけない」ことになっています。(実行するとエラーになります)
そのため、「消す予定のノーツ」を一度lostNotesという別のリストに書き出しておき、あとでまとめて消すという手順を踏んでいます。
保存して実行してみましょう。キーを叩かずにノーツをスルーすると、コンソールにMiss!と出るはずです。

まとめと次回予告 #
これで、リズムゲームの基礎となる仕組みが完成しました!
ステージの作成から始まり、譜面の生成、曲の再生、キー入力、そして今回の判定処理と、複雑な機能も多かったですが、ついにノーツを叩いて遊べる状態になりましたね。ここまで本当にお疲れ様でした!
しかし、今はまだコンソール画面に文字が出ているだけで、ゲーム画面上では自分が上手く叩けたのかどうかが分かりません。
次回は、画面に「Perfect」や「Miss」といった判定結果を表示する機能や、スコア・コンボの計算機能を実装し、より本格的なリズムゲームへと仕上げていきます!