【Unity】Unity始めました 〜サンプルを改修してみる〜

※画像クリックでリンクが開きます
<a href="https://www.amazon.co.jp/Unity%E3%81%AE%E6%95%99%E7%A7%91%E6%9B%B8-Unity-2018%E5%AE%8C%E5%85%A8%E5%AF%BE%E5%BF%9C%E7%89%88-3D%E3%82%B9%E3%83%9E%E3%83%BC%E3%83%88%E3%83%95%E3%82%A9%E3%83%B3%E3%82%B2%E3%83%BC%E3%83%A0%E5%85%A5%E9%96%80%E8%AC%9B%E5%BA%A7-Entertainment/dp/4797397659/ref=pd_bxgy_14_img_2?_encoding=UTF8&pd_rd_i=4797397659&pd_rd_r=ec9c33f9-2434-11e9-9e6a-91b30a74c408&pd_rd_w=xofR4&pd_rd_wg=PJF2o&pf_rd_p=a4de75e6-d8f7-4a34-bd69-503ea4866e6c&pf_rd_r=3TW3GC3JTEGBV1SP63N0&psc=1&refRID=3TW3GC3JTEGBV1SP63N0"><img border="0" src="https://images-na.ssl-images-amazon.com/images/I/514OQj8cqfL._SX391_BO1,204,203,200_.jpg" width="100"　></a>
`Unityの教科書 Unity 2018完全対応版 2D&3Dスマートフォンゲーム入門講座`

#はじめに
上記書籍4章の、 `車をゴールのギリギリ手前で止める「寸止めゲーム」` のサンプルを作ってみました。
こんな感じです。
<img width="902" alt="スクリーンショット 2019-02-01 11.39.52.png" src="https://qiita-image-store.s3.amazonaws.com/0/297525/6d93c796-bdea-0cce-aa3d-cf6a28a96ecd.png">
いざ作ってプレイしてみると、ゲームとして以下の欠点があることに気付きました。
(サンプルなので当たり前なのですが、、)

###欠点？
1.一見何をすれば良いのかわからない
2.スワイプで車が動くが、何度でもスワイプが有効
3.ゴールしてもしなくてもゲームとしての終わりが無い

もう少しゲームっぽくしたいなと思い、このサンプルに以下の仕様を追加したいと思います。

###改修！
1.ゲーム開始時にゲームの目的を提示する
2.スワイプは1回のみの一発勝負
3.プレイを評価して終わりを表現する

初心者ですので、サンプルのコードをベースに仕様を追加します。
「良いやり方がある」や「間違ってる」等御座いましたら、コメントを頂ければ幸いです。

#実践
##改修後に想定するゲーム仕様
※各画像ファイルは自作画像に差し替えてます
<img width="904" alt="スクリーンショット 2019-01-30 11.35.10.png" src="https://qiita-image-store.s3.amazonaws.com/0/297525/1934f052-c3ce-aa98-14aa-cdca51266130.png">
**ゲーム仕様**
・スワイプで車を動かし、ゴールのギリギリ手前で止める「寸止めゲーム」
　→ **車が動かせるのは１ゲームで1回**　※仕様追加1
　→ **車がゴールを越えてしまったらゲームオーバーを表示し、自動的にリトライ**　※仕様追加2
　→ **車がゴールから遠ざかった場合は自動的にリトライ**　※仕様追加3
　→ **いつでもリトライ出来るボタンを設置**　※仕様追加4

・画面中央のテキストで、ゲーム進行を表現する
　→ **ゲーム開始時の案内**　※仕様追加5
　→ ゴールまでの距離
　→ **停止位置に応じて評価する**　※仕様追加6

##スクリプトを改修する
このゲームは下記スクリプトが実装されており、これらに仕様を追加する。
1.`CarController`：スワイプによる車の動作に関するスクリプト
2.`GameDirector`：テキストの表示などUIに関するスクリプト

##CarController
###CarControllerで追加する仕様
>　→ **車が動かせるのは１ゲームで1回**　※仕様追加1

`CarController`は車の動作に関するコードなので、動作したかどうかの判定のみ実装。

```cs:改修前のCarControllerは何回でもスワイプ可能な実装となっている
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CarController : MonoBehaviour {

    float speed = 0;
    Vector2 startPos;

    void Start() {
    }

    void Update() {

        // スワイプの長さを求める
        if(Input.GetMouseButtonDown(0)) {
            // クリックした座標
            this.startPos = Input.mousePosition;
        } else if(Input.GetMouseButtonUp(0)) {
            // 離した座標
            Vector2 endPos = Input.mousePosition;
            float swipeLength = endPos.x - this.startPos.x;

            // スワイプの長さを初速度に変換する
            this.speed = swipeLength / 500.0f;
        }

        transform.Translate(this.speed, 0, 0);
        this.speed *= 0.98f;
    }
}
```

**改修↓**

```cs:改修後のCarController
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CarController : MonoBehaviour {

    bool movedFlg;
    float speed;
    Vector2 startPos;

    // Use this for initialization
    void Start(){
    }

    // Update is called once per frame
    void Update()
    {
        // ポイント１_1
        // 1度目のスワイプのみ有効
        if (this.movedFlg == false)
        {
            // スワイプの長さを求める
            if (Input.GetMouseButtonDown(0))
            {
                // クリックした座標
                this.startPos = Input.mousePosition;
            }
            else if (Input.GetMouseButtonUp(0))
            {
                // 離した座標
                Vector2 endPos = Input.mousePosition;

                // ポイント２
                // タップの場合は動作させない
                float swipeLength = endPos.x - this.startPos.x;
                if (swipeLength > 0 || swipeLength < 0)
                {
                    // 初速度設定
                    this.speed = swipeLength / 500.0f;

                    // ポイント１_2
                    this.movedFlg = true;
                }
            }
        }

        // 移動速度設定
        transform.Translate(this.speed, 0, 0);
        this.speed *= 0.98f;
    }
}
```

###ポイント1
・**ポイント1_1**　boolの`movedFlg`を設け1度のみ操作可能にする。　**→仕様追加1**

```cs
        // ポイント１_1
        // 1度目のスワイプのみ有効
        if (this.movedFlg == false)
```
・**ポイント1_2**　初速度が決まり、車が動いたタイミングでtrueに

```cs
                    // 初速度設定
                    this.speed = swipeLength / 500.0f;

                    // ポイント１_2
                    this.movedFlg = true;
```
###ポイント2
タップでは`movedFlg`がtrueにならないよう、スワイプしたかどうかの判定を追加。

```cs
                // ポイント２
                // タップの場合は動作させない
                float swipeLength = endPos.x - this.startPos.x;
                if (swipeLength > 0 || swipeLength < 0)
```

##GameDirector
###GameDirectorで追加する仕様
>　→ **車がゴールを越えてしまったらゲームオーバーを表示し、自動的にリトライ**　※仕様追加2
>　→ **車がゴールから遠ざかった場合は自動的にリトライ**　※仕様追加3
>　→ **ゲーム開始時の案内**　※仕様追加5
>　→ **停止位置に応じて評価する**　※仕様追加6

`GameDirector`はゲームの監督役なので、ゲームの進行を全て一任する。

```cs:改修前のGameDirectorはテキストのUIに距離のみ表示される実装となっている
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class GameDirector : MonoBehaviour {

    GameObject car;
    GameObject flag;
    GameObject distance;

    void Start() {
        this.car = GameObject.Find("car");
        this.flag = GameObject.Find("flag");
        this.distance = GameObject.Find("Distance");
    }

    void Update() {
        float length = this.flag.transform.position.x - this.car.transform.position.x;
        this.distance.GetComponent<Text>().text = "ゴールまで" + length.ToString("F2") + "m";
    }
}
```

**改修↓**

```cs:改修後のGameDirector
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;

public class GameDirector : MonoBehaviour
{
    GameObject car;
    GameObject flag;
    GameObject distance;
    string sceneName;
    float defaultLength;
    float length;
    bool finishFlg;

    // Use this for initialization
    void Start()
    {
        this.car = GameObject.Find("car");
        this.flag = GameObject.Find("flag");
        this.distance = GameObject.Find("Distance");

        // ポイント1_1
        this.sceneName = SceneManager.GetActiveScene().name;

        // ポイント2_1
        this.defaultLength = this.flag.transform.position.x - this.car.transform.position.x - 1.53f;
    }

    // Update is called once per frame
    internal void Update()
    {

        this.length = this.flag.transform.position.x - this.car.transform.position.x - 1.53f;

        if (this.finishFlg == false)
        {
            // ポイント2_2
            // 車が停止中のテキスト表示
            if (this.length.Equals(this.defaultLength))
            {
                this.distance.GetComponent<Text>().text = "スワイプでゴールを目指せ！\nゴールを越えたらゲームオーバー";
            }
            // ポイント2_3
            // 逆走したらリトライ
            else if (this.length > this.defaultLength)
            {
                // ポイント1_3
                Invoke("Retry", 1);
            }
            // 車が動作中のテキスト表示
            else if (this.length >= 0)
            {
                this.distance.GetComponent<Text>().text = "ゴールまで" + this.length.ToString("F2") + "m";

                // ポイント3_2
                Invoke("GetScore", 10);
            }
            // ポイント2_4
            // ゴールを超えたらゲームオーバー
            else
            {
                this.distance.GetComponent<Text>().text = "ゲームオーバー";

                // ポイント1_3
                Invoke("Retry", 1);
            }
        }
    }

    // ポイント3_1
    void GetScore()
    {
        this.finishFlg = true;

        if (this.length < 0.5)
        {
            this.distance.GetComponent<Text>().text = "perfect!!!";
        }
        else if (this.length < 1)
        {
            this.distance.GetComponent<Text>().text = "great!!";
        }
        else if (this.length < 1.5)
        {
            this.distance.GetComponent<Text>().text = "good!";
        }
        else
        {
            this.distance.GetComponent<Text>().text = "too bad...";
        }

        // ポイント1_3
        Invoke("Retry", 2);
    }

    // ポイント1_2
    public void Retry()
    {
        SceneManager.LoadScene(this.sceneName);
    }
}
```

###ポイント1　リトライの実装
・**ポイント1_1**　ゲーム開始時のシーンを取得しておく。
　※`using UnityEngine.SceneManagement;`を宣言する必要有り

```cs
        // ポイント1_1
        this.sceneName = SceneManager.GetActiveScene().name;
```
・**ポイント1_2**　`ポイント1_1`で取得したゲーム開始時のシーンを実行させるメソッドを作成する。
　※後述するRetryボタン(仕様追加4)からも呼び出したいのでメソッドはpublicに

```cs
    // ポイント1_2
    public void Retry(){
        SceneManager.LoadScene(this.sceneName);
    }
```
・**ポイント1_3**　リトライさせたい時に引数に指定した秒数後に`ポイント1_2`で作成したメソッドを実行させるように実装。

```cs
            // ポイント1_3
            Invoke("Retry", 1);
``` 
###ポイント2　テキストUIを変化させる
・**ポイント2_1**　ゲーム開始時の車とゴールの位置を取得しておく。

```cs
        // ポイント2_1
        this.defaultLength = this.flag.transform.position.x - this.car.transform.position.x -1.53f;
```
・**ポイント2_2**　車とゴールの距離が初期値から変わっていない場合にゲーム開始時の案内を表示する。 **→仕様追加5**

```cs
            // ポイント2_2
            // 車が停止中のテキスト表示
            if (this.length.Equals(this.defaultLength))
            {
                this.distance.GetComponent<Text>().text = "スワイプでゴールを目指せ！\nゴールを越えたらゲームオーバー";
            }
```
・**ポイント2_3**　車とゴールの距離が離れたら(車がゴールから遠ざかった場合は)1秒後にリトライ。 **→仕様追加3**

```cs
            // ポイント2_3
            // 逆走したらリトライ
            else if (this.length > this.defaultLength)
            {
                // ポイント1_3
                Invoke("Retry", 1);
            }
```
・**ポイント2_4**　車とゴールの位置関係がマイナスの値になったらゲームオーバーを表示し、1秒後にリトライ。 **→仕様追加2**

```cs
            // ポイント2_4
            // ゴールを超えたらゲームオーバー
            else
            {
                this.distance.GetComponent<Text>().text = "ゲームオーバー";

                // ポイント1_3
                Invoke("Retry", 1);
            }
```
###ポイント3　評価する
・**ポイント3_1**　距離に応じて評価するメソッドを作成する。1秒後にリトライ。

```cs
    // ポイント3_1
    void GetScore()
    {
        this.finishFlg = true;

        if (this.length < 0.5)
        {
            this.distance.GetComponent<Text>().text = "perfect!!!";
        }
        else if (this.length < 1)
        {
            this.distance.GetComponent<Text>().text = "great!!";
        }
        else if (this.length < 1.5)
        {
            this.distance.GetComponent<Text>().text = "good!";
        }
        else
        {
            this.distance.GetComponent<Text>().text = "too bad...";
        }

        // ポイント1_3
        Invoke("Retry", 2);
    }
```
・**ポイント3_2**　車が動作して10秒後に`ポイント3_1`で作成したメソッドを実行させるように実装。 **→仕様追加6**

```cs
            // 車が動作中のテキスト表示
            else if (this.length >= 0)
            {
                this.distance.GetComponent<Text>().text = "ゴールまで" + this.length.ToString("F2") + "m";

                // ポイント3_2
                Invoke("GetScore", 10);
            }
```

##Retryボタンを設置する
>　→ **いつでもリトライ出来るボタンを設置**　※仕様追加4

###配置位置を決める
<img width="286" alt="スクリーンショット 2019-02-22 11.41.37.png" src="https://qiita-image-store.s3.amazonaws.com/0/297525/1bac528b-321d-454c-6ba9-4c35a26f0cae.png">
・下記キーを押下しながら指定すると、その位置を基準に再計算してくれる。
　→ポジションやサイズはこれを行ってから微修正する
mac:`option + shift`
win:`Alt + Shift`

###スクリプトを割り当てる
<img width="403" alt="スクリーンショット 2019-02-22 11.59.01.png" src="https://qiita-image-store.s3.amazonaws.com/0/297525/ff017da5-28ce-c5a1-1bb9-fc91c14f92cf.png">
・クリック時に`GameDirector`の`Retry`を呼び出すように指定。 **→仕様追加4**
　※呼び出すメソッドはpublicにしておく必要あり

##細かい設定
###プレイ環境に合わせたサイズ比を設定
<img width="253" alt="スクリーンショット 2019-02-22 12.03.19.png" src="https://qiita-image-store.s3.amazonaws.com/0/297525/734a785d-e3d8-b125-b2ed-d6ec70a5c6b7.png">

###実機に合わせたプラットフォームでビルド
<img width="263" alt="スクリーンショット 2019-02-22 12.04.56.png" src="https://qiita-image-store.s3.amazonaws.com/0/297525/7f6bdf0e-957c-1479-5c23-b7b987e6a12b.png">

#完成！
<img width="666" alt="スクリーンショット 2019-02-22 12.15.34.png" src="https://qiita-image-store.s3.amazonaws.com/0/297525/0bedb54f-5495-f155-51ea-59e6e499257d.png">
**↓**
<img width="669" alt="スクリーンショット 2019-02-22 12.17.43.png" src="https://qiita-image-store.s3.amazonaws.com/0/297525/f57beba1-f01b-805e-1ac2-ed666075e2aa.png">
**↓**
<img width="665" alt="スクリーンショット 2019-02-22 12.12.54.png" src="https://qiita-image-store.s3.amazonaws.com/0/297525/9c315bc8-0027-f1c5-e9b6-7f361ab37e46.png">
**↓**
<img width="673" alt="スクリーンショット 2019-02-22 12.12.58.png" src="https://qiita-image-store.s3.amazonaws.com/0/297525/601f3716-1884-1eb2-a288-7149f98b95e9.png">

#所感
```cs
// ポイント3_2
                Invoke("GetScore", 10);
```

評価メソッド`GetScore()`を呼び出す際、`Invoke()`を用いて秒数指定しておりますが、ここは車が止まっているかどうかで判断したかったです。
`Rigidbody`を使うと`IsSleeping()`を使ってオブジェクトが動いているかどうかを判定できるようなので色々試しましたが、
今回はオブジェクトの操作を物理特性ではなく計算式で設定しているので、実現できませんでした。
PC上のテストプレイとスマートフォンの動作に乖離があったり躓くこともありますが、まだまだスタート地点なので勉強を続けていきたいと思います。