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

【リズムゲームの作り方】 #5 譜面とノーツの生成

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

ノーツの追加
#

今回の記事では、リズムゲームのノーツを置く場所を決める譜面を制作し、それを読み込んでノーツを生成するスクリプトを作っていきます。

前々回にプレハブ化したNoteをSceneビュー上にドラッグ&ドロップすることでノーツを追加することができるのですが、この方法で一つ一つノーツを置いていくのは面倒ですし、リズムに合うように自分で座標を計算してノーツを置くのは大変です。

そこで、テキストファイルに譜面のデータを保存しておき、それを読み込んでノーツを自動で生成・設置する仕組みを作っていきます。

この仕組みを完成させると、このような譜面を作ることができます。

譜面の制作方法
#

リズムゲームの譜面には、さまざまな情報が必要です。「どのタイミングでノーツが流れてくるか」や「どのレーンにノーツがあるか」をはじめ、「曲のテンポ(BPM)」や「曲の拍子」、さらには、曲の途中でのテンポが変化する曲については「テンポ変化の情報」なども必要になります。

実際のリズムゲームではこれらすべてに対応する必要がありますが、すべての要素に対応しようとすると非常に複雑になってしまうため、今回は「4分音符ごとにノートがあるかないか」だけを記録して譜面を作っていきます。

この方法は、4分よりも細かいリズムや3連符などには対応できず、ホールドノーツなどもありませんが、まずはシンプルにこの仕様で作っていきます。

横列がレーン(左から1、2、3、4レーン)、縦列が時間経過(下の行に行くほど曲の後ろ)となるようにして、0がノーツなし、1がノーツありを示すことにします。これをテキスト形式で書いていきます。

下に今回使うサンプルの譜面を載せておきます。これはAssets/Sources/SampleChart.txtの内容と同じものになります。

SampleChart.txt
0000
0000
0000
0000
1000
0100
0010
0001
0001
0010
0100
1000
0010
0100
0001
1000
0110
1001
1111
0000
0000
0000

このテキストデータを読み込んでノーツを生成するスクリプトを作っていきます。

スクリプトの作成
#

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

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

NotesGeneratorをダブルクリックしてコードエディタでスクリプトを書いていきます。

今回は開始時に譜面を読み込んで、開始時にノーツを生成するため、毎フレーム実行するUpdateメソッドは使いません。そのためUpdateメソッドは削除します。また、もともと書いてあるコメントも消しておきます。

NotesGenerator.cs
1using UnityEngine;
2
3public class NotesGenerator : MonoBehaviour
4{
5    void Start()
6    {
7
8    }
9}

ノーツを生成する流れ
#

ノーツを生成する流れとしては、

  1. テキストファイルを読み込む
  2. ノーツを生成するためのfor文を作る
  3. テキストファイルの内容からノーツを置く座標を求める
  4. NoteNotesParentの子として生成し、座標を変更

となります。順番にスクリプトを書いていきましょう。

テキストファイルを読み込む
#

テキストファイルで書かれた譜面を読み込むメソッドを作っていきます。

まずテキストファイルを取得するための変数を作ります。テキストファイルはTextAssetという型を使います。Startメソッドの上にtextAssetという変数を追加します。

NotesGenerator.cs
 1using UnityEngine;
 2
 3public class NotesGenerator : MonoBehaviour
 4{
 5    [SerializeField] TextAsset textAsset;
 6
 7    void Start()
 8    {
 9
10    }
11}

SerializeFieldとしておくことで、Unityエディタ上から譜面となるテキストファイルを指定することができるようになります。

次に、読み込んだ譜面をchartという名前のstring型の配列に保存していきます。

Startメソッドに、譜面のテキストファイルをstringの配列に変換するスクリプトを追加します。

NotesGenerator.cs
 1using System;
 2using UnityEngine;
 3
 4public class NotesGenerator : MonoBehaviour
 5{
 6    [SerializeField] TextAsset textAsset;
 7
 8    void Start()
 9    {
10        // 譜面を読み込む
11        string[] chart = textAsset.ToString().Split('\n', StringSplitOptions.RemoveEmptyEntries);
12    }
13}

10~11行目に、譜面のテキストファイルをstringの配列に変換するメソッドを書きました。

読み込んだものをstring型に変換し、行ごとに分けてchartに代入しています。

1行目のusing System;StringSplitOptionsを使うのに必要なため追加しました。

ノーツを生成するためのfor文を作る
#

次は、譜面データの各行についてノーツを生成する仕組みを作っていきます。

譜面データは以下のような形式でしたね。

1000
0100
0010

これを「1行目の1列目が1なら1レーンにノーツ生成」「1行目の2列目が1なら2レーンにノーツ生成」…というように処理していきます。

そのため、外側のループで時刻を表す行を、内側のループでレーンを表す列を処理する、という二重ループを使います。

GenerateNotesという名前でメソッドを作り、譜面のテキストファイルの各行・各列について処理していくプログラムを書きます。

NotesGenerator.cs
 1using System;
 2using UnityEngine;
 3
 4public class NotesGenerator : MonoBehaviour
 5{
 6    [SerializeField] TextAsset textAsset;
 7
 8    void Start()
 9    {
10        // 譜面を読み込む
11        string[] chart = textAsset.ToString().Split('\n', StringSplitOptions.RemoveEmptyEntries);
12
13        // ノーツを生成
14        GenerateNotes(chart);
15    }
16
17    void GenerateNotes(string[] chart)
18    {
19        for (int line = 0; line < chart.Length; line++)
20        {
21            string currentLine = chart[line];
22            for (int laneIndex = 0; laneIndex < 4; laneIndex++)
23            {
24                if (currentLine[laneIndex] == '1')
25                {
26                    // ここで座標を計算してノーツを生成する
27                }
28            }
29        }
30    }
31}

処理の流れは次のようになっています。

  1. 外側のループ(19〜29行目)
    • chart配列の各行(line)を順番に処理
    • line = 0が譜面の1行目、line = 1が2行目…という感じです
  2. currentLineに1行分を取得(21行目)
    • "1000""0100"などが入る
  3. 内側のループ(22〜28行目)
    • 1行の中の各文字(0〜3番目)を順番にチェック
    • laneIndex = 0が1レーン目、laneIndex = 1が2レーン目…という感じです
  4. '1'ならノーツを生成(26行目)
    • その位置の文字が'1'だったら、ノーツを生成
    • 例:"1000"の0番目は'1'なので、1レーン目にノーツを生成
注意

配列のインデックスは0始まりであることに注意しましょう。currentLine[0]が1文字目、currentLine[1]が2文字目を表します。

テキストファイルの内容からノーツを置く座標を求める
#

譜面のテキストファイルが1の部分でノーツを生成する準備ができましたので、次は拍数とレーンから座標を計算して、実際にノーツを作成するプログラムを書いていきます。

さて、ノーツそれぞれに座標を設定する必要がありますが、Z座標(リズムゲームの縦方向)とX座標(リズムゲームの横方向)について分けて考えます。(ちなみに、Y座標(上下方向)は0です)

Z座標(縦・時刻で動く方向)
#

まずはZ座標、リズムゲームでの縦方向の座標を考えてみましょう。

ここで重要になるのは、「曲の開始から何秒後に、そのノーツが判定線ピッタリに到達するか」です。

NotesMoverで書いたコードを思い出すと、ノーツは1秒間にnoteSpeedだけ、-Z方向に動きます。そのため、ノーツのZ座標を noteSpeed * (曲が開始してからの秒数)にしておけば、想定したタイミングでノーツが判定線を通過することになります。

参考: NotesMoverUpdate()部分

void Update()
{
    float elapsedTime = Time.time - startTime;
    transform.position = new(0.0f, 0.0f, -noteSpeed * elapsedTime);
}

ではそのノーツが判定線を通過する時刻はどのように求めれば良いでしょうか?

これは、60 ÷ テンポ(BPM) × 拍数で求められます。テンポは4分音符が1分(60秒)間に何個入るかという数ですから、60 ÷ テンポとすれば、1拍あたりの秒数がわかります。

譜面のテキストデータでは1行あたり1拍で表していましたから、最終的に60 ÷ テンポ × 最初の行を0とした行番号で時刻を求められ、これにnoteSpeedを掛ければ求める座標が出てきます。

X座標(横・レーン方向)
#

Z座標を求めることができましたので、次は横方向、X座標について考えます。

各レーンの中心のX座標は、左のレーンから-1.5-0.50.51.5になっています。そこで、laneIndexを使い、laneIndex - 1.5fとすればX座標が求められそうです。

X座標とZ座標がわかりましたので、コードにしていきましょう。

ノーツの生成
#

  • NotesMoverからnoteSpeedを取得
  • 座標を求め、ノーツを生成

の2つをプログラムとして書いていきます。

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

NotesMoverも変更します。[SerializeField]にしていたnoteSpeedpublicにします。

このようにすることでNotesGeneratorからnoteSpeedの値を参照できるようになります。

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

ではコードについて解説します。

まず、NotesGeneratorの上の方では、いくつかの変数を追加しました。

[SerializeField] GameObject note;
[SerializeField] float bpm;
float noteSpeed;

noteは、以前プレハブ化したNoteを設定するための変数です。これを元にノーツを生成していきます。

bpmでは曲のテンポ(BPM)を設定します。ノーツの座標を計算するのに必要でしたね。

noteSpeedNotesMoverから取得した値を保存するための変数です。

次に、noteSpeedNotesMoverから取得する処理をStartメソッド内に追加しています。

// noteSpeedを読み込む
noteSpeed = GetComponent<NotesMover>().noteSpeed;

NotesMoverNotesGeneratorはどちらもNotesParentにアタッチしますので、自分自身のゲームオブジェクトにあるNotesMoverから値を取得するという処理を書いています。

そして、GenerateNotesでは、前の節で考えた方法で座標を計算しています。

float time = 60.0f / bpm * line;
float posZ = time * noteSpeed;
float posX = laneIndex - 1.5f;
Vector3 notePosition = new(posX, 0.0f, posZ);

求めた座標を使って、プレハブからオブジェクトを生成しています。

Instantiate(note, notePosition, Quaternion.identity, transform);

Unityでプレハブからオブジェクトを生成する際は、Instantiateというメソッドを使います。引数は順番に

  1. プレハブのゲームオブジェクト
  2. 座標
  3. 回転
  4. 親オブジェクト

となっています。

プレハブのゲームオブジェクトにはNoteを指定、座標は計算したものを指定、回転は無回転として、親を自分自身(NotesParent)としています。

これで指定した座標にノーツを生成することができます。

アタッチと実行
#

それではスクリプトを保存し、NotesGeneratorNotesParentにアタッチしていきましょう。

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

アタッチできたら、譜面データ、ノーツのプレハブ、テンポを設定します。

HierarchyウィンドウでNotesParentを選択した状態で、ProjectウィンドウのAssets > Sources > SampleChartをInspectorウィンドウのText Assetにドラッグ&ドロップします。

次に、HierarchyウィンドウでNotesParentを選択した状態で、ProjectウィンドウのAssets > PrefabにあるNoteをInspectorウィンドウのNoteにドラッグ&ドロップします。

さらに、Bpmの値を120にします。(今回使用するサンプル曲のBPMが120です)

これで実行してみましょう。

譜面のテキストデータを読み込んでノーツが生成され、レーンを流れました!

まとめと次回予告
#

今回は、譜面データを読み込んでノーツを生成しました。

二重for文での譜面データの読み込みや判定線を通過するタイミングの計算などの重めの実装が多く、コードもたくさん書いたため大変だったかもしれません。ただその分、見た目はリズムゲームっぽくなってきましたね!

次回は流れるノーツに合わせて曲を流していきます。

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