ミニゲームを作ってUnityを学ぶ！  [ タンクウォーズ編 - 7. シーンを作り込む（前） ]

[ミニゲームを作ってUnityを学ぶ！[タンクウォーズ編]](https://qiita.com/YzRoid/items/544426ceffd4283d6977)
###**第7回目: シーンを作り込む（前）**
今回は前回実装したシーンを制御するスクリプトの土台について、ゲームの状態が「はじまり」から「プレイ中」に至るまでの具体的な処理を記述していきます。

また、具体的な内容を実装していく際にどうしても必要になってしまうUIや、それぞれのオブジェクトを一元管理するマネージャーも同時に実装していきます。



#戦車マネージャー
シーンの分岐先を実装する前に、まずはプレイヤーの操作する戦車と相手戦車をひとまとめにするためのマネージャーを実装します。

+ TankManagerという名前の空オブジェクトをゼロポジションに配置
+ TankManagerという名前のスクリプトを作成し、上記の空オブジェクトにアタッチ

```cs:TankManager.cs

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

    public class TankManager : MonoBehaviour
    {
        private List<TankModel> mTankList = new List<TankModel>();

        /// <summary>
        /// Tankタグのついたオブジェクトを戦車としてリスト保持する
        /// </summary>
        public void RegisterTanks()
        {
            GameObject[] tank = GameObject.FindGameObjectsWithTag(GameController.TAG_TANK);
            foreach (GameObject model in tank)
            {
                mTankList.Add(model.GetComponent<TankModel>());
            }
        }

    }

    // ↓TankModelは2台しかないので、Listを使わない場合はこちら

    [SerializeField]
    private TankModel mPlayer;

    [SerializeField]
    private TankModel mEnemy;

```

TankModelをまとめるためにはListを活用します。

TankManagerはRegisterTanks()が実行されるとゲーム内にあるタグ名がTankのオブジェクト全てについて、アタッチされているTankModelをListに格納し、戦車に対して何か処理が必要な際はListに格納されたTankModelの参照を使ってそれぞれのメソッドを呼び出します。

<Pre>
Listを利用した管理について

今回のゲームに登場する戦車の台数はプレイヤーの操作する1台と相手になる1台の合計で2台
ですので、コード下部分のようにListでなくインスペクタから個別に戦車オブジェクトを登録
してしまうこともできます。

ですが今後もし戦車の台数が増える場合などを考慮した場合はListを用いたコードのほうが
修正が容易になります。
</Pre>


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

###**SceneMainの修正**

SceneMainには状態(mState)としてINIT, PLAY, GAME_OVERの3種類が存在していましたが、ここで新しく状態を定義しなおします。

```cs:SceneMain.cs

        private enum STATE
        {
            WAIT_ENTER_KEY = 0,
            PLAY
        };
        private STATE mState = STATE.WAIT_ENTER_KEY;

        // 更新
        void Update()
        {
            switch (mState)
            {
                case STATE.WAIT_ENTER_KEY: // プレイヤーのEnterキー入力待ち
                    if (WaitEnter())
                    {
                        mState = STATE.PLAY;
                    }
                    break;
                case STATE.PLAY: // ゲームプレイ中
                    break;
            }
        }

        // Enterキーの入力を監視
        private bool WaitEnter()
        {
            if (Input.GetKeyDown(KeyCode.Return)) return true;
            return false;
        }

```

ゲームを起動した直後はSceneMainの状態がWAIT_ENTER_KEYになっています。
この状態のときUpdate()では更新毎にEnterキーが押されたかどうかの判定を行い、もし押された場合は状態をPLAYへ遷移させています。



###**TankModelの修正**

続いて戦車側の修正を行います。
今の段階ではSceneMainがどんな状態であってもPlayerTank自体はゲーム起動直後から操作できてしまいますが、これをSceneMainがPLAY状態になるタイミングで初めて操作できるよう変更していきます。

```cs:TankModel.cs

        [SerializeField]
        // プレイヤーの場合はtrue
        private bool mIsPlayer;

        public bool IsPlayer
        {
            get { return mIsPlayer; }
        }

        // 操作可能な場合はtrue
1:      private bool mIsActive;

        public bool IsActive
        {
            get { return mIsActive; }
        }

2:      public void OnActive()
        {
            mIsActive = true;
        }

        // HPが0の場合はtrue
        private bool mIsDead;

        public bool IsDead
        {
            get { return mIsDead; }
            set { mIsDead = value; }
        }

```

1: mIsActiveの初期値をtrueからfalseに変更
2: mIsActiveをtrueにするためのメソッドを追加

この修正により、プロジェクトを実行しても戦車はmIsActiveがtrueになるまで操作ができなくなりました。



###**ゲーム開始に合わせて戦車を操作可能にする**
それでは、動かなくなってしまった戦車をシーンがPLAY状態になるタイミングで操作可能にするために、TankManagerとSceneMainを修正していきます。

```cs:TankManager.cs

        public void OnActiveAllTanks()
        {
            foreach(TankModel model in mTankList)
            {
                model.OnActive();
            }
        }

```

TankManagerに新しくOnActiveAllTanks()を追加しました。
このメソッドでListに格納されている全ての戦車を操作可能な状態に変更します。

```cs:SceneMain.cs

        [SerializeField]
        [Tooltip("戦車マネージャー")]
1:      private TankManager mTank;

        void Update()
        {
            switch (mState)
            {
                case STATE.WAIT_ENTER_KEY: // プレイヤーのEnterキー入力待ち
                    if (WaitEnter())
                    {
追加                    mTank.RegisterTanks();
追加                    mTank.OnActiveAllTanks();
                        mState = STATE.PLAY;
                    }
                    break;
                case STATE.PLAY: // ゲームプレイ中
                    break;
            }
        }

```
1: フィールドを追加して空オブジェクトTankManagerをインスペクタから設定

SceneMainではEnterキーの入力を感知した際に、シーンに配置されているタグ名がTankのオブジェクト全てについてアタッチされたTankModel（戦車本体）をマネージャーのListに格納し、それら全ての戦車を操作可能状態に変更しています。



###**Enterキーを促すテキストを表示する**
これでプレイヤーがEnterキーを押したタイミングで戦車を操作できるようになりました。
次は実際にEnterキーを押してもらうためにそれを促すテキストを画面中央に表示し、Enterキーの入力を受け取ったタイミングでそのテキストが非表示になるようにします。

![tankwars_ss_7_1.jpg](https://qiita-image-store.s3.amazonaws.com/0/239877/b5c71777-e51d-0236-f02a-03438e75e662.jpeg)

+ 既存のCanvas内にPanelCetenrMsgという名前でPanelを作成
+ PanelにアタッチされたImageのColorを<font color="Blue">(R=0, G=0, B=0, A=100)</font>に変更
+ Panelのポジションを以下のように設定

![tankwars_ss_7_2.jpg](https://qiita-image-store.s3.amazonaws.com/0/239877/9bcb854f-5c20-343e-5535-b7aa29bcaae6.jpeg)

+ Pannel内にTextを作成
+ TextのColorを<font color="Blue">(R=255, G=255, B=255, A=255)</font>に変更
+ Textの赤枠部分とポジションを以下のように設定

![tankwars_ss_7_3.png](https://qiita-image-store.s3.amazonaws.com/0/239877/78d8c5ab-9ff5-5f75-87df-4009048e46fb.png)

中央にテキストを表示できたらそれを操作するためのマネージャーを新しく作成し、SceneMainにも対応するコードを追加します。

+ UiManagerという名前の空オブジェクトをゼロポジションに配置
+ UiManagerという名前のスクリプトを作成し、上記の空オブジェクトにアタッチ

```cs:UiManager.cs

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

    public class UiManager : MonoBehaviour
    {

        [SerializeField]
1:      private Image mPanelCenterMsg;

        /// <summary>
        /// センターテキストを背景のパネルごと非表示にする
        /// </summary>
        public void HideCenterMsg()
        {
            mPanelCenterMsg.gameObject.SetActive(false);
        }

    }

```

```cs:SceneMain.cs

        [SerializeField]
        [Tooltip("戦車マネージャー")]
        private TankManager mTank;

        [SerializeField]
        [Tooltip("UIマネージャー")]
2:      private UiManager mUi;

        void Update()
        {
            switch (mState)
            {
                case STATE.WAIT_ENTER_KEY: // プレイヤーのEnterキー入力待ち
                    if (WaitEnter())
                    {
追加                    mUi.HideCenterMsg();
                        mTank.RegisterTanks();
                        mTank.OnActiveAllTanks();
                        mState = STATE.PLAY;
                    }
                    break;
                case STATE.PLAY: // ゲームプレイ中
                    break;
            }
        }

```

1: PanelCenterMsgをインスペクタから設定
2: フィールドを追加して空オブジェクトUiManagerをインスペクタから設定



###**テキストの点滅アニメーション**
Enterキーの入力を受けたタイミングで中央のテキストを非表示にすることができましたので、追加でテキストが表示されている間は点滅アニメーションをさせることでゲーム開始前の画面に少しだけ動きをつけていきます。

```cs:UiManager.cs

        [SerializeField]
1:      private Text mTextCenterMsg;
        private readonly float VALUES_FLASH_CENTER_MSG_WAIT = 1.5f;
        private readonly float VALUES_FLASH_CENTER_MSG_ALPHA = 1.4f;
        private int mCenterMsgState;
        private float mCenterMsgWait;
        private float mCenterMsgAlpha = 1.0f;

        public void UpdateCenterMsg()
        {
            switch (mCenterMsgState)
            {
                case 0: // フェードアウト開始まで待機
                    mCenterMsgWait += Time.deltaTime;
                    if (mCenterMsgWait >= VALUES_FLASH_CENTER_MSG_WAIT) mCenterMsgState = 1;
                    break;
                case 1: // フェードアウト（完全には消さない）
                    mCenterMsgAlpha -= VALUES_FLASH_CENTER_MSG_ALPHA * Time.deltaTime;
                    if(mCenterMsgAlpha <= 0.1f)
                    {
                        mCenterMsgAlpha = 0.1f;
                        mTextCenterMsg.color = new Color(1.0f, 1.0f, 1.0f, mCenterMsgAlpha);
                        mCenterMsgState = 2;
                    }else
                    {
                        mTextCenterMsg.color = new Color(1.0f, 1.0f, 1.0f, mCenterMsgAlpha);
                    }
                    break;
                case 2: // 半透明状態からフェードイン
                    mCenterMsgAlpha += VALUES_FLASH_CENTER_MSG_ALPHA * Time.deltaTime;
                    if(mCenterMsgAlpha >= 1.0f)
                    {
                        mCenterMsgAlpha = 1.0f;
                        mTextCenterMsg.color = new Color(1.0f, 1.0f, 1.0f, mCenterMsgAlpha);
                        // 待機時間とstateをリセット
                        mCenterMsgWait = 0;
                        mCenterMsgState = 0;
                    }else
                    {
                        mTextCenterMsg.color = new Color(1.0f, 1.0f, 1.0f, mCenterMsgAlpha);
                    }
                    break;
            }
        }

```

```cs:SceneMain.cs

        void Update()
        {
            switch (mState)
            {
                case STATE.WAIT_ENTER_KEY: // プレイヤーのEnterキー入力待ち
追加                mUi.UpdateCenterMsg();
                    if (WaitEnter())
                    {
                        mUi.HideCenterMsg();
                        mTank.RegisterTanks();
                        mTank.OnActiveAllTanks();
                        mState = STATE.PLAY;
                    }
                    break;
                case STATE.PLAY: // ゲームプレイ中
                    break;
            }
        }

```

1: 文字列「Press the Enter key !」のTextをインスペクタから設定

UpdateCenterMsg()では、現在の状態mCenterMsgStateによってTextのアルファ値を加減算することで点滅アニメーションを実現しています。



#カウントダウンを実装する
起動直後からゲーム開始までの流れの中で、最後にもう1つだけ解決しなければいけない問題があります。
それは**Enterキーを押したタイミングでゲームが開始されてしまう**という点です。

Enterキーを押すということは、その瞬間に移動キーのWSもしくはマウスから手を放すことになりますので、ゲーム開始から即座に動ける相手戦車と比べてプレイヤー側が不利な状態になってしまいます。

この問題を解消するために、**Enterキーが押されたらカウントダウンを開始し、カウントゼロでゲームを開始する**という流れを実装していきます。

###**カウントダウンテキストの作成**

+ 既存のCanvas内にTextCountDownという名前でTextを作成
+ TextのColorを<font color="Blue">(R=255, G=255, B=255, A=255)</font>に変更
+ Textの赤枠部分とポジションを以下のように設定

![tankwars_ss_7_4.jpg](https://qiita-image-store.s3.amazonaws.com/0/239877/e28ad8c4-9466-56ec-0d7c-06b127eabc4d.jpeg)

+ インスペクタ上のオブジェクト名の横にあるチェックを外して非表示の状態にしておく
+ UiManagerクラスとSceneMainクラスを修正

```cs:UiManager.cs

        [SerializeField]
1:      private Text mTextCountDown;
        private readonly string[] COUNT_ARRAY = { "2", "1", "FIRE!" };
        
        private int mCurrentIndex;
        private float mDurationTime;

        /// <summary>
        /// カウントダウンアクション
        /// </summary>
        /// <returns>アクション完了でtrueを返す</returns>
        public bool UpdateCountDown()
        {
            switch (mCurrentIndex)
            {
                case 0: // カウントダウンテキストを表示
                    mTextCountDown.gameObject.SetActive(true);
                    mDurationTime = 1.0f;
                    mCurrentIndex++;
                    break;
                case 1: // 2を表示
                    mDurationTime -= Time.deltaTime;
                    if (mDurationTime <= 0.0f)
                    {
                        mTextCountDown.text = COUNT_ARRAY[0];
                        mDurationTime += 1.0f;
                        mCurrentIndex++;
                    }
                    break;
                case 2: // 1を表示
                    mDurationTime -= Time.deltaTime;
                    if (mDurationTime <= 0.0f)
                    {
                        mTextCountDown.text = COUNT_ARRAY[1];
                        mDurationTime += 1.0f;
                        mCurrentIndex++;
                    }
                    break;
                case 3: // Fire!を表示
                    mDurationTime -= Time.deltaTime;
                    if (mDurationTime <= 0.0f)
                    {
                        mTextCountDown.text = COUNT_ARRAY[2];

                        // コルーチンを使った遅延処理でテキストを1秒後に非表示にする
                        StartCoroutine(HideCountDownText());
                        return true;
                    }
                    break;
            }
            return false;
        }

        private IEnumerator HideCountDownText()
        {
            yield return new WaitForSeconds(1.0f);
            mTextCountDown.gameObject.SetActive(false);
        }

```

1:TextCountDownをインスペクタから設定

UpdateCountDown()では状態によって表示する文字を切り替えていき、最後のFire!の文字を表示した後はコルーチンを使った遅延処理で1秒後にTextを非表示にする処理を行っています。

```cs:SceneMain.cs

        private enum STATE
        {
            WAIT_ENTER_KEY = 0,
追加        COUNT_DOWN,
            PLAY
        };
        private STATE mState = STATE.WAIT_ENTER_KEY;

        void Update()
        {
            switch (mState)
            {
                case STATE.WAIT_ENTER_KEY: // プレイヤーのEnterキー入力待ち
                    mUi.UpdateCenterMsg();
                    if (WaitEnter())
                    {
                        mUi.HideCenterMsg();
変更                    mState = STATE.COUNT_DOWN;
                    }
                    break;
追加            case STATE.COUNT_DOWN: // カウントダウン
                    if (mUi.UpdateCountDown())
                    {
                        mTank.RegisterTanks();
                        mTank.OnActiveAllTanks();
                        mState = STATE.PLAY;
                    }
                    break;
                case STATE.PLAY: // ゲームプレイ中
                    break;
            }
        }

```
SceneMainでは新しくCOUNT_DOWNの状態を追加し、カウントダウンが終了した段階で戦車の操作が可能になるよう修正を行っています。

これでようやくゲームを開始する準備が整いました。
最後にプロジェクトを実行して、意図したとおりの動きになっているか確認します。

---

[次のページに進む](https://qiita.com/YzRoid/items/6d0124a5856d082eac6c)
[イントロダクションに戻る](https://qiita.com/YzRoid/items/544426ceffd4283d6977)
<BR>