ゲーム管理オブジェクト(GameManager)の作成&UIの追加

前回までの作業でゲームが一応動くようになりました。しかし、今のままだと障害物が流れる間隔が一定で単調ですし、スコアもないのであまり面白いゲームだとは言えません。

そこで、ここではゲーム管理オブジェクト(=GameManager)を作り、

  • 走行距離やレベル、スコア等の概念を導入
  • スコアを記録する
  • 1m走るごとにスコア加算する

といったことができるようにしていきましょう。


※地味な割に大変な部分ですが頑張りましょう。

スポンサーリンク

GameManagerについて&その役割

では、はじめに「ゲーム管理オブジェクトとは何か?」ということから説明します。

皆さんはゲームで遊んだことがあると思うので分かると思いますが、ゲームにはだいたい次のような流れがあります。[注1]

  1. タイトル画面
  2. ゲームスタート。キャラクターを操作してゴールを目指すなどする
  3. クリア、またはゲームオーバー
  4. リトライしたり、ほかのステージに進んだり、タイトル画面に戻ったりする

この大きな処理の流れを実現しようとするとき、色々なゲームオブジェクトを使ってアレコレやろうとすると、こんがらがって訳のわからないことになってしまいかねません。なので、こういうときは「映画監督」や「指揮者」のようなゲームオブジェクトがあれば良いなと思うことでしょう。

そこで登場するのが「ゲーム管理オブジェクト(GameManager)」です(※これはUnityの機能ではなく、「こういう風にしたら楽じゃないか?」という考え方なので注意してください)。このゲーム管理オブジェクトは

  • シーンに必ず1つだけ存在する
  • シーンをまたいで同じオブジェクトとして存在する

という特性を持ち、ゲーム進行を一手に管理するゲームオブジェクトとして定義します。具体的な役割はゲーム内容にもよりますが、例えば今回のゲームなら

  • ゲームの初期化
  • クリア・ゲームオーバー処理
  • レベルアップ判定
  • スコアの一時的な記録
  • スコア等のUIの操作

などが該当します。まあ、要するに裏方の仕事を色々こなすゲームオブジェクトというわけです。


[注1]:この流れはごく当たり前なので、初心者の方はこれを簡単に実現する機能がUnityにあるだろう、と思うかもしれません。しかし、実はUnityはデフォルトではこの流れを用意してくれません。なのでゲームを作るときはその辺の処理を自作する必要があります(とても地味な部分なのですが、意外と作り方がややこしいので初心者の方が詰まる原因になっているのではないか?と私は思います)。

GameManagerのC#スクリプト

それで、普通ならGameManagerを一から自作する必要があるのですが、最初に導入してもらった「くろくま基本アセット」にはこれのテンプレートが入っているので、このゲームでもそれを使います。

では早速、GameManagerのスクリプトを書き替えて必要な機能を追加しましょう。「Scripts」フォルダ→「System」内にGameManagerのスクリプトがあるので、それを次のように変更してください(「追加」と書かれている部分が処理などを追加した部分です)。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;	//追加

[RequireComponent(typeof(MoveSceneManager))]
[RequireComponent(typeof(SaveManager))]
[RequireComponent(typeof(SoundManager))]
[DefaultExecutionOrder(-5)]
public class GameManager : SingletonMonoBehaviour<GameManager>
{

  [Header("シーンロード時に自動生成するプレハブを登録")]
  [SerializeField]
  GameObject[] prefabs = null;

  //--ここから追加--
  [Header("ゲーム設定")]
  [SerializeField]
  int maxLevel = 10;
  [SerializeField, Min(1), Tooltip("LvUp毎に障害物の生成間隔を小さくするための除数")]
  float divisor = 1.1f;
  [SerializeField, Tooltip("スコアの桁数")]
  int scoreDigits = 8;
  [SerializeField]
  Vector2 playerSpawnPosition = Vector2.zero;
  [SerializeField, Tooltip("1メートルを何秒で走るか")]
  float secondsPerMeter = 0.05f;
  [SerializeField, Tooltip("1メートル走った時に加算される基本スコア")]
  int scorePerMeter = 10;
  [SerializeField, Tooltip("レベルアップに必要な走行距離")]
  int meterPerLevel = 100;
  [Header("UIの設定")]
  [SerializeField]
  string mileageTextName = "MileageText";
  [SerializeField]
  string scoreTextName = "ScoreText";
  [SerializeField]
  string highScoreTextName = "HighScoreText";
  [SerializeField]
  string levelTextName = "LvText";
  //--追加ここまで--

  MoveSceneManager moveSceneManager;
  SaveManager saveManager;
  SoundManager soundManager;

  //--ここから追加--
  bool timerIsActive = false;
  int level = 1;
  int mileage = 0;    //走行距離
  int maxScore = 0;
  int score = 0;
  int highScore = 0;
  Text mileageText;
  Text scoreText;
  Text highScoreText;
  Text levelText;
  WallSpawner wallSpawner;
  Coroutine timer;

  public int Score
  {
    set
    {
      score = Mathf.Clamp(value, 0, maxScore);

      if(score > highScore)
      {
        highScore = score;
      }

      UpdateScoreUi();
    }
    get
    {
      return score;
    }
  }

  public int HighScore
  {
    get
    {
      return highScore;
    }
  }
  //--追加ここまで--

  protected override void Awake()
  {
    base.Awake();

    if (Debug.isDebugBuild)
    {
      
    }

    moveSceneManager = GetComponent<MoveSceneManager>();
    saveManager = GetComponent<SaveManager>();
    soundManager = GetComponent<SoundManager>();
  }

  void Start()
  {
    if (Debug.isDebugBuild)
    {
      InstantiateWhenLoadScene();
      LoadComponents();   //追加
      GameStart();	//追加
    }
  }

  void Update()
  {
    if (moveSceneManager.SceneName == "Title")
    {
      if(timer != null)
      {
        StopCoroutine(timer);
      }

      return;
    }

    if (!timerIsActive)
    {
      timer = StartCoroutine("MileageTimer");
    }
  }

  public void InstantiateWhenLoadScene()
  {
    if(moveSceneManager.SceneName == "Title")
    {
      return;
    }

    foreach (GameObject prefab in prefabs)
    {
      Instantiate(prefab, transform.position, Quaternion.identity);
    }
  }

  //--ここから追加--
  public void InitGame()
  {
    level = 1;
    mileage = 0;
    maxScore = (int)Mathf.Pow(10, scoreDigits) - 1; //スコアの最大値を作成。例えば、8桁なら99999999
    score = 0;
    timerIsActive = false;
  }

  public void GameStart()
  {
    InitGame();
  }

  public void GameOver()
  {

  }

  public void Retry()
  {

  }

  IEnumerator Countdown()
  {
    yield break;
  }
  
  //シーン読み込み時に各種コンポーネントを取得するメソッド
  public void LoadComponents()
  {
    if (moveSceneManager.SceneName == "Title")
    {
      return;
    }

    wallSpawner = GameObject.FindGameObjectWithTag("WallSpawner").GetComponent<WallSpawner>();
    mileageText = GameObject.Find(mileageTextName).GetComponent<Text>();
    scoreText = GameObject.Find(scoreTextName).GetComponent<Text>();
    highScoreText = GameObject.Find(highScoreTextName).GetComponent<Text>();
    levelText = GameObject.Find(levelTextName).GetComponent<Text>();
  }

  IEnumerator MileageTimer()
  {
    timerIsActive = true;

    mileage++;
    LevelUp();
    UpdateMileageUi();

    Score += scorePerMeter * level;

    yield return new WaitForSeconds(secondsPerMeter);

    timerIsActive = false;
  }

  void LevelUp()
  {
    if(level < maxLevel && mileage % meterPerLevel == 0)
    {
      level++;
      UpdateLevelUi();

      //障害物の生成間隔を小さくする
      wallSpawner.MinWaitTime /= divisor;
      wallSpawner.MaxWaitTime /= divisor;
    }
  }

  void UpdateMileageUi()
  {
    mileageText.text = mileage.ToString() + "m";
  }

  void UpdateScoreUi()
  {
    scoreText.text = "Score: " + score.ToString("D" + scoreDigits.ToString());  //ToStringに特定の文字列を渡すと、桁数などを指定できる
    highScoreText.text = "High: " + highScore.ToString("D" + scoreDigits.ToString());
  }

  void UpdateLevelUi()
  {
    levelText.text = "Lv." + level.ToString();
  }

}

それから、もしシーンにGameManagerオブジェクトがない場合は「Prefabs」→「System」内にプレハブがあるので、それをシーンにドラッグ&ドロップしましょう。

ただし、このままゲームを実行するとエラーが出ると思います。なぜかというと、スコアなどを表示するUIを検索する処理が書かれているのですが、検索対象のUIをまだ作っていないからです。そこでUIをいくつか追加しましょう。

スコア表示等のUIの追加

「くろくま基本アセット」を導入している場合、デフォルトではタイトル画面以外のシーンを読み込んだ時、自動的に「MainCanvas」という名前のキャンバスが生成されるようになっています。なのでこのMainCanvasのプレハブに必要なUIを追加しましょう。

「Prefabs」→「UI」→「Canvas」の中に「Maincanvas」のプレハブがあるので、それを開いて下の図のようなUIを追加してください。

メインキャンバスにUIを追加した例

一応文字としても書いておくと、作る必要があるのは

  1. MileageText:走行距離
  2. ScoreText:スコア
  3. HighScoreText:ハイスコア
  4. LvText:レベル

という4つのテキストです(※GameManagerから名前で検索するので、各テキストの名前を間違えないようにしてください)。

シーン移動クラスのスクリプトの変更

最後に、後回しにすると忘れそうなので「くろくま基本アセット」のシーン移動クラス(MoveSceneManager、名前がちょっと変ですが気にしないでください)のスクリプトも少し変更しておきます。

スクリプトを見てもらうと「OnsceneLoaded」というメソッドがあるので、それを次のように書き換えてください。

//シーンのロード時に実行(最初は実行されない)
protected virtual void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
  gameManager.InstantiateWhenLoadScene();
  gameManager.LoadComponents();   //追加
  gameManager.GameStart();	//追加
}

シーンの切り替え時に、GameManagerの

  • 必要なコンポーネントを取得するメソッド
  • ゲームを開始するメソッド

が自動的に呼ばれるようにしました。

ここまできちんとできていれば、ゲームを実行したときに次のようになるはずです。

スコア等を実装した場合の例

(クリックで再生)


次のページ→ゲームオーバー&リトライ処理の作り方