Unity 公式チュートリアル「Space Shooter tutorial」をベースに自作ゲームを作る【後編】

[Unity 公式チュートリアル「Space Shooter tutorial」をコードから理解してみる](https://qiita.com/yamachan360/items/3690d4a4de5e6f02b0b9) という投稿をしましたが、今回はこれをベースに拡張し、簡易的なゲームエンジン的なものとして汎用化し、それを用いて独自のシューティングゲームを作成してみます。

[【前編】](https://qiita.com/yamachan360/items/460ef13dd08118f9bf21) では、[元のサンプルゲーム](https://qiita.com/yamachan360/items/3690d4a4de5e6f02b0b9) に対し、岩石に大きさと耐久度の差をつけ、真っすぐ高速に進んでくるミサイル、弾を斜めに発射する中ボス的な敵キャラ、赤い敵エース機などを追加し、敵の拡張を中心に実施しました。

この後編ではステージごとの難易度の上昇や、タイトル画面を作成し、ゲームとして最低限の体裁を整えたいと思います。完成品は [コチラ](http://rinco.jp/unity/sst1/) でプレイ可能。

# ステージの実装

まずはステージを実装してみます。`Done_GameController` スクリプトを対象に、まずは private 変数 `stage` の追加と初期化。

```c#:Done_GameController.cs
    private int stage; // 追加

    void Start()
    {
        gameOver = false;
        restart = false;
        restartText.text = "";
        gameOverText.text = "";
        score = 0;
        stage = 1; // 追加
        UpdateScore();
        StartCoroutine(SpawnWaves());
    }
```

サンプルゲームでは内部的に wave の概念があるので、これをステージにそのまま利用します。つまり、wave ごとにステージを1つ進めることになります。

```c#:Done_GameController.cs
    IEnumerator SpawnWaves()
    {
        yield return new WaitForSeconds(startWait);
        while (true)
        {
            // 中略
            if (gameOver)
            {
                restartText.text = "Press 'R' for Restart";
                restart = true;
                break;
            }
            stage++; // 追加
        }
    }
```

そして画面表示ですが、ちょっと手抜きしてスコア表示とあわせて実施してしまいましょう。

```c#:Done_GameController.cs
    void UpdateScore()
    {
        //scoreText.text = "Score: " + score;
        scoreText.text = "Score: " + score + "\nStage: " + stage; // 修正
    }
```

ただこれだと2行目になったステージ表示が画面に描画されないので、`Score` UIテキスト・オブジェクトのインスペクターから、高さを倍の 60 に変更しておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/ffd3f18d-84c8-7132-cbcb-c40e6e1a0588.png)
これでゲーム実行時に、左上にのスコアの下に、ステージ数も表示されるようになりました。ゲームが進んでいくと、ステージ数も増加していきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/1992b61b-ad6b-9b89-139b-709354b104dc.png)

## ステージにあわせて敵の種類を増やす

最初から全ての種類の敵が出現すると難しすぎるため、最初のステージではサンプルゲームと同じ岩石と紫の敵だけ出現するようにし、ステージが1つ進むごとに新しい敵が1つ追加されるように修正します。

```c#:Done_GameController.cs
            for (int i = 0; i < hazardCount; i++)
            {
                //GameObject hazard = hazards[Random.Range(0, hazards.Length)];
                GameObject hazard = hazards[Random.Range(0, System.Math.Min(stage + 3, hazards.Length))]; // 修正
                Vector3 spawnPosition = new Vector3(Random.Range(-spawnValues.x, spawnValues.x), spawnValues.y, spawnValues.z);
                Quaternion spawnRotation = Quaternion.identity;
                Instantiate(hazard, spawnPosition, spawnRotation);
                yield return new WaitForSeconds(spawnWait);
            }
```

試しプレイしたところ、各ステージが短すぎる気がしたので、`Done_Game Controller` オブジェクトの Hazard Count を倍の 20 に変更して、ステージ中に表示されるオブジェクトの数を増やして調節してみました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/21aa6b8c-9c77-5600-1e81-0e6c38bd5b81.png)

## 更に難易度の調整をする

上記の修正だけだと、ステージ4くらいで新キャラの登場がなくなり、難易度が変わらなくなってしまいます。もう少し調整してみましょう。

まずは細かいですが、各ステージ中に登場する敵オブジェクトの数を、ステージが進むにつれて増やしてみます。修正(1)の部分です。

```c#:Done_GameController.cs
            //for (int i = 0; i < hazardCount; i++)
            for (int i = 0; i < hazardCount + stage * 2; i++) // 修正 (1)
            {
                GameObject hazard = hazards[Random.Range(0, System.Math.Min(stage + 3, hazards.Length))];
                Vector3 spawnPosition = new Vector3(Random.Range(-spawnValues.x, spawnValues.x), spawnValues.y, spawnValues.z);
                Quaternion spawnRotation = Quaternion.identity;
                Instantiate(hazard, spawnPosition, spawnRotation);
                //yield return new WaitForSeconds(spawnWait);
                yield return new WaitForSeconds(spawnWait * Mathf.Clamp(5 / stage, 0.2f, 1f)); // 修正
            }
```

また敵の出現待ち時間を、ステージが進むに従って少しずつ短くしていきます。これにより敵の出現量が多くなり、難易度が上昇します。ステージ25ぐらいで難易度が頭打ちになりますが、現時点ではこれが超絶難しいので、それ以上は不要に思われます。

## ゲームオーバー時の処理を改善する

各ステージを長くした結果、ゲームオーバー時にリスタート待ちの時間が長くなってしまいました。すぐ表示されるように修正してみましょう。

まず、wave の終了待ちをしていた部分の一部をコメントアウトし、

```c#:Done_GameController.cs
            if (gameOver)
            {
                //restartText.text = "Press 'R' for Restart";
                //restart = true;
                break;
            }
            stage++;
```

同じ処理を `gameover()` 関数の中にコピーします。

```c#:Done_GameController.cs
    public void GameOver()
    {
        gameOverText.text = "Game Over!";
        gameOver = true;
        restartText.text = "Press 'R' for Restart"; // 追加
        restart = true; // 追加
    }
```

これで自機が破壊された直後にリスタートの表示が出るようになりました。

# ハイスコアを記録する

Unity では [PlayerPrefs](https://docs.unity3d.com/ja/2017.4/ScriptReference/PlayerPrefs.html) クラスを用いて、ゲームデータの保持ができます。これを利用して、ハイスコアを記録する仕組みを実装します。

まずは Done_GameController スクリプトに、ハイスコアの読み込み部分を実装します。保存キーは "hi-score" とし、初期値は 0 です。

```c#:Done_GameController.cs
    private int hiScore; // 追加

    void Start()
    {
        gameOver = false;
        restart = false;
        restartText.text = "";
        gameOverText.text = "";
        score = 0;
        hiScore = PlayerPrefs.GetInt("hi-score", 0); // 追加
        stage = 1;
        UpdateScore();
        StartCoroutine(SpawnWaves());
    }
```

後は `GameOver()` 関数に、表示と保存のロジックを追加します。

```c#:Done_GameController.cs
    public void GameOver()
    {
        gameOverText.text = "Game Over!";
        if (score > hiScore)                         // 追加
        {                                            // 追加
            hiScore = score;                         // 追加
            PlayerPrefs.SetInt("hi-score", hiScore); // 追加
            gameOverText.text += "\n Hi Score!!";    // 追加
        }                                            // 追加
        gameOver = true;
        restartText.text = "Press 'R' for Restart";
        restart = true;
    }
```

そうそう、`gameOverText` オブジェクトの高さも、忘れず倍にしておきましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/ad96cf0a-756b-56a5-2b77-5eea129850fd.png)
これでゲームオーバー時に、ハイスコアを獲得していれば「Hi Score!!」という文字列が画面中央に表示されるようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/5c38b1ec-acd5-a6fe-07fe-5e8f6ee1bc38.png)
なおハイスコアを削除するには、Unity エディタの編集メニューにある「すべてのPlayerPrefsを消去」を実行してください。

# タイトル画面を作成する

ゲームとしての体裁も整ってきたので、いよいよタイトル画面を作成してみましょう。とはいえ、今回はお手軽に [公式アセット「Unity Samples: UI」Menu 3D シーンで uGUI の基礎を理解する](https://qiita.com/yamachan360/items/2f909a11665161a6b485) で理解した宇宙っぽいメニューの部品を流用して楽しちゃいましょう！

まずは新規にシーンを作成し、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/db6a53dd-ebec-1641-7412-4c9c17b60901.png)
`Title` という名前で `Done_Main` と同じフォルダに保存します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/76da570d-0d8c-ca68-e6ce-f34cef1dba47.png)
Unity Samples: UI アセットから `SF Scene Elements` PreFab をエクスポートし、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/7bbf4a7b-db9c-f44a-0af2-810296e2436e.png)
今回のゲームで読み込んで、新しく作成した `Title` シーンに配置します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/78185603-ab64-b5af-b53c-60738720770f.png)

## ゲームタイトルの表示

まず新規で UI 要素の Canvas を追加します。インスペクターの設定はそのままで大丈夫でしょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/bf6924c8-a673-b93c-c065-7635eb815d34.png)

そして Canvas の子要素として `TextMeshPro - Text` を追加します。初回は以下のようなポップアップが表示されるので、ボタンをクリックして必要なファイルを読み込んでおきましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/de7904b4-3869-2e85-5b78-a2547f5ecba3.png)
表示するテキストをゲームタイトル(今回は Space Shooter tutorial+)に変更し、色やフォントのオプションを好みで設定します。

ついでにハイスコア表示用と、キー操作説明(Press 'S' for Start\nPress 'Q' for Quit)用に UI Text を二つ、その下に配置しておきましょう。シーンを実行して配置を確かめます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/209c1dd3-7525-cedb-1295-82cc4e7eeff8.png)
さて、タイトル用のコードを書きましょう。`SF Scene Elements` のインスペクターで「コンポーネントを追加」から「新しいスクリプト」を選択して `TitleScript` と名前をつけます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/ad569c77-f49c-b957-6bba-6fdf8cfaf8b4.png)
`Done_GameController` スクリプトのコードを参照して、以下のようにハイスコア読み込みと、キーに応じた処理を記述します。

```c#:TitleScript.cs
using UnityEngine.SceneManagement;
using UnityEngine.UI;

public class TitleScript : MonoBehaviour
{
    public Text hiScoreText;

    void Start()
    {
        hiScoreText.text += PlayerPrefs.GetInt("hi-score", 0);
    }

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.S))
        {
            SceneManager.LoadScene("Done_Main");
        }
        if (Input.GetKeyDown(KeyCode.Q))
        {
#if UNITY_EDITOR
            UnityEditor.EditorApplication.isPlaying = false;
#else
            Application.Quit();
#endif
        }
    }
}
```

hiScoreText にはハイスコア表示用の UI Text をインスペクターで指定しておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/e4019b82-be12-7350-dd9e-fedb50338f3c.png)
これでハイスコアの表示とタイトル画面からの遷移はokです。あとはゲーム画面からタイトル画面へ戻れるように修正する必要がありますね。

## ゲーム終了時にタイトルに戻る

`Done_GameController` スクリプトの `GameOver` 関数で表示メッセージを追加します。

```c#:Done_GameController.cs
    public void GameOver()
    {
        gameOverText.text = "Game Over!";
        if (score > hiScore)
        {
            hiScore = score;
            PlayerPrefs.SetInt("hi-score", hiScore);
            gameOverText.text += "\n Hi Score!!";
        }
        gameOver = true;
        //restartText.text = "Press 'R' for Restart";
        restartText.text = "Press 'R' for Restart\nPress 'Q' to Title"; // 修正
        restart = true;
    }
```

表示される行数が増えるので、例によって `Restart` テキストの高さを倍の 60 に変更しておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/54bf03c4-8bd8-aba3-698d-1b9bee76570e.png)
ゲームオーバー時に右上に表示されるテキストが変更されていることを確認しておきましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/a94dea0f-02b3-df2c-d4e3-dc8fe37b8885.png)
そして `Done_GameController` スクリプトに、ゲームオーバー時に Q キーを押した場合の処理を追加します。

```c#:Done_GameController.cs
    void Update()
    {
        if (restart)
        {
            if (Input.GetKeyDown(KeyCode.R))
            {
                SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
            }
            if (Input.GetKeyDown(KeyCode.Q))     // 追加
            {                                    // 追加
                SceneManager.LoadScene("Title"); // 追加
            }                                    // 追加
        }
    }
```

これでゲーム用の `Done_Main` シーンと、タイトル画面の `Title` シーンの間の遷移が設定できました。ゲームとしての体裁が整った感じですね。

# いよいよビルド

「さぁ、実験を始めようか。」と、いよいよビルドのフェーズです。今回は WebGL で「ビルドして実行」を実施してみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/18c482f7-77b6-70b1-0aa9-e3ac7d5b02fc.png)
問題なくビルドできたようで、ローカルで Web サービスが立ち上がってWeb ブラウザでゲームをプレイすることができました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/fcc158cf-5107-c98f-2847-5488834b256e.png)
Web ブラウザで実行される場合、PlayerPrefs データはブラウザの IndexedDB に格納されている模様。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/5e1c3ff2-0eaa-b38a-adf5-582837942e56.png)
ビルド結果は以下のように指定したフォルダに出力されており、サイズは13Mバイトぐらいでした。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/16de71c0-016a-bac2-93da-dcd379e9b407.png)
生成されるのは通常の Web コンテンツなので、生成されたフォルダ内で [webserv](https://www.npmjs.com/package/webserv) などのWeb サーバーを起動させれば、そちらでも動作確認が可能です。

そして生成されたフォルダの中身をWeb サーバーにアップロードすれば、ゲームを公開可能です。実際に [http://rinco.jp/unity/sst1/](http://rinco.jp/unity/sst1/) に公開してみましたので、よろしかったらプレイしてみてください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/b1d838c9-ded9-e5ac-51aa-d1b52a32a1ac.png)

# というわけで！

GW は過ぎちゃいましたが、公式チュートリアルをベースとして簡単なシューティングゲームを作成してみました。シンプルですが、ゲームとして必要な要素はだいたい揃っていると思いますので、独自ゲームを作成する際に参考にしていただければ嬉しいです。

それではまた！