【Unity2D】Flappy Bird を作るチュートリアル (3/3)

# 今回やること
1. [x] プロジェクトの作成
2. [x] プレイヤーの作成
3. [x] ブロック（障害物）の作成
4. [x] ブロック管理の作成
5. [ ] スコアの実装　←③
6. [ ] ゲームオーバーの実装　←③
7. [ ] リトライの実装　←③

最後にゲーム的な要素である「スコア」の実装と、ゲームオーバー表示、リトライを実装します。

# ページのリンク

* [Flappy Bird を作るチュートリアル (1/3)](https://qiita.com/2dgames_jp/items/59b361cf430d3dc6584b)
* [Flappy Bird を作るチュートリアル (2/3)](https://qiita.com/2dgames_jp/items/95f598dd9044c258df88)
* Flappy Bird を作るチュートリアル (3/3) ←今のページ

# スコアの実装
## ゲーム管理オブジェクトの作成
スコアなどゲーム全体を管理するオブジェクトを作成します。
最初にダウンロードした素材フォルダから「nasu.png」を Projectビューにドラッグ＆ドロップします。
<img width="395" alt="スクリーンショット_2019_11_27_21_42.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/67438d13-f190-24ed-17d5-5aece365c83f.png">

続けて、作成した「nasu」スプライトを、Hierarchyビューにドラッグ＆ドロップします。
<img width="440" alt="スクリーンショット_2019_11_27_21_45.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/7d383469-6724-2bb6-08da-1bd990496497.png">

「nasu」ゲームオブジェクトが作られるので、名前を「GameMgr」に変更します。
![038.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/097662f2-841a-ce64-27f9-dfa1f8fdb736.gif)
nasuオブジェクトを選択した状態で、Windows環境であれば「F2」、MacOSX環境であれば「Enter」で変更できます。

GameMgrオブジェクトは画面中央にあると邪魔なので、Sceneビューでドラッグ＆ドロップして画面端に移動させておきましょう。
<img width="375" alt="Unity_2018_4_12f1_Personal_-_SampleScene_unity_-_TestFlappyBird_-_PC__Mac___Linux_Standalone__Personal___Metal_.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/e0dc3bcf-2b2a-757b-863a-ee58d95f4cd4.png">

## スクリプトでスコア表示をする
スクリプトでスコア表示の実装をします。
まずはスクリプトコンポーネントを追加します。GameMgrオブジェクトを選択し、Inspectorビューから、Add Component > New Script を選び、スクリプト名は「GameMgr」とします。
![039.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/87eea04d-a174-5b94-f95a-3d12cd2c8a20.gif)

スクリプトが追加できたら、GameMgr.cs を開いて以下のように記述します。

```cs:GameMgr.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GameMgr : MonoBehaviour {
  // ①スコア
  int _score = 0;

  void Start() {
  }

  void Update() {
  }

  private void FixedUpdate() {
    // ②スコア上昇
    _score += 1;
  }

  private void OnGUI() {
    // ③スコアを描画
    _DrawScore();
  }

  // ④スコアの描画
  void _DrawScore() {
    // 文字を大きくする
    GUI.skin.label.fontSize = 32;
    // 左揃え
    GUI.skin.label.alignment = TextAnchor.MiddleLeft;
    Rect position = new Rect(8, 8, 400, 100);
    GUI.Label(position, string.Format("score:{0}", _score));
  }
}
```

スコア用の変数を追加して、時間経過でスコアを上昇させ、その値を表示します。

①ではスコア用の変数を追加しています。

```cs:GameMgr.cs
  // ①スコア
  int _score = 0;
```

②でスコア加算用に `FixedUpdate()` を追加してその中でスコアを加算しています。

```cs:GameMgr.cs
  private void FixedUpdate() {
    // ②スコア上昇
    _score += 1;
  }
```

`FixedUpdate()` を使用するのは、決まった間隔で呼び出しが行われるためとなります。

③で `OnGUI()` を定義しています。スコアなどのGUIの描画はこの関数で行います。

```cs:GameMgr.cs
  private void OnGUI() {
    // ③スコアを描画
    _DrawScore();
  }
```

本来であれば uGUI を使うべきですが、テスト・実験用に作る場合、`OnGUI()` を使った方が素早く作れるので、今回は `OnGUI()` を使います。

④が実際の描画処理です。

```cs:GameMgr.cs
  // ④スコアの描画
  void _DrawScore() {
    // 文字を大きくする
    GUI.skin.label.fontSize = 32;
    // 左揃え
    GUI.skin.label.alignment = TextAnchor.MiddleLeft;
    Rect position = new Rect(8, 8, 400, 100);
    GUI.Label(position, string.Format("score:{0}", _score));
  }
```

初期状態だとフォントが小さいので `GUI.skin.label.fontSize = 32` で大きくします。
`GUI.Label()` で文字を描画するのですが、第一引数には描画領域として `Rect` で左上の位置と幅・高さを指定します。
数値を文字として描画する場合、`string.Format()` を使うと自由な書式で描画できます。

では実行して動作を確認します。
![040.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/aad4bdeb-3e97-d729-0d1a-82c249230f32.gif)
時間経過によりスコアが上昇していくのがわかります。

# ゲームオーバーの実装
プレイヤーがブロックに当たったらゲームオーバーとします。
そしてゲームオーバーになったらスコアを増加しないようにします。

処理の流れとしては以下の通りです。
<img width="560" alt="名称未設定.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/1c29fec2-e00d-158a-aae0-cd9fd874c142.png">

①プレイヤーとブロックの衝突、②プレイヤー消滅、この２つはすでに実装しているので、PlayerからGameMgrにゲームオーバーになったことを通知するようにします（②から③の部分）。

GameMgrスクリプトを以下のように修正します。（修正部分は７箇所）

```cs:GameMgr.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GameMgr : MonoBehaviour {
  // ①状態定数
  enum State {
    Main, // メインゲーム
    GameOver, // ゲームオーバー
  }

  // スコア
  int _score = 0;
  // ②状態
  State _state = State.Main;

  // ③ゲームオーバーの開始
  public void StartGameOver() {
    _state = State.GameOver;
  }

  void Start() {
  }

  void Update() {
  }

  private void FixedUpdate() {
    if(_state == State.Main) {
      // ④メインゲーム中のみスコア上昇
      _score += 1;
    }
  }

  private void OnGUI() {
    // スコアを描画
    _DrawScore();

    // ⑤画面の中心座標を計算する
    float CenterX = Screen.width / 2;
    float CenterY = Screen.height / 2;
    if(_state == State.GameOver) {
      // ⑥ゲームオーバーの描画
      _DrawGameOver(CenterX, CenterY);
    }
  }

  // ⑦ゲームオーバーの描画
  void _DrawGameOver(float CenterX, float CenterY) {
    // 中央揃え
    GUI.skin.label.alignment = TextAnchor.MiddleCenter;
    float w = 400;
    float h = 100;
    Rect position = new Rect(CenterX - w / 2, CenterY - h / 2, w, h);
    GUI.Label(position, "GAME OVER");
  }

  // スコアの描画
  void _DrawScore() {
    // 文字を大きくする
    GUI.skin.label.fontSize = 32;
    // 左揃え
    GUI.skin.label.alignment = TextAnchor.MiddleLeft;
    Rect position = new Rect(8, 8, 400, 100);
    GUI.Label(position, string.Format("score:{0}", _score));
  }
}
```

①で「メインゲーム中」「ゲームオーバー」を判定するための enum定数を定義しています。

```cs:GameMgr.cs
  // ①状態定数
  enum State {
    Main, // メインゲーム
    GameOver, // ゲームオーバー
  }
```

②では、状態を格納する変数を定義しています。

```cs:GameMgr.cs
  // ②状態
  State _state = State.Main;
```

③では外部からゲームオーバーに切り替えるための関数 `StartGameOver` を定義しています。

```cs:GameMgr.cs
  // ③ゲームオーバーの開始
  public void StartGameOver() {
    _state = State.GameOver;
  }
```

④では `FixedUpdate` 関数でのスコア上昇の判定を修正しました。状態変数 `_state` がメインゲーム中だったときだけスコア上昇するようにしています。

```cs:GameMgr.cs
  private void FixedUpdate() {
    if(_state == State.Main) {
      // ④メインゲーム中のみスコア上昇
      _score += 1;
    }
  }
```

⑤・⑥は GUI表示である `OnGUI()` を修正しています。

```cs:GameMgr.cs
  private void OnGUI() {
    // スコアを描画
    _DrawScore();

    // ⑤画面の中心座標を計算する
    float CenterX = Screen.width / 2;
    float CenterY = Screen.height / 2;
    if(_state == State.GameOver) {
      // ⑥ゲームオーバーの描画
      _DrawGameOver(CenterX, CenterY);
    }
  }
```

`Screen.width` / `Screen.height` で幅と高さを取得し、その半分の値を中心として文字の表示をする関数 `_DrawGameOver` を呼び出しています。

実際のゲームオーバーの描画を行なっているのが⑦の `_DrawGameOver()` です。

```cs:GameMgr.cs
  // ⑦ゲームオーバーの描画
  void _DrawGameOver(float CenterX, float CenterY) {
    // 中央揃え
    GUI.skin.label.alignment = TextAnchor.MiddleCenter;
    float w = 400;
    float h = 100;
    Rect position = new Rect(CenterX - w / 2, CenterY - h / 2, w, h);
    GUI.Label(position, "GAME OVER");
  }
```

少し計算がややこしいので図で説明します。
<img width="477" alt="名称未設定.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/5b1fae2a-93fe-3172-c5a2-d5ad850c4812.png">
`CenterX` / `CenterY` は画面の中心です。文字を描画するには文字の左上座標と描画矩形(Rect)の幅(w)と高さ(h)が必要となります。左上の座標を求めるには、幅を2で割った値、高さを2で割った値を中央の座標から引き算します。

長くなりましたが、これで GameMgrスクリプトの準備はできました。
続けて、Playerスクリプトを修正します。

```Cs:Player.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

// ■プレイヤー
public class Player : MonoBehaviour {
  // スプライト番号定義
  const int SPR_FALL = 0; // 落下中
  const int SPR_JUMP = 1; // ジャンプ中

  [SerializeField]
  float JUMP_VELOCITY = 1000; // ジャンプ力の定義
  public Sprite[] SPR_LIST; // アニメーション用スプライトの保持
  public GameObject gameMgr; // ①ゲーム管理

  Rigidbody2D _rigidbody; // 物理挙動コンポーネント保持用
  SpriteRenderer _renderer; // スプライト描画
  GameMgr _gameMgr; // ②ゲーム管理スクリプト

  // 開始処理
  void Start() {
    // 物理挙動コンポーネントを取得
    _rigidbody = GetComponent<Rigidbody2D>();
    // スプライト描画コンポーネントを取得
    _renderer = GetComponent<SpriteRenderer>();
    // ③ゲーム管理スクリプトを取得
    _gameMgr = gameMgr.GetComponent<GameMgr>();
  }

  ・
  ・
  ・

  // 衝突判定
  private void OnTriggerEnter2D(Collider2D collision) {
    // 衝突したので消滅
    Destroy(gameObject);
    // ④ゲームオーバーを通知
    _gameMgr.StartGameOver();
  }
}
```

①でGameMgrオブジェクトを格納する変数を定義します。
②でGameMgrスクリプトを格納する変数を定義します。
③は `Start()` でGameMgrオブジェクトからスクリプトを取り出して、`_gameMgr` に格納しています。
④でブロックにぶつかって消滅する時に `_gameMgr.StartGameOver()` を呼び出すことでゲームオーバーを通知します。

では、Unityエディタに戻って、`Player` オブジェクトを選択し、Inspectorビューの Spriteの `Game Mgr` に GameMgrオブジェクトを指定します。
![041.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/b66c0a07-e505-225e-7a43-1bfe44a080a3.gif)

では実行して、ブロックに衝突すると画面中央に「GAME OVER」表示がされ、スコアのカウントアップが停止しているのを確認します。
![042.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/9eb6c98a-6131-c165-5c80-f3b75540119b.gif)

# リトライの実装
最後にリトライ処理を実装して終わりとします。
ゲームオーバー時に「Retry」ボタンを押すと、ゲームに再挑戦できるようにします。

## シーンの登録
リトライ処理は、シーンを作り直す方法で実装します。
Unityエディタのメニューから File > Build Settings... を選びます。
<img width="305" alt="File_と_Menubar.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/199b12b2-6ee0-72b3-0ccd-00b72ffe7c9d.png">

するとビルド設定画面が表示されるので、「Add Open Scenes」をクリックして、「SampleScene」をビルド対象に設定します。
<img width="745" alt="Build_Settings_と_Unity_2018_4_12f1_Personal_-_SampleScene_unity_-_TestFlappyBird_-_PC__Mac___Linux_Standalone__Personal___Metal_.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/8d592d2b-2453-7d61-c81e-b261556f6430.png">
追加したら、×ボタンを押して設定画面を閉じます。

なお、「SampleScene」とは今までに編集をしていたメインゲームとなるシーンです。
<img width="590" alt="Unity_2018_4_12f1_Personal_-_SampleScene_unity_-_TestFlappyBird_-_PC__Mac___Linux_Standalone__Personal___Metal_.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/64c67ca0-3ea7-cca2-7d32-d5ebb6cf974f.png">

ファイルは Projectビューの Assets > Scenes に存在していて、通常はわかりやすい名称（例えば「MainScene」など）にリネームしておくべきですが、今回は "SampleScene" のままにしておきます。

## リトライボタンの実装
リトライボタンは GameMgrスクリプトで実装します。
GameMgrスクリプトを開いて以下のよう修正します。

```cs:GameMgr.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// ①シーンの読み直しに必要
using UnityEngine.SceneManagement;

public class GameMgr : MonoBehaviour {
  // 状態定数
  enum State {
    Main, // メインゲーム
    GameOver, // ゲームオーバー
  }

  // スコア
  int _score = 0;
  // 状態
  State _state = State.Main;

  // ゲームオーバーの開始
  public void StartGameOver() {
    _state = State.GameOver;
  }

  void Start() {
  }

  void Update() {
  }

  private void FixedUpdate() {
    if(_state == State.Main) {
      // メインゲーム中のみスコア上昇
      _score += 1;
    }
  }

  private void OnGUI() {
    // スコアを描画
    _DrawScore();

    // 画面の中心座標を計算する
    float CenterX = Screen.width / 2;
    float CenterY = Screen.height / 2;
    if(_state == State.GameOver) {
      // ゲームオーバーの描画
      _DrawGameOver(CenterX, CenterY);
      // ②リトライボタンの描画
      if(_DrawRetryButton(CenterX, CenterY)) {
        // ③クリックしたらやり直しする
        SceneManager.LoadScene("SampleScene");
      }
    }
  }

  // ④リトライボタンの描画
  bool _DrawRetryButton(float CenterX, float CenterY) {
    float ofsY = 40;
    float w = 100;
    float h = 64;
    Rect rect = new Rect(CenterX - w / 2, CenterY + ofsY, w, h);
    if (GUI.Button(rect, "RETRY")) {
      // ボタンを押した
      return true;
    }
    return false;
  }

  // ゲームオーバーの描画
  void _DrawGameOver(float CenterX, float CenterY) {
    // 中央揃え
    GUI.skin.label.alignment = TextAnchor.MiddleCenter;
    float w = 400;
    float h = 100;
    Rect position = new Rect(CenterX - w / 2, CenterY - h / 2, w, h);
    GUI.Label(position, "GAME OVER");
  }

  // スコアの描画
  void _DrawScore() {
    // 文字を大きくする
    GUI.skin.label.fontSize = 32;
    // 左揃え
    GUI.skin.label.alignment = TextAnchor.MiddleLeft;
    Rect position = new Rect(8, 8, 400, 100);
    GUI.Label(position, string.Format("score:{0}", _score));
  }
}
```

修正箇所は、
1. `using` の追加
2. `OnGUI()` の修正
3. `_DrawRetryButton()` の追加

の３つです。

まずファイルの先頭で

```cs:GameMgr.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// ①シーンの読み直しに必要
using UnityEngine.SceneManagement;
```

というように `using UnityEngine.SceneManagement;` を追加し、シーン管理である SceneManager を使えるようにします。

次に、`OnGUI()` の修正です。

```cs:GameMgr.cs
  private void OnGUI() {
    // スコアを描画
    _DrawScore();

    // 画面の中心座標を計算する
    float CenterX = Screen.width / 2;
    float CenterY = Screen.height / 2;
    if(_state == State.GameOver) {
      // ゲームオーバーの描画
      _DrawGameOver(CenterX, CenterY);
      // ②リトライボタンの描画
      if(_DrawRetryButton(CenterX, CenterY)) {
        // ③クリックしたらやり直しする
        SceneManager.LoadScene("SampleScene");
      }
    }
  }
```

②で `_DrawRetryButton()` を呼び出し、戻り値が `true` なら③のやり直し処理を行います。 `SceneManager.LoadScene()` には「シーン名」を文字列で渡します。

④でリトライボタンの描画（とクリック）を行う `_DrawRetryButton()` を実装しています。

```cs:SceneMgr.cs
  // ④リトライボタンの描画
  bool _DrawRetryButton(float CenterX, float CenterY) {
    float ofsY = 40;
    float w = 100;
    float h = 64;
    Rect rect = new Rect(CenterX - w / 2, CenterY + ofsY, w, h);
    if (GUI.Button(rect, "RETRY")) {
      // ボタンを押した
      return true;
    }
    return false;
  }
```

`ofsY` とは 「Offset Y座標」の略で、Y座標を少しずらす値としてよく使われる略語です。
`GUI.Button()` はボタンの描画とクリックしたかどうかを判定する関数です。`true` を返した場合はボタンをクリックしたと判定されます。

では実行して動作を確認します。
![043.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/3477a3cd-a7b9-8e53-27ca-a81619ad15d5.gif)

ゲームオーバー時に「RETRY」ボタンを押すことでゲームを遊び直すことができるようになりました。

## BlockMgr / GameMgr のスプライト表示を消す
BlockMgr(xbox) / GameMgr(nasu) がスプライト表示されていますが、俺は不要なので消しておきます。

Inspectorビューから `Sprite Renderer` のチェックを外すことで表示されなくなります。
<img width="268" alt="Unity_2018_4_12f1_Personal_-_SampleScene_unity_-_TestFlappyBird_-_PC__Mac___Linux_Standalone__Personal___Metal_.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/71287fc3-7b5a-b6ae-7deb-9ab9e24bc9f7.png">
再び表示したい場合は、ここにチェックを入れ直すと表示されるようになります。

# 最後に
まだゲームとして物足りない部分はありますが、これでひとまずゲームは完成です。

* ブロックの出現パターンをランダムでなく決まった形にする
* 接触すると得点になるアイテム
* 一定距離進むとゲームクリア

など色々な要素を加えて、より面白いゲームに作り替えていくと、ゲームプログラムの勉強、ゲームデザインの練習になって良いかもしれません。

なお、今回作成したプロジェクトは以下の場所にアップロードしています。
http://syun777.sakura.ne.jp/tmp/Unity/FlappyBird/TestFlappyBird.zip