【Unity】ゲーム進行を管理する「GameManager」の作り方(ゲームの作り方チュートリアル「悪路王」その5)

悪路王 GameManager ゲームの作り方

前回までの作業でゲームに障害物が加わり、だいぶゲームらしくなってきました。ですがこのままでは何となく障害物をジャンプして避けるだけなので何かプレイヤーのモチベーションを刺激するような要素を入れたいものです。

そこでここではそのような要素として

  • 走行距離に応じてスコアが加算される
  • 一定距離を走るとレベルアップし、難易度が上がる

というような仕組みを作ってみることにします。こういった処理はゲームの裏方的な部分なので、「GameManager」と呼ばれるゲーム進行管理システムを作ってスコアやレベルの管理を行ってみましょう。

ゲーム進行を管理する「GameManager」について

では、はじめに「GameManagerとは何か?」ということから説明します。

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

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

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

そこで登場するのが「GameManager」です(※これはUnityの機能ではなく、「こういう風にしたら楽じゃないか?」という考え方なので注意してください)。GameManagerの具体的な役割はゲーム内容にもよりますが、例えば今回のゲームなら

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

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


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

GameManagerのC#スクリプト

ではスコア管理などを担うGameManagerのC#スクリプトは次のようになります。新しいC#スクリプトを作り、名前を「GameManager」に変更したら次のソースコードを書いてください。

using System.Collections;
using UnityEngine;
using UnityEngine.UI;

public class GameManager : MonoBehaviour
{

    [Header("必要なコンポーネントを登録")]
    [SerializeField]
    Transform playerTransform;
    [SerializeField]
    WallSpawner wallSpawner;
    [SerializeField]
    Text mileageText;
    [SerializeField]
    Text scoreText;
    [SerializeField]
    Text highScoreText;
    [SerializeField]
    Text levelText;

    [Header("ゲーム設定")]
    [SerializeField]
    int maxLevel = 10;
    [SerializeField, Min(1), Tooltip("LvUp毎に障害物の生成間隔を小さくするための除数")]
    float divisor = 1.15f;
    [SerializeField, Tooltip("スコアの桁数")]
    int scoreDigits = 8;
    [SerializeField, Tooltip("1メートルを何秒で走るか")]
    float secondsPerMeter = 0.075f;
    [SerializeField, Tooltip("1メートル走った時に加算される基本スコア")]
    int scorePerMeter = 10;
    [SerializeField, Tooltip("レベルアップに必要な走行距離")]
    int meterPerLevel = 100;

    bool timerIsActive = false;
    int level = 1;
    int mileage = 0;    //走行距離
    int maxScore = 0;
    int score = 0;
    int highScore = 0;
    Vector2 playerSpawnPosition;
    Coroutine timer;

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

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

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

    public int Mileage
    {
        get
        {
            return mileage;
        }
        set
        {
            mileage = Mathf.Max(value, 0);

            UpdateMileageUi();
        }
    }

    public int Level
    {
        get
        {
            return level;
        }
        set
        {
            level = Mathf.Max(value, 1);

            UpdateLevelUi();
        }
    }

    public bool IsActive { get; set; } = true;

    void Start()
    {
        playerSpawnPosition = playerTransform.position;

        InitGame();

        IsActive = true;
    }

    void Update()
    {
        if (!IsActive)
        {
            return;
        }

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

    public void StartGame()
    {
        playerTransform.position = playerSpawnPosition;
        playerTransform.gameObject.SetActive(true);

        InitGame();

        wallSpawner.IsActive = true;
        IsActive = true;
    }

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

        UpdateScoreUi();
        wallSpawner.InitSpawner();
    }

    IEnumerator MileageTimer()
    {
        timerIsActive = true;
        Mileage++;
        LevelUp();
        Score += scorePerMeter * level;

        yield return new WaitForSeconds(secondsPerMeter);

        timerIsActive = false;
    }

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

            //障害物の生成間隔を小さくする
            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();
    }

}

今回も比較的長いC#スクリプトになってしまいましたが、処理的には難しいことは何もやっていないので「よくわからないかも」という方も丁寧にソースコードを読んで処理を追ってみてください。

書けたらこれを適当な空のゲームオブジェクトにアタッチしておきましょう。

スコア等のUIの作成

さてこれでスコア加算などの処理ができたのですが、今のままではスコアやレベル・走行距離が目に見えないのでプレイヤーからしたら何が起きているのかが全然分からないと思います。そこでスコアの数値をUnity標準の「UI」機能を使って目に見える形で表示する部分を作ってみましょう。

主な作業手順は次のとおりです。

  1. シーンにキャンバスを作る
  2. キャンバスの子としてテキストを作る

シーンにキャンバスを作る

まずはシーンにキャンバスを作ります。キャンバスはUIのパーツを表示するためのパネルのようなもので、テキストやボタンを表示させるためには必須のゲームオブジェクトです。

作り方はヒエラルキーで右クリック→「UI」→「キャンバスグループ」(※おそらく翻訳ミス)から作ることができます。キャンバンスをシーンに追加すると巨大な四角形がシーンに出現しますが、これで正常なのでご安心ください。

キャンバスを作れたら設定を行います。このゲームオブジェクトをインスペクターで表示すると「Canvas Scaler」というコンポーネントがあると思うので、そこの「UIスケールモード」を「画面サイズに拡大」にしておいてください。

Canvas Scalerの設定

これでもし画面サイズが変わってもUIの大きさが自動的に変わるようになります。

キャンバスの子としてテキストを作る

次にスコア等を表示するテキストを作ってキャンバスに配置します。Unityのテキストは現状

  • TextMeshPro:新機能。綺麗なテキストを作れるが設定が複雑
  • 旧テキスト:旧式のテキスト機能。拡大するとテキストがぼやけるといった問題があるが、設定が簡単

という2種類があるのですが、ここでは簡単な旧式のテキスト機能を利用します。このテキストを作るには、ヒエラルキーで右クリック→「UI」→「古い機能」→「テキスト」を選択します。

旧式のテキストの作り方

なおこのとき必ずキャンバスの子としてテキストを作るようにしてください。キャンバスの子にしないとテキストが表示されません。

テキストを作れたらサイズを適当に調整し、フォントを最初にインポートした「MPLUS」に変更しておきましょう。そうしたら一旦テキストをプレハブ化し、次の画像のように4つのテキストをキャンバスに配置してください。

テキストの配置例

ここまでできたらあとはGameManagerにこれらのテキスト等を登録すればOKです。

GameManagerに必要なコンポーネントを登録する

テストプレイして動作を確認しよう

ではテストプレイして動作を確認してみましょう。距離に応じてスコアが加算され、レベルが上がると障害物の生成間隔が短くなるようになっていれば成功です。

悪路王 スコア加算の様子

(画像クリックでGIF再生)

なお、まだハイスコアのセーブ処理は作っていないのでゲームを終了するとハイスコアはリセットされてしまいます。セーブ処理は次回作ることにしましょう。

次のページ

【Unity】ゲームオーバー処理の作り方(ゲームの作り方チュートリアル「悪路王」その6)
前回までの作業でゲームとしては大体良い感じになってきました。しかしまだゲームオーバーなどの処理を実装していないため、ミスをしてもキャラクターが画面端から落ちていくだけでゲームがそのまま続いてしまいます。 そこでここでは プレイヤーがミスをし...