音ゲー制作手順書#2
- Amagirasu
- 7月7日
- 読了時間: 4分

こんにちは、アマギラスです。前回の記事の後半戦です。音ゲー制作手順書#2スクリプトを追加し、よりゲームらしくしていきましょう!
音ゲー制作目次
1. 譜面 JSON フォーマットとロード
譜面は Resources フォルダ以下の JSON ファイルとして保存します。構成例:
Assets/Resources/Charts/test_song_easy.json
{
"bpm": 140,
"offset": 0.15,
"notes": [
{"beat": 4.0, "lane": 0},
{"beat": 4.5, "lane": 1},
{"beat": 5.0, "lane": 2},
{"beat": 5.5, "lane": 3}
]
}
※Jsonファイルの作成は、テキストドキュメントで上記の文を貼りつけて、ファイルの種類をjsonにするとJsonファイルになります。
C# 側で受け取るためのクラスは以下のように定義します:
[System.Serializable]
public class NoteData
{
public double beat;
public int lane;
}
[System.Serializable]
public class Chart
{
public float bpm;
public float offset;
public List<NoteData> notes;
}
読み込み方法の例:
var text = Resources.Load<TextAsset>("Charts/test_song_easy");
Chart chart = JsonUtility.FromJson<Chart>(text.text);
2. ノートスポーナー (NoteSpawner.cs)
譜面データから時間に応じてノートを生成するスクリプトです。
using UnityEngine;
using System.Collections.Generic;
public class NoteSpawner : MonoBehaviour
{
public Transform[] lanes; // レーン位置
public GameObject notePrefab;
public string chartName = "test_song_easy";
List<NoteData> notes;
int nextIndex = 0;
float bpm;
float offset;
float noteSpeed = 5f;
float spawnY = 7f;
void Start()
{
var text = Resources.Load<TextAsset>($"Charts/{chartName}");
var chart = JsonUtility.FromJson<Chart>(text.text);
bpm = chart.bpm;
offset = chart.offset;
notes = chart.notes;
TimingManager.Instance.Init(bpm, offset);
}
void Update()
{
double songBeat = TimingManager.Instance.SongTime / 60d * bpm;
while (nextIndex < notes.Count && notes[nextIndex].beat - songBeat <= 8.0)
{
Spawn(notes[nextIndex]);
nextIndex++;
}
}
void Spawn(NoteData data)
{
Vector3 spawnPos = new Vector3(lanes[data.lane].position.x, spawnY, 0);
var obj = Instantiate(notePrefab, spawnPos, Quaternion.identity);
var note = obj.GetComponent<Note>();
note.speed = noteSpeed;
note.lane = data.lane;
}
}
このようにして、譜面に登録されたタイミングでノートが上から下に降ってくるようになります。
3. 判定・スコア・ゲージ処理
InputHandler.cs
using UnityEngine;
using UnityEngine.InputSystem;
public class InputHandler : MonoBehaviour
{
public NoteSpawner spawner;
void OnTapL() => TryHit(0);
void OnTapD() => TryHit(1);
void OnTapU() => TryHit(2);
void OnTapR() => TryHit(3);
void TryHit(int lane)
{
Note closest = null;
float minDist = float.MaxValue;
foreach (var note in FindObjectsOfType<Note>())
{
if (note.lane != lane) continue;
float dist = Mathf.Abs(note.transform.position.y);
if (dist < minDist)
{
minDist = dist;
closest = note;
}
}
if (closest != null)
{
var judge = TimingManager.Instance.CheckHit(closest.transform.position.y / 5f); // 簡易換算
if (judge != TimingManager.JudgeType.Miss)
{
TimingManager.Instance.Judge(closest, judge);
ScoreManager.Instance.AddScore(judge);
GaugeManager.Instance.OnJudge(judge);
}
}
}
}
ScoreManager.cs
using UnityEngine;
using TMPro;
public class ScoreManager : MonoBehaviour
{
public static ScoreManager Instance { get; private set; }
public TMP_Text scoreText;
public TMP_Text comboText;
int score;
int combo;
int totalNotes;
void Awake() => Instance = this;
public void Init(int total)
{
score = 0;
combo = 0;
totalNotes = total;
UpdateUI();
}
public void AddScore(TimingManager.JudgeType type)
{
if (type == TimingManager.JudgeType.Miss)
{
combo = 0;
}
else
{
combo++;
int baseScore = 1000000 / totalNotes;
score += baseScore;
}
UpdateUI();
}
void UpdateUI()
{
scoreText.text = $"Score: {score}";
comboText.text = $"Combo: {combo}";
}
}
GaugeManager.cs
using UnityEngine;
using UnityEngine.UI;
public class GaugeManager : MonoBehaviour
{
public static GaugeManager Instance { get; private set; }
public Slider gaugeBar;
int gauge = 100;
void Awake() => Instance = this;
public void OnJudge(TimingManager.JudgeType type)
{
if (type == TimingManager.JudgeType.Miss) gauge -= 5;
else gauge = Mathf.Min(gauge + 3, 100);
gaugeBar.value = gauge / 100f;
if (gauge <= 0)
{
GameOver();
}
}
void GameOver()
{
Debug.Log("GAME OVER");
// TODO: リザルト画面へ移行など
}
}
4. UI の配置 (Score, Combo, Gauge)
Canvas 上に以下の UI を配置します:
ScoreText(TMP Text) → 右上などに設置
ComboText(TMP Text) → 画面中央上に設置
GaugeBar(Slider) → 画面上部または下部に配置
各 UI は ScoreManager や GaugeManager にドラッグで参照を設定しておきます。
5. ゲームループ & リザルト表示
音楽再生 → プレイ → 終了までの一連の流れを管理します。
using UnityEngine;
using System.Collections;
public class GameManager : MonoBehaviour
{
public AudioSource music;
public float offset;
IEnumerator Start()
{
yield return new WaitForSeconds(1f);
music.PlayDelayed(offset);
yield return new WaitUntil(() => !music.isPlaying);
ShowResult();
}
void ShowResult()
{
Debug.Log("SHOW RESULT");
// TODO: リザルトUI表示、再挑戦ボタンなど
}
}
6. 動作確認とデバッグのコツ
判定ラインの位置と BPM を確認して譜面の offset を微調整する
AudioSource.time をログ出力して再生タイミングの誤差をチェック
判定幅を広めに設定してプレイし、精度を詰める段階で徐々に狭めていく
Unity の Time.captureFramerate を 60 に固定してリプレイ検証
7. 発展: ロングノート, BPM 変更, マルチレーン入力
ロングノート:HoldNote クラスを用意し、押し続けの判定を追加
BPM 変更:譜面 JSON に bpmChanges 配列を追加し、TimingManager で現在 BPM を動的に切り替え
難易度別譜面読み込み、タイトル画面、スコア保存、オンラインランキングなど、さらに発展可能です
この実装はあくまで"最低限動く"ことを優先したシンプルなものですが、構造を発展させれば市販レベルの音ゲーも作成可能です。次のステップとして、エディターの作成や演出の強化、ノート種類の追加などにチャレンジしてみてください。