Skip to content

第18回 ステージの流れ ​

この回でつくるもの:「何秒に、どの敵が、どこから出るか」を表にして、そのとおりに敵が出るようにします。最後に「WARNING!!」の文字とともにボスが登場し、倒すと「STAGE CLEAR」が出ます。

この回で学ぶこと ​

  • ScriptableObject(データだけを入れておくアセット)を作る
  • [System.Serializable] で、自分で作ったデータの形を Inspector に表示する
  • 配列と foreach で、表のとおりに順番に処理する
  • SetActive で、オブジェクトを好きなタイミングで登場させる

前回までのふりかえり ​

第17回では、ボスを作りました。ボスは登場してから上下に動き、扇形の弾と自機ねらいの弾を撃ってきます。 上には体力バーが出て、倒すと Console に「BOSS DEFEATED」と出ました。 ただ、今はゲームを始めるとすぐにボスが出てきます。今回は、ステージの最初から最後までの流れを作ります。

1. ScriptableObject とは ​

1-1. 「敵の出現表」を作りたい ​

今の EnemySpawner は、ランダムに敵を出します。これだと毎回ちがう展開になりますが、「ここで波の敵が5機来て、次に弾を撃つ敵が来て…」のような、考えて作ったステージにはなりません。

本物のシューティングゲームは、敵の出方が「表」のように決まっています。

時間敵高さ数
2秒まっすぐの敵上のほう1機
7秒波の敵真ん中5機つながって
…………

この表を、Unity の中に「データ」として作ります。

1-2. ScriptableObject はデータの入れ物 ​

ScriptableObject(スクリプタブル・オブジェクト)は、データだけを入れておくアセット(Project ウィンドウに置くファイル)です。

MonoBehaviourScriptableObject
どこにある?シーンの中のオブジェクトに付けるProject ウィンドウにファイルとして置く
何をする?動き(Update など)データを持つだけ
たとえると役者台本

「台本」を別のファイルにしておくと、2面目を作りたいときは台本(Stage2)を作って差しかえるだけで済みます。

2. StageData と SpawnInfo ​

2-1. StageData スクリプトを作る ​

  1. Project ウィンドウで Assets/_Game/Scripts を開きます。
  2. 右クリックし、Create > Scripting > MonoBehaviour Script を選びます。
  3. 名前を StageData にします。
  4. ダブルクリックして開き、中身を 全部消して、次のコードを書きます。
csharp
using UnityEngine;

// 「何秒に、どの敵を、どこに、何機」出すかの1行分
[System.Serializable]
public class SpawnInfo
{
    public float time;            // ステージ開始から何秒後か
    public GameObject enemyPrefab;
    public float y;               // 出す高さ
    public int count = 1;         // 何機つなげて出すか
    public float gap = 0.3f;      // つなげるときの間隔
}

// ステージの「敵の出現表」
[CreateAssetMenu(fileName = "StageData", menuName = "Shooting/Stage Data")]
public class StageData : ScriptableObject
{
    public SpawnInfo[] spawns;    // 時間の早い順に並べる
    public float bossTime = 60f;  // ボスが出てくる時間
}

保存します(Ctrl+S、Macは Cmd+S)。

MonoBehaviour ではありません

今回のクラスは : MonoBehaviour ではなく : ScriptableObject です。そのため、このスクリプトはオブジェクトに Add Component できません。それで正しいです。

2-2. コードの解説 ​

SpawnInfo(表の1行分)

変数意味
timeステージが始まってから何秒後に出すか
enemyPrefab出す敵のプレハブ
y出す高さ(-4〜4 くらい)
count何機つなげて出すか
gapつなげて出すときの間隔(秒)

[System.Serializable] の意味

[ ] で書くものは 属性(アトリビュート)といって、クラスや変数に「しるし」を付けるものです。 [System.Serializable] は「このクラスの中身を Inspector に表示して、保存できるようにしてね」というしるしです。 これを付けないと、SpawnInfo の配列を作っても Inspector に何も出てきません。

「シリアライズ」とは

データをファイルに保存できる形に変えることです。Unity は、Inspector で入れた値をシリアライズしてシーンやアセットに保存しています。

StageData(表ぜんたい)

行意味
[CreateAssetMenu(...)]Project ウィンドウの Create メニューに Shooting > Stage Data を追加します。fileName は最初に付く名前です
: ScriptableObjectデータ用のアセットにします
public SpawnInfo[] spawns;SpawnInfo の配列。表の「行」がたくさん並んだものです
public float bossTime = 60f;ボスが来る時間(秒)

3. Stage1 を作って表を埋める ​

3-1. Stage1 アセットを作る ​

  1. Project ウィンドウで Assets/_Game/Data を開きます。
  2. 何もないところを右クリックし、Create > Shooting > Stage Data を選びます。
  3. 名前を Stage1 にします。
  4. Stage1 をクリックすると、Inspector に Spawns と Boss Time が表示されます。

メニューに出てこないとき

Create の中に Shooting が出ないときは、Console にエラー(赤い文字)が出ていないか確認しましょう。スクリプトにエラーがあると、メニューが追加されません。

3-2. 表の例 ​

今回は、次の表のとおりに入れてみましょう。全部で15行です。

ElementTimeEnemy PrefabYCountGap
02Enemy_Straight210.3
14Enemy_Straight-210.3
27Enemy_Wave050.3
311Enemy_Straight330.5
414Enemy_Red150.3
518Enemy_Shooter-310.3
621Enemy_Wave250.3
725Enemy_Shooter320.8
829Enemy_Red-150.3
933Enemy_Straight040.4
1037Enemy_Wave-260.25
1141Enemy_Shooter030.6
1245Enemy_Red250.3
1350Enemy_Straight-350.3
1454Enemy_Shooter221

Boss Time は 60 にします。

表のコツ

  • Time は必ず「小さい順」に並べます
  • 最初はやさしく、だんだんむずかしくします
  • Enemy_Red(カプセルを落とす敵)をときどき入れて、パワーアップできるようにします
  • 最後の敵(54秒)からボス(60秒)まで、少し「静かな時間」を作ると、ボス前のドキドキが出ます

3-3. 表を入力する ​

  1. Stage1 を選びます。
  2. Inspector の Spawns の左の三角をクリックして開きます。
  3. Spawns の右にある数字(要素の数)を 15 にして Enter を押します。Element 0 〜 Element 14 ができます。
  4. Element 0 の三角を開きます。
  5. Time に 2 を入れます。
  6. Enemy Prefab に、Assets/_Game/Prefabs の Enemy_Straight をドラッグします。
  7. Y に 2、Count に 1、Gap に 0.3 を入れます。
  8. 同じようにして、Element 1 〜 Element 14 を表のとおりに入れます。
  9. Boss Time が 60 になっていることを確認します。
  10. File > Save Project を選んで保存します。

数を増やすと値がコピーされる

要素の数を増やすと、新しい要素には一番下の要素の値がコピーされます。数字の入れまちがいがないか、最後に表と見比べましょう。

4. StageController で表のとおりに出す ​

4-1. StageController スクリプトを作る ​

  1. Assets/_Game/Scripts で右クリックし、Create > Scripting > MonoBehaviour Script を選びます。
  2. 名前を StageController にします。
  3. ダブルクリックして開き、中身を全部消して、次のコードを書きます。
csharp
using System.Collections;
using UnityEngine;

// StageData の表どおりに敵を出し、最後にボスを出す
public class StageController : MonoBehaviour
{
    public StageData stageData;
    public GameObject boss;          // シーンに置いて非表示にしておいたボス
    public GameObject bossHPBar;     // 非表示にしておいた体力バー
    public GameObject warningText;   // 「WARNING!!」の文字

    void Start()
    {
        StartCoroutine(StageFlow());
    }

    IEnumerator StageFlow()
    {
        float time = 0f;

        foreach (SpawnInfo info in stageData.spawns)
        {
            // 次の出現時間まで待つ
            yield return new WaitForSeconds(info.time - time);
            time = info.time;
            StartCoroutine(SpawnGroup(info));
        }

        // ボスの時間まで待つ
        yield return new WaitForSeconds(stageData.bossTime - time);

        warningText.SetActive(true);
        yield return new WaitForSeconds(3f);
        warningText.SetActive(false);

        boss.SetActive(true);
        bossHPBar.SetActive(true);
    }

    IEnumerator SpawnGroup(SpawnInfo info)
    {
        for (int i = 0; i < info.count; i++)
        {
            if (!GameManager.instance.isPlaying) yield break;

            Vector3 pos = new Vector3(transform.position.x, info.y, 0f);
            Instantiate(info.enemyPrefab, pos, Quaternion.identity);
            yield return new WaitForSeconds(info.gap);
        }
    }
}

保存します。

4-2. コードの解説 ​

行意味
foreach (SpawnInfo info in stageData.spawns)表の行を、上から1行ずつ info に入れて取り出します
info.time - time「次に出す時間」から「今の時間」を引いて、あと何秒待てばいいかを計算します
time = info.time;待ち終わったので、今の時間を更新します
StartCoroutine(SpawnGroup(info));1行分の敵を出すコルーチンを、別に動かします。こうすると、つなげて出している間も表の時間がずれません
stageData.bossTime - time最後の敵からボスの時間まで待ちます
warningText.SetActive(true);WARNING の文字を出し、3秒待って消します
boss.SetActive(true);非表示にしておいたボスと体力バーを表示します
if (!GameManager.instance.isPlaying) yield break;ゲームが終わっていたら、敵を出すのをやめます。yield break はコルーチンを終わらせる命令です

待ち時間の計算の例

表の1行目は 2秒、2行目は 4秒です。 1行目のあと、time は 2 になっています。2行目では 4 - 2 = 2 秒だけ待てば、ちょうど4秒のときに出せます。

4-3. StageController を置く ​

  1. Hierarchy で右クリックし、Create Empty を選びます。
  2. 名前を StageController にします。
  3. Position を (11, 0, 0) にします(EnemySpawner と同じく、画面の右の外です)。
  4. Add Component で Stage Controller を追加します。
  5. Stage Data に、Project の Assets/_Game/Data/Stage1 をドラッグします。

Boss、Boss HP Bar、Warning Text は、このあとつなぎます。

4-4. EnemySpawner を止める ​

これからは StageController が敵を出すので、ランダムに出す EnemySpawner は使いません。

  1. Hierarchy で EnemySpawner を選びます。
  2. Inspector の名前の左のチェックボックスを OFF にします。

消さずに残しておく

EnemySpawner は消さずに、非アクティブで残しておきます。「エンドレスモード」を作りたくなったときに使えます。

4-5. ボスと体力バーを非アクティブにする ​

ボスと体力バーは、最初は隠しておき、StageController が出すようにします。

  1. Hierarchy で Boss を選びます。
  2. Position が (14, 0, 0) であることを確認します。
  3. Inspector の名前の左のチェックボックスを OFF にします。
  4. Hierarchy で Canvas の中の BossHPBar を選びます。
  5. チェックボックスを OFF にします。
  6. Hierarchy で StageController を選びます。
  7. Boss に、Hierarchy の Boss をドラッグします。
  8. Boss HP Bar に、Hierarchy の BossHPBar をドラッグします。

非アクティブでもつなげる

非アクティブ(グレーの文字)のオブジェクトも、Hierarchy からドラッグしてつなぐことができます。

5. WARNING 表示 ​

5-1. WARNING の文字を作る ​

  1. Hierarchy の Canvas を右クリックし、UI > Text - TextMeshPro を選びます。
  2. 名前を WarningText にします。
  3. Anchor Presets で middle-center(真ん中)を選びます。
  4. Pos X を 0、Pos Y を 0、Width を 1000、Height を 200 にします。
  5. TextMeshPro - Text (UI) の文字の欄に WARNING!! と入力します。
  6. Font Size を 120 くらいにします。
  7. Vertex Color を赤にします。
  8. Alignment で、横も縦も中央ぞろえのボタンを押します。
  9. Inspector の名前の左のチェックボックスを OFF にして、非アクティブにします。
  10. Hierarchy で StageController を選び、Warning Text に WarningText をドラッグします。
  11. シーンを保存します(Ctrl+S、Macは Cmd+S)。

5-2. 動作確認(ボスが出るまで) ​

  1. ▶ を押します。
  2. 2秒後に上のほうから敵が1機、4秒後に下のほうから1機、7秒後に波の敵が5機…と、表のとおりに出てくればOKです。
  3. 60秒たつと WARNING!! が3秒表示され、そのあとボスと体力バーが出てくればOKです。
  4. ▶ を押して止めます。

60秒待つのがたいへんなとき

テスト用に、Stage1 を選んで Ctrl+D(Macは Cmd+D)で複製し、Stage_Test という名前にします。Spawns を 1〜2行にして Boss Time を 5 にし、StageController の Stage Data を Stage_Test に差しかえてテストします。終わったら Stage1 に戻しましょう。

6. クリア画面 ​

6-1. ClearPanel を作る ​

第11回で作った GameOverPanel をコピーして作ります。

  1. Hierarchy で Canvas の中の GameOverPanel を選びます。
  2. Ctrl+D(Macは Cmd+D)で複製します。
  3. 複製したものの名前を ClearPanel にします。
  4. ClearPanel の左の三角を開き、GAME OVER と書いてある文字のオブジェクトを選びます。
  5. 文字を STAGE CLEAR に変えます。色も黄色などに変えるとクリアらしくなります。
  6. ClearPanel の中の RetryButton を選び、Delete キーで消します。
  7. ClearPanel の中の TitleButton を選び、Button の On Click () に GameManager の BackToTitle が入っていることを確認します。
  8. ClearPanel が非アクティブ(チェックが OFF)になっていることを確認します。

複製すると中身もそのまま

GameOverPanel を複製したので、TitleButton のボタンの設定もそのままコピーされています。

6-2. GameManager に StageClear を足す ​

Assets/_Game/Scripts/GameManager.cs を開き、次のように書きかえます。

csharp
using TMPro;
using UnityEngine;
using UnityEngine.SceneManagement;

// スコア・残機・ゲームの状態を管理する
public class GameManager : MonoBehaviour
{
    public static GameManager instance;   // どこからでも使えるようにする

    public int score = 0;
    public TextMeshProUGUI scoreText;

    public int lives = 3;                 // 残機(第11回)
    public TextMeshProUGUI livesText;
    public GameObject gameOverPanel;
    public bool isPlaying = true;         // ゲーム中かどうか

    public GameObject clearPanel;         // クリア画面(第18回) // ★追加

    void Awake()
    {
        instance = this;
    }

    void Start()
    {
        UpdateUI();
    }

    public void AddScore(int amount)
    {
        score += amount;
        UpdateUI();
    }

    public void LoseLife()
    {
        lives--;
        UpdateUI();

        if (lives <= 0)
        {
            GameOver();
        }
    }

    void GameOver()
    {
        isPlaying = false;
        gameOverPanel.SetActive(true);
    }

    public void StageClear()              // ★追加
    {                                     // ★追加
        isPlaying = false;                // ★追加
        clearPanel.SetActive(true);       // ★追加
    }                                     // ★追加

    void UpdateUI()
    {
        scoreText.text = "SCORE " + score.ToString("D6");
        livesText.text = "LIFE " + lives;
    }

    // ボタンから呼ぶ
    public void Retry()
    {
        SceneManager.LoadScene(SceneManager.GetActiveScene().name);
    }

    public void BackToTitle()
    {
        SceneManager.LoadScene("Title");
    }
}

保存します。

コードの解説

行意味
public GameObject clearPanel;クリア画面をつなぐ変数
public void StageClear()ボスを倒したときに Enemy から呼ぶので public にします
isPlaying = false;ゲームを終わった状態にします。敵がもう出なくなります
clearPanel.SetActive(true);クリア画面を表示します

6-3. Enemy からステージクリアを呼ぶ ​

第17回で Debug.Log("BOSS DEFEATED"); と書いたところを、StageClear に変えます。 Assets/_Game/Scripts/Enemy.cs を開き、次のように書きかえます。

csharp
using UnityEngine;

// 敵の基本(移動・HP・やられたとき)
public class Enemy : MonoBehaviour
{
    public float speed = 3f;              // 左へ進む速さ
    public int hp = 1;                    // 体力(第8回)
    public GameObject explosionPrefab;    // 爆発(第9回)
    public AudioClip explosionSound;      // 爆発音(第9回)
    public int score = 100;               // 倒したときの点数(第10回)
    public GameObject capsulePrefab;      // 落とすカプセル(第14回)
    public bool isBoss = false;           // ボスかどうか(第17回)

    [HideInInspector] public int maxHp;   // 最初の体力(第17回)

    bool isDead = false;

    void Awake()
    {
        maxHp = hp;
    }

    void Update()
    {
        transform.Translate(Vector3.left * speed * Time.deltaTime, Space.World);

        // 画面の左に出たら消す
        if (transform.position.x < -12f)
        {
            Destroy(gameObject);
        }
    }

    // ダメージを受ける(第8回)
    public void TakeDamage(int damage)
    {
        if (isDead) return;

        hp -= damage;
        if (hp <= 0)
        {
            Die();
        }
    }

    void Die()
    {
        isDead = true;

        GameManager.instance.AddScore(score);

        if (explosionPrefab != null)
        {
            Instantiate(explosionPrefab, transform.position, Quaternion.identity);
        }
        if (explosionSound != null)
        {
            AudioSource.PlayClipAtPoint(explosionSound, Camera.main.transform.position);
        }
        if (capsulePrefab != null)
        {
            Instantiate(capsulePrefab, transform.position, Quaternion.identity);
        }
        if (isBoss)
        {
            GameManager.instance.StageClear();   // ★変更
        }

        Destroy(gameObject);
    }
}

保存します。

6-4. ClearPanel をつなぐ ​

  1. Hierarchy で GameManager を選びます。
  2. Clear Panel に、Hierarchy の ClearPanel をドラッグします。
  3. シーンを保存します(Ctrl+S、Macは Cmd+S)。

6-5. 動作確認(通しプレイ) ​

  1. StageController の Stage Data が Stage1 になっていることを確認します。
  2. ▶ を押して、最初から最後まで遊んでみます。
  3. 表のとおりに敵が出て、60秒で WARNING!!、そのあとボスが出ればOKです。
  4. ボスを倒すと、爆発して体力バーが消え、STAGE CLEAR の画面が出ればOKです。
  5. TITLE ボタンを押すと、タイトル画面にもどればOKです。
  6. ▶ を押して止めます。

先生へ

通しプレイには約1分半かかります。授業時間が足りないときは、Stage_Test を使ってクリア画面まで確認させ、通しプレイは宿題や次回の最初にするとよいでしょう。

チャレンジ課題 ​

  • 自分だけの出現表を作ってみましょう。友だちと交換して遊ぶのもおすすめです
  • Stage2 を作り、StageController の Stage Data を差しかえて、ちがうステージにしてみましょう
  • ゲームオーバーになったあとはボスが出ないようにしてみましょう(ヒント:StageFlow の boss.SetActive(true); の前で GameManager.instance.isPlaying を調べます)
  • WARNING!! の文字を点滅させてみましょう(ヒント:第11回の自機の点滅と同じように、SetActive を 0.2秒ごとに切り替えます)

つまずきポイント ​

こんなときはここを確認
Create に Shooting > Stage Data が出ないConsole にエラーがないか。[CreateAssetMenu(...)] のつづり、: ScriptableObject になっているか
Inspector に Spawns の中身が出ないSpawnInfo の上に [System.Serializable] があるか
敵が1機も出ないStageController の Stage Data に Stage1 が入っているか。各行の Enemy Prefab が空になっていないか
ランダムな敵も出てくるEnemySpawner が非アクティブ(チェック OFF)になっているか
ゲーム開始すぐにボスが出るBoss と BossHPBar が非アクティブになっているか
ボスを倒してもクリア画面が出ないBoss の Is Boss が ON か。GameManager の Clear Panel をつないだか

まとめ ​

  • ScriptableObject は、データだけを入れておくアセットです。ステージの「台本」として使いました
  • [System.Serializable] を付けると、自分で作ったクラスの中身を Inspector で編集できます
  • StageController が表を上から順に読み、時間どおりに敵を出し、最後にボスを出します
  • ボスを倒すと StageClear が呼ばれ、クリア画面が出るようになりました