ひつじコレクション - 9. ゲームの開始と終了

[ミニゲームを作ってUnityを学ぶ！[ひつじコレクション編]](https://qiita.com/YzRoid/items/8dfd6c168b916ac82a0d)
###**第9回目: ゲームの開始と終了**

前回はサクサクっと画面内にミニマップを表示する機能を実装しました。
次はひと通り遊べるようになったひつじコレクションについて、ゲームとしての開始と終わりの概念を実装します。

今回の実装はゲームとしての見栄えに大きな変化はありませんが、オンラインランキングを実装する際など後々重要な仕組みになります。


#シーンを制御する
一般的なUnityのプロジェクトは例えばタイトル画面のシーン、実際にプレイするシーン、結果表示のシーンなど複数のシーンから構成されていて、それらを適切なタイミングで切り替えることでゲームとしての開始や終了を表現しています。

一方で今回のひつじコレクションのように1つのシーンで成り立ってしまう小規模なゲームの場合は、この開始と終了を**シーン内の状態を制御する**ことで表現します。

+ ゼロポジションに「SceneMain」という名前の空オブジェクトを配置
+ 同じく「SceneMain」という名前のスクリプトを作成し、それをSceneMainオブジェクトにアタッチ

```cs:SceneMain.cs:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

    public class SceneMain : MonoBehaviour
    {

        private GameController mGame;

        void Awake()
        {
            mGame = GameController.Instance;
            mGame.Init();
        }

        //--------
        // 状態 //
        //---------------------------------------------------------------------------------

        private enum STATE
        {
            LOAD_STAGE,
            PLAY,
            GAME_OVER
        }
        private STATE mState = STATE.LOAD_STAGE;

        //--------
        // 更新 //
        //---------------------------------------------------------------------------------

        void Update()
        {
            switch (mState)
            {
                // ステージの生成
                case STATE.LOAD_STAGE:
                    mGame.StageManager.LoadStage(1);
                    mState = STATE.PLAY;
                    break;
                // プレイ中
                case STATE.PLAY:
                    if (mGame.IsGameOver)
                    {
                        mState = STATE.GAME_OVER;
                    }
                    break;
            }
        }
    }

```

プロジェクト開始時にステージをファイルから生成してPLAY状態へ遷移。
GameControllerのプロパティ**「IsGameOver」**がtrueになったタイミングでGAME_OVER状態へ遷移。
このように適切なタイミングでシーンの状態「mState」を変更することによってUpdate()の処理を切り替えています。

また併せてGameControllerとStageManagerについて、SceneMainに対応するための修正を行います。

+ SceneMain#Awake()でGameControllerの初期化を行うため、StageManagerの該当部分を修正

```cs:StageManager.cs

    void Start()
    {
        GameController game = GameController.Instance;
        game.Init();
        game.StageManager = this;

        mConstructor = GetComponent<StageConstructor>();
        LoadStage(1);

        StartCoroutine("PopupFollower");
    }

↓以下のように修正

    void Start()
    {
        GameController.Instance.StageManager = this;
        mConstructor = GetComponent<StageConstructor>();

        StartCoroutine("PopupFollower");
    }

```

+ GameControllerを修正

```cs:GameController.cs

        public void Init()
        {
            Application.targetFrameRate = 30;
            Scorer = new Scorer();
        }

        //----------
        // フラグ //
        //---------------------------------------------------------------------------------

        public bool IsGameOver { get; set; }

```
GameControllerにはゲームオーバーに関するフラグを追加し、Init()ですっかり忘れていたフレームレートの設定を行いました。
これ以降、各スクリプトのUpdate()は1秒間に30回の頻度で呼び出されるようになります。


# Enterキーでゲームを開始する
今まではプロジェクトを実行した瞬間から操作可能な、いわゆるゲームが始まっている状態でしたが、この開始方法をプレイヤーが自分のタイミングで決定できるように修正していきます。

###Playerオブジェクト（ユニティちゃん）の制御
まずはユニティちゃんについて、プロジェクト開始時点ではプレイヤーの操作入力を受け付けない状態にします。

+ PlayerActionを修正

```cs:PlayerAction.cs

        void Start()
        {
            mTrans = GetComponent<Transform>();
            mRigid = GetComponent<Rigidbody>();
            mAnim = GetComponent<PlayerAnimation>();
修正        State = STATE.DEFAULT;
        }

        public void OnIdle()
        {
            State = STATE.IDLE;
        }

```

+ StageConstructorにメソッドを追加

```cs:StageConstructor.cs

        public void OnActivePlayer()
        {
            Player.GetComponent<PlayerAction>().OnIdle();
        }

```

初期状態のStateをIDLEからDEFAULTに変更し、新しくOnIdle()を追加しました。
これによってユニティちゃんはStageConstructor#OnActivePlayer()が実行されるまでプレイヤーの操作入力を受け付けない状態になります。

###チェイサーの制御
敵キャラクターについても同様に、プロジェクト開始時点ではAI行動を無効にしてその場に待機させます。

+ ChaserModelを修正

```cs:ChaserModel.cs

        public void Init(float speed, float incSpeed)
        {
            mSpeed = speed;
            mIncSpeed = incSpeed;
            mAi.ApplySpeed(mSpeed);

ここを削除  IsActive = true;
        }

```

+ StageConstructorにメソッドを追加

```cs:StageConstructor.cs

        public void OnActiveChaser()
        {
            foreach (ChaserModel model in mChaserList)
            {
                model.IsActive = true;
            }
        }

        public void OffActiveChaser()
        {
            foreach (ChaserModel model in mChaserList)
            {
                model.gameObject.SetActive(false);
            }
        }

```

敵キャラクターはStageConstructorのOnActiveChaser()が実行されるまでAIによる移動を行わずその場に待機します。
また同時に、全てのChaser~オブジェクトを非アクティブにするためのメソッド、OffActiveChaser()を追加しています。


###ポップアップの制御
さらにフォロワーのポップアップについて、こちらもプロジェクト開始時点ではポップアップさせるかどうかの判定を行わないようにします。

+ StageManagerを修正

```cs:StageManager.cs

        void Start()
        {
            GameController.Instance.StageManager = this;
            mConstructor = GetComponent<StageConstructor>();

ここを削除  StartCoroutine("PopupFollower");
        }

```


### ゲーム開始時の初期化
上記のユニティちゃん、敵キャラクター、ポップアップそれぞれに対応するため、StageManagerにゲーム開始時の初期化メソッドを追加します。

+ StageManagerにメソッドを追加

```cs:StageManager.cs

        public void StartGame()
        {
            // プレイヤーとチェイサーの行動開始
            mConstructor.OnActivePlayer();
            mConstructor.OnActiveChaser();

            // コルーチンを開始
            StartCoroutine("PopupFollower");
        }

```

### Enterキーを受け付ける
最後に、SeneMainにEnterキーの入力を監視する仕組みを実装します。

+ SceneMainを修正

```cs:SceneMain.cs

        private enum STATE
        {
            LOAD_STAGE,
追加        WAIT_ENTER_KEY,
            PLAY,
            GAME_OVER
        }

        void Update()
        {
            switch (mState)
            {
                // ステージの生成
                case STATE.LOAD_STAGE:
                    mGame.StageManager.LoadStage(1);
修正                mState = STATE.WAIT_ENTER_KEY;
                    break;
                // Enter入力でゲームを開始
追加            case STATE.WAIT_ENTER_KEY:
                    if (WaitEnter())
                    {
                        mGame.StageManager.StartGame();
                        mState = STATE.PLAY;
                    }
                    break;
                // プレイ中
                case STATE.PLAY:
                    if (mGame.IsGameOver)
                    {
                        mState = STATE.GAME_OVER;
                    }
                    break;
            }
        }

        //--------
        // 入力 //
        //---------------------------------------------------------------------------------

        private bool WaitEnter()
        {
            if (Input.GetKeyDown(KeyCode.Return)) return true;
            return false;
        }

```

以上でEnterキーによってゲームを開始する仕組みが完成しました。

![action_sheep_ss_9_1.jpg](https://qiita-image-store.s3.amazonaws.com/0/239877/3088f313-06f3-95c8-11a3-11f259722378.jpeg)

プロジェクトを実行後、Enterキーを押すことでゲームが開始します。


#ゲームの終了処理を実装
先ほどStageManagerにゲーム開始時の初期化メソッドを追加したように、同じくStageManagerに終了処理に関するメソッドを追加します。

+ StageManagerにメソッドを追加

```cs:StageManager.cs

        public void EndGame()
        {
            // チェイサーを除去
            mConstructor.OffActiveChaser();

            // コルーチン終了
            StopAllCoroutines();

            // フラグを立てる
            GameController.Instance.IsGameOver = true;
        }

```

EndGame()ではゲームの終了に必要な処理を行うと同時に、フラグを立てることでシーンの状態をGAME_OVERに遷移させています。

###アニメーションイベントの設定

ゲームオーバーのタイミングはプレイヤーがチェイサーと接触してその場で回転。
その後のダウンアニメーションで地面に倒れ込んだ直後に設定します。

+ PlayerActionにメソッドを追加

```cs:PlayerAction.cs

        public void EndDownAnimation()
        {
            GameController.Instance.StageManager.EndGame();
        }

```

今回のようにアニメーション再生時の特定タイミングでメソッドを呼び出す場合は第1回で解説したようにアニメーションイベントを利用します。

+ UnityChan/SD_unitychan/Animations/SD_unitychan_motion_humanoidを選択
+ Animationsタグ内、Clipsに表示されている項目からKneelDownのクリップを選択
+ さらに下に進み、Events項目を開いて以下のようにイベントを設定

![action_sheep_ss_9_3.jpg](https://qiita-image-store.s3.amazonaws.com/0/239877/4d0e1288-1254-3c87-1009-a61c3a4160f6.jpeg)


ユニティちゃんがその場に倒れ込んだタイミングでEndDownAnimation()が呼び出され、さらにStageManager.EndGame()が実行されます。


#リスタート機能を実装する
以上でゲームに開始と終了の概念を実装できました。
最後に、ゲームをはじめからやり直す機能を追加します。

+ GameControllerにシーンを再読み込みするメソッドを追加

```cs:GameController.cs

        //-------------
        // シーン管理 //
        //---------------------------------------------------------------------------------

        public void OnRestartButton()
        {
            SceneManager.LoadScene(SceneManager.GetActiveScene().name);
        }

```

+ SceneMainを修正してリスタートの入力を受け付ける

```cs:SceneMain.cs

        void Update()
        {
            CheckBackspaceInput();

            switch (mState)
            {
                // ステージの生成
                case STATE.LOAD_STAGE:
                    mGame.StageManager.LoadStage(1);
                    mState = STATE.WAIT_ENTER_KEY;
                    break;
                // Enter入力でゲームを開始
                case STATE.WAIT_ENTER_KEY:
                    if (WaitEnter())
                    {
                        mGame.StageManager.StartGame();
                        mState = STATE.PLAY;
                    }
                    break;
                // プレイ中
                case STATE.PLAY:
                    if (mGame.IsGameOver)
                    {
                        mState = STATE.GAME_OVER;
                    }
                    break;
            }
        }

        private void CheckBackspaceInput()
        {
            if (Input.GetKeyDown(KeyCode.Backspace)) mGame.OnRestartButton();
        }

```
SceneMainの現在のmStateに関係なく、BackSpaceキーの入力を感知したタイミングでシーンを再読み込みすることでゲームを開始前の状態へ戻します。

![action_sheep_ss_9_2.jpg](https://qiita-image-store.s3.amazonaws.com/0/239877/38bd918a-1335-8d3e-3610-d5e36dccb437.jpeg)

<font color="blue">＊ゲームをリスタートした際に画面が暗くなってしまう場合は以下の設定を行ってください。</font>

1. メニュー>Window>Lighting>Settingsを選択
2. Auto Generateのチェックを外す
3. Generate Lightingをクリック

### GameControllerについての注意
<font color="red">シーンを再読み込みすると各オブジェクトやそれにアタッチされているスクリプトなどは破棄されて新しく生成されなおしますが、シングルトンかつシーン内に配置されていないクラスGameControllerに関しては生成されている唯一のインスタンスが破棄されず同じインスタンスをそのまま参照する形になります。</font>

つまりゲームオーバーの際に一度trueになったIsGameOverフラグはシーンを再読み込みしてもtrueになったままの状態ですので、ゲームを完全に開始前の状態に戻すにはこのフラグを初期化する必要があります。

+ GameControllerを修正

```cs:GameController.cs

        public void Init()
        {
            Application.targetFrameRate = 30;
追加        IsGameOver = false;
            Scorer = new Scorer();
        }

```

![action_sheep_ss_9_1.jpg](https://qiita-image-store.s3.amazonaws.com/0/239877/3088f313-06f3-95c8-11a3-11f259722378.jpeg)

前回から見栄えは全く変わりありませんが、これでひつじコレクションに開始・終了の概念とやり直しの機能を実装することができました。

















---
[次のページに進む](https://qiita.com/YzRoid/items/9a8c7db065a868ace439)
[イントロダクションに戻る](https://qiita.com/YzRoid/items/8dfd6c168b916ac82a0d)
<BR>
<div><img src="http://unity-chan.com/images/imageLicenseLogo.png" alt="ユニティちゃんライセンス"><p>この作品は<a href="http://unity-chan.com/contents/license_jp/" target="_blank">ユニティちゃんライセンス条項</a>の元に提供されています</p></div>
<Br><Br>