Week07_GameFlow - M-634/unity-game-dev-tutorial GitHub Wiki

📘 Week07 - ゲームループの構築


🎯 今週の目的

  • プレイ開始から終了(タイムアップ or プレイヤー死亡)までのゲームループを実装する
  • シーンを使って「タイトル → プレイ → リザルト」までの流れを構築する
  • GameManagerによるスコア・ゲーム時間・状態の一元管理
  • サバイバル型ゲームに必須なタイマーや状態制御を体験する

🛠 授業の流れ


🎬 1. シーン構成(3つ)

以下新規3つのSceneファイルをScenesフォルダ以下に作成する。

  • 今まで開発していたシーンはGameSceneへリネームすること
シーン名 役割
TitleScene ゲームの開始画面(スタートボタンなど)
GameScene 実際のプレイ画面(敵出現、成長、制限時間)
ResultScene 結果画面(スコア、リトライ、タイトルへ戻る)

🔧 2. Build Settings にシーンを追加

  1. 各シーンを Assets/Scenes に保存する
  2. Unity上部メニュー File > Build Settings... を開く
  3. Scenes In Buildに TitleScene, GameScene, ResultScene をドラッグアンドドロップで追加
  4. 並び順は上から TitleScene, GameScene, ResultScene になるように調整(起動時にロードされる)

SceneManager.LoadScene() で指定する名前はこの Build Settings に登録された名前と一致している必要があります。


🧠 3. GameManager(Singleton)による状態管理とスコア管理

using UnityEngine;
using UnityEngine.SceneManagement;

public class GameManager : MonoBehaviour
{
    public static GameManager Instance { get; private set; }

    public GameState CurrentState { get; private set; }
    public float ElapsedTime { get; private set; }
    public int KillCount { get; private set; }

    [SerializeField]
    private float _maxTime = 600f; // 10分
    public float MaxTime => _maxTime;

    private void Awake()
    {
        if (Instance != null) { Destroy(gameObject); return; }
        Instance = this;
        DontDestroyOnLoad(gameObject);
    }

    private void Update()
    {
        if (CurrentState != GameState.Playing) return;

        ElapsedTime += Time.deltaTime;

        if (ElapsedTime >= _maxTime)
        {
            EndGame(true); // タイムアップ勝利
        }
    }

    public void StartGame()
    {
        ElapsedTime = 0f;
        KillCount = 0;
        CurrentState = GameState.Playing;
    }

    public void AddKill()
    {
        KillCount++;
    }

    public void EndGame(bool isTimeUp)
    {
        CurrentState = isTimeUp ? GameState.TimeUp : GameState.GameOver;
        SceneManager.LoadScene("ResultScene");
    }
}

public enum GameState
{
    Title = 0,
    Playing = 1,
    GameOver = 2,
    TimeUp = 3
}

💡 4. SceneLoaderを作成 (静的クラス)

using UnityEngine;
using UnityEngine.SceneManagement;

public static class SceneLoader 
{
    public static void LoadGame()
    {
        SceneManager.LoadScene("GameScene");
    }

    public static void LoadTitle()
    {
        SceneManager.LoadScene("TitleScene");
    }

    public static void LoadResult()
    {
        SceneManager.LoadScene("ResultScene");
    }

    public static void QuitGame()
    {
        Application.Quit();
    }
}

⏳ 5. タイマーUIの実装(TextMeshProUGUI)

using UnityEngine;
using TMPro;

public class GameTimerUI : MonoBehaviour
{
    [SerializeField] 
    private TextMeshProUGUI _timerText;

      private void Start()
      {
          //GameSceneがロードされた瞬間にゲームスタート
          if (GameManager.Instance != null)
          {
              GameManager.Instance.StartGame();
          }
      }

      private void Update()
      {
          if (GameManager.Instance == null) return;
          
          float remain = Mathf.Max(0f, GameManager.Instance.MaxTime - GameManager.Instance.ElapsedTime);
          int minutes = Mathf.FloorToInt(remain / 60);
          int seconds = Mathf.FloorToInt(remain % 60);
          _timerText.text = $"{minutes:00}:{seconds:00}";
      }
}
  • GameScene上に空のゲームオブジェクトを作成してアタッチ
  • その後Timer用のテキストを用意する

🪦 6. PlayerHealth.cs に追記(プレイヤー死亡時の遷移処理)

追加部分のところをコメントしてます。

using UnityEngine;

public class PlayerHealth : MonoBehaviour
  {
      [SerializeField] 
      private int _maxHp = 5;

      [SerializeField]
      private PlayerHealthUI _playerHealthUI;
      
      private int _currentHp;
      private bool _isDead;
      
      void Start()
      {
          _currentHp = _maxHp;
          _playerHealthUI.SetHp(_currentHp, _maxHp);
      }

      public void TakeDamage(int damage)
      {
          if(_isDead) return;
          
          _currentHp -= damage;
          _playerHealthUI.SetHp(_currentHp, _maxHp);

          if (_currentHp <= 0)
          {
              OnDead();//追加・修正部分
          }
      }

      private void OnDead()
      {
          _isDead = true;
          GameManager.Instance?.EndGame(false);
          Destroy(gameObject);
      }
}

7. 🎮 Title.cs(タイトル画面用)作成

空のGameObjectをTitle.scene上で作成し、以下コンポーネントをアタッチする。


using UnityEngine;
using UnityEngine.UI;

public class Title : MonoBehaviour
{
    [SerializeField]
    private Button _loadGameButton;
    
    [SerializeField]
    private Button _quitGameButton;

    private void Start()
    {
        _loadGameButton.onClick.AddListener(OnStartButton);
        _quitGameButton.onClick.AddListener(OnQuitButton);
    }

    private void OnDestroy()
    {
        _loadGameButton.onClick.RemoveListener(OnStartButton);
        _quitGameButton.onClick.RemoveListener(OnQuitButton);
    }

    private void OnStartButton()
    {
        SceneLoader.LoadGame();
    }

    private void OnQuitButton()
    {
        SceneLoader.QuitGame();
    }
}


8. 🏁 Result.cs(リザルト画面用)

空のGameObjectをResult.scene上で作成し、以下コンポーネントをアタッチする。


using TMPro;
using UnityEngine;
using UnityEngine.UI;

public class Result : MonoBehaviour
{
    [SerializeField]
    private TextMeshProUGUI _killText;

    [SerializeField]
    private TextMeshProUGUI _timeText;
    
    [SerializeField]
    private Button _loadGameButton;
    
    [SerializeField]
    private Button _loadTitleButton;

    private void Start()
    {
        int kills = GameManager.Instance?.KillCount ?? 0;
        float time = GameManager.Instance?.ElapsedTime ?? 0f;

        _killText.text = $"Defeat Enemy Count: {kills}";
        _timeText.text = $"Survival Time Count : {Mathf.FloorToInt(time / 60):00}:{Mathf.FloorToInt(time % 60):00}";
        
        _loadGameButton.onClick.AddListener(OnRetryButton);
        _loadTitleButton.onClick.AddListener(OnTitleButton);
    }
    
    private void OnDestroy()
    {
        _loadGameButton.onClick.RemoveListener(OnRetryButton);
        _loadTitleButton.onClick.RemoveListener(OnTitleButton);
    }

    private void OnRetryButton()
    {
        SceneLoader.LoadGame();
    }

    private void OnTitleButton()
    {
        SceneLoader.LoadTitle();
    }
}


9. 🔧 Title と ResultシーンのUIを調整しよう

  • 動作の必要なButtonやTextを用意する
  • レイアウトの調整はおこのみで
  • Titleシーン上にGameManagerコンポーネントをアタッチした空オブジェクトを作成しておくこと

✅ 動作チェックリスト

  • TitleScene → GameScene へ遷移する
  • GameScene で 10分タイマーが進行し、0で終了
  • プレイヤーが死亡すると即座に終了
  • ResultScene に Kill数、時間が表示される
  • ResultScene から「Retry」「Title」へ戻れる
  • GameManager がスコアと時間を保持している

📝 課題

  • ResultSceneからTitleへ戻るか、Game画面をリスタートさせてみよう
  • ResultSceneで「Victory」or「GameOVer」のUI演出を表示しよう
  • ResultSceneに敵を倒した数(killCount)を正しく表示しよう
  • ResultScene に「ハイスコア」を記録する仕組みを追加しよう
    • 例)PlayerPrefs を用いた保存・読み出し
    • 例)JSONファイルでのスコア保存(応用)
// 保存
PlayerPrefs.SetInt("HighScore", GameManager.Instance.KillCount);
PlayerPrefs.Save();

// 読み込み
int best = PlayerPrefs.GetInt("HighScore", 0);
// JSON保存(応用)
[System.Serializable]
public class ScoreData
{
    public int highScore;
}

public void SaveToJson()
{
    ScoreData data = new() { highScore = GameManager.Instance.KillCount };
    string json = JsonUtility.ToJson(data);
    File.WriteAllText(Application.persistentDataPath + "/score.json", json);
}

🔗 参考リンク