#はじめに
音ゲーを作ろうとした時に調べても具体的なソースコードやアルゴリズムに関する記事が無かったので、自己解決した方法を載せます。
分からないことがあったり、間違いがあればコメントお願いします。
#手順
1.譜面を作る
2.ノーツを作る
3.ノーツをタイミングに合わせて生成する
4.ノーツのタップ判定を作る
5.音楽を再生する
#1.譜面を作る
私はNoteEditorという外部Assetを使用して作りました。
NoteEditorの使い方はコガネブログさんが説明されてますので、そちらを参考にしてください。
ちなみにBPMが分からないものはBPMカウンターという便利なサイトがあるので使用してみてください。
#2.ノーツを作る
生成したいノーツを作りましょう。
後でノーツ生成から判定場所までかかる時間を取得するので、それを考慮したものの方が良いと思います。
今回は簡単に横移動のみのノーツでやっていきます。
とりあえずこんな感じです。
これはPrefabにしておきます。
#3.ノーツをタイミングに合わせて生成する
①譜面の解説
②譜面を読み込む
③読み取った情報からノーツを生成する
の順で説明していきます。
##①譜面の解説
先ほど作った譜面はJson形式で保存されています。
まず、そのファイルを見てみましょう。
{
"name": "MusicName",
"maxBlock": 5,
"BPM": 130,
"offset": 0,
"notes": [
{
"LPB": 8,
"num": 2,
"block": 4,
"type": 1,
"notes": []
},
{
"LPB": 8,
"num": 137,
"block": 1,
"type": 1,
"notes": []
},
こういった形式になっています。
こちらに書かれているもので使用しないものもあるので、使用したものを上から順に説明していきます。
###BPM
一分間に何回拍が打たれるかを数値にしたものです。
例えば、BPM120なら60秒間に120拍、つまり60/120で1拍0.5秒ということになりますね。
この1拍の時間というのは後々使うことになるので覚えておいてください。
###notes
1つのノーツの情報をまとめた配列になってます。
ここに書いてあるものも使うので説明していきます。
###LPB
私の知る限り音楽用語ではありません。
NoteEditor独自の用語ではないでしょうか?
これは1拍を何分割してノーツを置くかというものです。
NoteEditorでは下記のようになっています。
太い縦線が拍を意味していて、細線がここでいうLPBを意味しています。
例えば、BPM120でLPB8で、細線に二連続で置くと、その間隔は 60/120/8[秒] となりますね。
これも後々使います。
途中でLPBを変えると色々おかしくなってしまうので注意してください。
###num
これは開始の縦線を0、次の縦線を1としてどこの縦線に置かれているかの情報です。
ということは、縦線の間隔は均等なので開始からどのくらいの時間が必要なのかは**『60/BPM/LPB*num』**で分かりますね。
###block
これは横線の位置を意味しています。
上から0,1,2,3,4です。
私はノーツの種類を分ける時に使いました。
##②譜面を読み込む
UnityでJSONファイルを読み込むを参考に読み込みます。
using UnityEngine;
using System;
public class NotesGenerator : MonoBehaviour
{
[Serializable]
public class InputJson
{
public Notes[] notes;
public int BPM;
}
[Serializable]
public class Notes
{
public int num;
public int block;
public int LPB;
}
private int[] scoreNum;//ノーツの番号を順に入れる
private int[] scoreBlock;//ノーツの種類を順に入れる
private int BPM;
private int LPB;
void Awake()
{
MusicReading();
}
/// <summary>
/// 譜面の読み込み
/// </summary>
void MusicReading()
{
string inputString = scoreData.ToString();
InputJson inputJson = JsonUtility.FromJson<InputJson>(inputString);
scoreNum = new int[inputJson.notes.Length];
scoreBlock = new int[inputJson.notes.Length];
BPM = inputJson.BPM;
LPB = inputJson.notes[0].LPB;
for (int i = 0; i < inputJson.notes.Length; i++)
{
//ノーツがある場所を入れる
scoreNum[i] = inputJson.notes[i].num;
//ノーツの種類を入れる(scoreBlock[i]はscoreNum[i]の種類)
scoreBlock[i] = inputJson.notes[i].block;
}
}
}
これでscoreNum[]にすべての音符の場所データが入りました。
さてここから音楽のタイミングに合わせて生成を行います。
##③読み取った情報からノーツを生成する
一番の難所であろうところです。
いやぁ・・・苦戦したな・・・。
先に生成部分のソースコードを載せます。
using UnityEngine;
using System;
public class NotesGenerator : MonoBehaviour
{
[SerializeField]
private GameObject notesPre;
private float moveSpan = 0.01f;
private float nowTime;// 音楽の再生されている時間
private int beatNum;// 今の拍数
private int beatCount;// json配列用(拍数)のカウント
private bool isBeat;// ビートを打っているか(生成のタイミング)
void Awake()
{
InvokeRepeating("NotesIns", 0f, moveSpan);
}
/// <summary>
/// 譜面上の時間とゲームの時間のカウントと制御
/// </summary>
void GetScoreTime()
{
//今の音楽の時間の取得
nowTime += moveSpan; //(1)
//ノーツが無くなったら処理終了
if (beatCount > scoreNum.Length) return;
//楽譜上でどこかの取得
beatNum = (int)(nowTime * BPM / 60 * LPB); //(2)
}
/// <summary>
/// ノーツを生成する
/// </summary>
void NotesIns()
{
GetScoreTime();
//json上でのカウントと楽譜上でのカウントの一致
if (beatCount < scoreNum.Length)
{
isBeat = (scoreNum[beatCount] == beatNum); //(3)
}
//生成のタイミングなら
if (isBeat)
{
//ノーツ0の生成
if (scoreBlock[beatCount] == 0)
{
}
//ノーツ1の生成
if (scoreBlock[beatCount] == 1)
{
Instantiate(notesPre);
}
beatCount++; //(5)
isBeat = false;
}
}
}
(1)InvokeRepeatingを使いmoveSpan(0.01秒)ごとに処理を行っているためです。
(2)NoteEditorでいう、今どこの縦線にいるかの取得をします。
(3)scoreNum[]はノーツの場所の情報を順に入れているものなので、いまどこの縦線にいるかを見ているbeatNumと等しかったらノーツ生成のタイミングになります。
(4)生成が終わったら次のscoreNum[]の要素を見たいので足します。
さあこれでノーツの生成が終わりました。
#4.ノーツのタップ判定を作る
タップした時の判定を作っていきます。
生成からタップさせたいタイミングのところまでの移動でかかる時間からカウントダウンしていき、その差で判定します。
using UnityEngine;
using System;
public class NotesMove: MonoBehaviour
{
[SerializeField]
private float notesSpeed;
[SerializeField]
private Vector2 startPos;//ノーツの開始位置
[SerializeField]
private Vector2 judgePos;//判定したい場所
public static float moveSpan = 0.01f;//回すスパン
private float notesTime;
void Start()
{
notesTime = (startPos.x - judgePos.x) / notesSpeed;
InvokeRepeating("NotesMove", 0, moveSpan);
}
void NotesMove()
{
transform.position += new Vector3( -notesSpeed, 0f, 0f);
notesTime -= moveSpan;
NotesJudge();
}
void NotesJudge()
{
if(Math.Abs(notesTime) < 0.5f)
{
//判定した時の処理を書く
}
}
}
こんな感じです。
これを使い次の工程に移ります。
#5.音楽を再生する
音ゲーに欠かせないのが音楽です。
先ほどの生成のタイミングと音楽が噛み合わなかったら元も子もないですよね。
今のコードでは本来ノーツが判定されるタイミングで生成を行っています。
つまり、生成から判定したい場所までにかかる時間分ずれていることになります。
このずれを直していきます。
まず、譜面のnotes[0]に
"notes": [
{
"LPB": 8,
"num": 0,
"block": 0,
"type": 1,
"notes": []
},
を追加します。LPBは各自合わせてください。
次にNotesGeneratorに
using UnityEngine;
using System;
public class NotesGenerator : MonoBehaviour
{
[SerializeField]
private AudioSource gameAudio;
public static isAudioPlay=false;
//ノーツ0(音再生用ノーツ)の生成
if (scoreBlock[beatCount] == 0)
{
}
//音再生開始
void AudioPlay()
{
gameAudio.enabled = isAudioPlay;
}
}
を追加します。
そして、音再生用ノーツの設定です。
先ほど作ったノーツのprefabを複製し、タグをAudioPlayとしておきます。
コードは
using UnityEngine;
using System;
public class NotesMove: MonoBehaviour
{
void NotesJudge()
{
if(this.gameObject.tag=="AudioPlay")
{
if(notesTime =< 0)//判定位置に来たら
{
NotesGenerator.isAudioPlay= true;
}
}
}
}
を追加します。
これはどういうことかというと、NoteEditorでは音楽が開始したときにnum0になっています。
これはプレイヤーがノーツをタップしたいタイミングですよね。
ということはすべてnum0からの差分で生成するタイミングを取っているので、num0が判定場所に来てそのタイミングで音を開始したらずれません。
#完成
これで完成です。
誰かの力になれたら嬉しいです。