top of page

音ゲー制作手順書#2

音ゲー制作手順書#2

こんにちは、アマギラスです。前回の記事の後半戦です。音ゲー制作手順書#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 を動的に切り替え

  • 難易度別譜面読み込み、タイトル画面、スコア保存、オンラインランキングなど、さらに発展可能です


この実装はあくまで"最低限動く"ことを優先したシンプルなものですが、構造を発展させれば市販レベルの音ゲーも作成可能です。次のステップとして、エディターの作成や演出の強化、ノート種類の追加などにチャレンジしてみてください。

最新記事

すべて表示
個人開発ゲーム制作者の「あるある」20連発

こんにちは、アマギラスです。個人でゲーム開発をしているとふとしたタイミングでアイデアが思い浮かんだり、途端にやる気がなくなったり、嫌気がさしたり…色々ありますよね? そんなあるあるを20個ほどご用意しました。個人開発ゲーム制作者の「あるある」20連発少しでも共感していただけ...

 
 
bottom of page