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

【リズムゲームの作り方】 #6 曲を再生する・判定の準備

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

曲を再生する
#

リズムゲームには欠かせない、曲を再生する機能をつけていきます。

Unityで音楽を再生するには、Audio Sourceというものを作る必要があります。

  1. Hierarchyウィンドウの「+」ボタンをクリックします。
  2. Audio > Audio Source でAudio Sourceを作ります。

Positionは(0,0,0)にしておきます。

Audio Sourceに今回使用するサンプル曲を設定し、曲を流せるようにします。

HierarchyウィンドウでAudio Sourceを選択した状態で、Projectウィンドウの Assets > Sources > SampleSong をInspectorウィンドウのAudio Generatorにドラッグ&ドロップします。

また、Play On Awakeはオフにしておきます。

次に、曲を再生するスクリプトを作ります。

  1. ProjectウィンドウでAssets > Scriptsフォルダ内に移動します。
  2. 空いているところで右クリックします。
  3. Create > MonoBehaviour Scriptを選択してスクリプトを作成します。スクリプト名はMusicPlayerにします。

以下のコードを書いてください。

MusicPlayer.cs
 1using UnityEngine;
 2
 3public class MusicPlayer : MonoBehaviour
 4{
 5    [SerializeField] float songOffsetSec;
 6
 7    void Start()
 8    {
 9        AudioSource audioSource = GetComponent<AudioSource>();
10
11        if (songOffsetSec >= 0f)
12        {
13            audioSource.PlayDelayed(songOffsetSec);
14        }
15        else
16        {
17            float seekTime = Mathf.Min(-songOffsetSec, audioSource.clip.length - 0.01f);
18            audioSource.time = Mathf.Max(0f, seekTime);
19            audioSource.Play();
20        }
21    }
22}

このスクリプトでは、songOffsetSec の値によって曲の再生タイミングを変える処理を行っています。

songOffsetSecが0以上のプラスの値なら、「ノーツが流れてくるよりも曲が早く始まってしまう」状態なので、PlayDelayed を使って指定した秒数だけ曲の再生開始を遅らせる処理をしています。

逆にマイナスの値なら、「曲よりもノーツが先に流れてきてしまう」状態なので、audioSource.time に秒数を指定して、曲の途中から再生を始めることでタイミングを合わせています。このとき、曲の時間を超えてエラーにならないように Mathf.Min などを使って安全な値に直しています。

では今書いたMusicPlayerAudio Sourceにアタッチしましょう。

HierarchyウィンドウのAudio Sourceを選択してsongOffsetSecの値を変更すると、曲が早く流れ始めたり遅く流れ始めたりします。環境によってどのくらい調整するかは変わると思うので、タイミングが合うように調整しましょう。

私の環境では0.8くらいでちょうど良さそうでした。

判定
#

判定の概要
#

ここからは、リズムゲーム制作の山場となる判定について考えていきます。

判定とは、ノーツがタイミングよく叩かれたかを、理想的なタイミングと実際に叩かれたタイミングの差によって調べて表示するものです。

そのため、「ノーツが通過する時刻」と「キーが押された時刻」を比較して判定をします。

判定の流れを整理して、何が必要か考えてみましょう。

まず、キーが押されたときにそのイベントを受け取るメソッドが必要です。

次に、キーが押されたら、押されたキーと対応するレーンで判定を行うわけですが、そのレーンのどのノーツに対して判定するかを調べる必要があります。判定は一番近いノーツに対して行うため、現在時刻に最も近いノーツを調べる必要があります。

ノーツの情報について、今の時点ではテキストを加工したstring型の配列しかなく、時刻でノーツを検索するのが難しいので、ノーツの情報を確認しやすくするためのクラスを作ったほうが良さそうです。そのノーツが流れるレーンと、判定線を通り過ぎる時刻を持つ、NoteInfoクラスを作ります。

現在時刻に最も近いノーツを調べたら、そのノーツに対して現在時刻とノーツが判定線を通過する時刻の差を求めて判定評価(Perfect・Goodなど)を行います。

判定評価を行ったらその結果を表示し、さらに判定評価によってスコアやコンボを変化させます。

そして、判定を行ったらそのノーツを消します。

また、キーを押さずにノーツが通り過ぎた場合にミスとして扱い、コンボを途切れさせる処理も必要です。

まとめると、以下のことが必要です。

  1. キー入力を受け取るメソッドを作る
  2. ノーツの情報を持ったNoteInfoクラスを作る
  3. 譜面をNoteInfoのリストに変換する仕組みを作る
  4. レーンが一致し、現在時刻が最も近いノーツを調べる関数を作る
  5. 時刻の差から判定評価を行う関数を作る
  6. 判定評価を表示する
  7. スコア・コンボを変化させる
  8. ノーツを消す
  9. 通り過ぎたノーツをミスにする

必要な要素は多いですが、一つずつ完成させていきましょう。

これらの機能を書くスクリプトを作ります。

  1. Projectウィンドウで Assets > Scripts に移動して右クリックします。
  2. Create > Monobehaviour Script でスクリプトを作成します。名前はJudgementManagerにします。

キーの入力
#

まずは、「DFJKキーが押されたら判定を始める」という処理が必要でしたね。

キーが押されたときに、判定を行うメソッド「Judge」を呼び出すようにします。

Judgeは返り値なし、引数はint型のlaneにします。

JudgementManager.cs
 1using UnityEngine;
 2
 3public class JudgementManager : MonoBehaviour
 4{
 5    void Update()
 6    {
 7
 8    }
 9
10    void Judge(int lane)
11    {
12        // 判定処理
13    }
14}

Updateメソッド内に、DFJKの各キーが押されたら、対応するレーンでJudgeメソッドを呼ぶように処理を書きます。

JudgementManager.cs
 1using UnityEngine;
 2using UnityEngine.InputSystem;
 3
 4public class JudgementManager : MonoBehaviour
 5{
 6    void Update()
 7    {
 8        // 現在のキーボード情報
 9        var current = Keyboard.current;
10
11        // キーボードが接続されていなければ無視
12        if (current == null)
13        {
14            return;
15        }
16
17        // Dキーが押された瞬間なら
18        if (current.dKey.wasPressedThisFrame)
19        {
20            Judge(1);
21        }
22
23        // Fキーが押された瞬間なら
24        if (current.fKey.wasPressedThisFrame)
25        {
26            Judge(2);
27        }
28
29        // Jキーが押された瞬間なら
30        if (current.jKey.wasPressedThisFrame)
31        {
32            Judge(3);
33        }
34
35        // Kキーが押された瞬間なら
36        if (current.kKey.wasPressedThisFrame)
37        {
38            Judge(4);
39        }
40    }
41
42    void Judge(int lane)
43    {
44        // 判定処理
45    }
46}

2行目のusing UnityEngine.InputSystem;は、キーボードの入力を得るために必要な記述です。

Keyboard.current.aKey.wasPressedThisFrameのようにすることで、今のフレームが特定のキーが押された瞬間かどうかを調べることができます。これを使ってキーが押されたことを検知しています。

NoteInfoクラスの作成
#

正しい判定を行うには、「現在の時刻」と「ノーツが判定線を通過する時刻」を比較する必要があります。

そのための準備として、ノーツの通過時刻やレーンを記録する NoteInfo というクラスを作ります。すべてのノーツの情報をこの NoteInfo にまとめてリストにしておけば、ノーツの検索がしやすくなります。

まずはスクリプトを作成します。

  1. Projectウィンドウで Assets > Scripts に移動して右クリックします。
  2. Create > Monobehaviour Script でスクリプトを作成します。名前はNoteInfoにします。

時刻を持っておくfloat型の変数timeと、レーンを持っておくint型の変数laneを定義しておきましょう。

NoteInfoはノーツの情報を定義するだけなので、StartメソッドやUpdateメソッド、MonoBehaviourなどは不要です。デフォルトで書かれているコードをすべて消して、以下のように新しく書き直してください。

NoteInfo.cs
1public class NoteInfo
2{
3    public float time;
4    public int lane;
5}

NoteInfoの作成処理
#

NoteInfoのリストを作るには、譜面を読み込んですべてのノーツのレーンと時刻を取得する必要があります。この「譜面を読み込んでレーンと時刻を取得する」という処理はNotesGeneratorでしている処理と一緒なので、NotesGeneratorにリストを作ってもらおうと思います。

NotesGeneratorに処理を追加します。

NotesGenerator.cs
 1using System;
 2using System.Collections.Generic;
 3using UnityEngine;
 4
 5public class NotesGenerator : MonoBehaviour
 6{
 7    [SerializeField] TextAsset textAsset;
 8    [SerializeField] GameObject note;
 9    [SerializeField] float bpm;
10    float noteSpeed;
11    public List<NoteInfo> noteInfoList = new();
12
13    void Start()
14    {
15        // noteSpeedを読み込む
16        noteSpeed = GetComponent<NotesMover>().noteSpeed;
17
18        // 譜面を読み込む
19        string[] chart = textAsset.ToString().Split('\n', StringSplitOptions.RemoveEmptyEntries);
20
21        // ノーツを生成
22        GenerateNotes(chart);
23    }
24
25    void GenerateNotes(string[] chart)
26    {
27        for (int line = 0; line < chart.Length; line++)
28        {
29            string currentLine = chart[line];
30            for (int laneIndex = 0; laneIndex < 4; laneIndex++)
31            {
32                if (currentLine[laneIndex] == '1')
33                {
34                    // 座標を計算
35                    float time = 60.0f / bpm * line;
36                    float posZ = time * noteSpeed;
37                    float posX = laneIndex - 1.5f;
38                    Vector3 notePosition = new(posX, 0.0f, posZ);
39
40                    // ノーツを生成
41                    Instantiate(note, notePosition, Quaternion.identity, transform);
42
43                    // NoteInfoを作成
44                    NoteInfo noteInfo = new() { time = time, lane = laneIndex + 1 };
45                    noteInfoList.Add(noteInfo);
46                }
47            }
48        }
49    }
50}

ノーツを作成するときに時刻とレーンの情報を保存したNoteInfoをリストに入れる処理を追加しました。

laneIndex0 ~ 3でレーンが表されているため、NoteInfoとして保存する際は1だけ足しています。

今度は、作ったnoteInfoListJudgeManagerから読み取ります。

JudgementManager.cs
 1using System.Collections.Generic;
 2using UnityEngine;
 3using UnityEngine.InputSystem;
 4
 5public class JudgementManager : MonoBehaviour
 6{
 7    List<NoteInfo> noteInfoList = new();
 8
 9    void Start()
10    {
11        noteInfoList = GetComponent<NotesGenerator>().noteInfoList;
12    }
13
14    void Update()
15    {
16        // 現在のキーボード情報
17        var current = Keyboard.current;
18
19        // キーボードが接続されていなければ無視
20        if (current == null)
21        {
22            return;
23        }
24
25        // Dキーが押された瞬間なら
26        if (current.dKey.wasPressedThisFrame)
27        {
28            Judge(1);
29        }
30
31        // Fキーが押された瞬間なら
32        if (current.fKey.wasPressedThisFrame)
33        {
34            Judge(2);
35        }
36
37        // Jキーが押された瞬間なら
38        if (current.jKey.wasPressedThisFrame)
39        {
40            Judge(3);
41        }
42
43        // Kキーが押された瞬間なら
44        if (current.kKey.wasPressedThisFrame)
45        {
46            Judge(4);
47        }
48    }
49
50    void Judge(int lane)
51    {
52        // 判定処理
53    }
54}

これで全てのノーツの判定線通過時刻とレーンがわかるようになったので、最も近いノートを調べて判定を行えるようになりました。次の記事では、キーが押されたときに最も近いノートを探す処理を作っていきます。

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