ダークモード
第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 ウィンドウに置くファイル)です。
| MonoBehaviour | ScriptableObject | |
|---|---|---|
| どこにある? | シーンの中のオブジェクトに付ける | Project ウィンドウにファイルとして置く |
| 何をする? | 動き(Update など) | データを持つだけ |
| たとえると | 役者 | 台本 |
「台本」を別のファイルにしておくと、2面目を作りたいときは台本(Stage2)を作って差しかえるだけで済みます。
2. StageData と SpawnInfo
2-1. StageData スクリプトを作る
- Project ウィンドウで
Assets/_Game/Scriptsを開きます。 - 右クリックし、Create > Scripting > MonoBehaviour Script を選びます。
- 名前を
StageDataにします。 - ダブルクリックして開き、中身を 全部消して、次のコードを書きます。
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 アセットを作る
- Project ウィンドウで
Assets/_Game/Dataを開きます。 - 何もないところを右クリックし、Create > Shooting > Stage Data を選びます。
- 名前を
Stage1にします。 Stage1をクリックすると、Inspector に Spawns と Boss Time が表示されます。
メニューに出てこないとき
Create の中に Shooting が出ないときは、Console にエラー(赤い文字)が出ていないか確認しましょう。スクリプトにエラーがあると、メニューが追加されません。
3-2. 表の例
今回は、次の表のとおりに入れてみましょう。全部で15行です。
| Element | Time | Enemy Prefab | Y | Count | Gap |
|---|---|---|---|---|---|
| 0 | 2 | Enemy_Straight | 2 | 1 | 0.3 |
| 1 | 4 | Enemy_Straight | -2 | 1 | 0.3 |
| 2 | 7 | Enemy_Wave | 0 | 5 | 0.3 |
| 3 | 11 | Enemy_Straight | 3 | 3 | 0.5 |
| 4 | 14 | Enemy_Red | 1 | 5 | 0.3 |
| 5 | 18 | Enemy_Shooter | -3 | 1 | 0.3 |
| 6 | 21 | Enemy_Wave | 2 | 5 | 0.3 |
| 7 | 25 | Enemy_Shooter | 3 | 2 | 0.8 |
| 8 | 29 | Enemy_Red | -1 | 5 | 0.3 |
| 9 | 33 | Enemy_Straight | 0 | 4 | 0.4 |
| 10 | 37 | Enemy_Wave | -2 | 6 | 0.25 |
| 11 | 41 | Enemy_Shooter | 0 | 3 | 0.6 |
| 12 | 45 | Enemy_Red | 2 | 5 | 0.3 |
| 13 | 50 | Enemy_Straight | -3 | 5 | 0.3 |
| 14 | 54 | Enemy_Shooter | 2 | 2 | 1 |
Boss Time は 60 にします。
表のコツ
- Time は必ず「小さい順」に並べます
- 最初はやさしく、だんだんむずかしくします
Enemy_Red(カプセルを落とす敵)をときどき入れて、パワーアップできるようにします- 最後の敵(54秒)からボス(60秒)まで、少し「静かな時間」を作ると、ボス前のドキドキが出ます
3-3. 表を入力する
Stage1を選びます。- Inspector の Spawns の左の三角をクリックして開きます。
- Spawns の右にある数字(要素の数)を
15にして Enter を押します。Element 0 〜 Element 14 ができます。 - Element 0 の三角を開きます。
- Time に
2を入れます。 - Enemy Prefab に、
Assets/_Game/PrefabsのEnemy_Straightをドラッグします。 - Y に
2、Count に1、Gap に0.3を入れます。 - 同じようにして、Element 1 〜 Element 14 を表のとおりに入れます。
- Boss Time が
60になっていることを確認します。 - File > Save Project を選んで保存します。
数を増やすと値がコピーされる
要素の数を増やすと、新しい要素には一番下の要素の値がコピーされます。数字の入れまちがいがないか、最後に表と見比べましょう。
4. StageController で表のとおりに出す
4-1. StageController スクリプトを作る
Assets/_Game/Scriptsで右クリックし、Create > Scripting > MonoBehaviour Script を選びます。- 名前を
StageControllerにします。 - ダブルクリックして開き、中身を全部消して、次のコードを書きます。
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 を置く
- Hierarchy で右クリックし、Create Empty を選びます。
- 名前を
StageControllerにします。 - Position を (11, 0, 0) にします(
EnemySpawnerと同じく、画面の右の外です)。 - Add Component で Stage Controller を追加します。
- Stage Data に、Project の
Assets/_Game/Data/Stage1をドラッグします。
Boss、Boss HP Bar、Warning Text は、このあとつなぎます。
4-4. EnemySpawner を止める
これからは StageController が敵を出すので、ランダムに出す EnemySpawner は使いません。
- Hierarchy で
EnemySpawnerを選びます。 - Inspector の名前の左のチェックボックスを OFF にします。
消さずに残しておく
EnemySpawner は消さずに、非アクティブで残しておきます。「エンドレスモード」を作りたくなったときに使えます。
4-5. ボスと体力バーを非アクティブにする
ボスと体力バーは、最初は隠しておき、StageController が出すようにします。
- Hierarchy で
Bossを選びます。 - Position が (14, 0, 0) であることを確認します。
- Inspector の名前の左のチェックボックスを OFF にします。
- Hierarchy で
Canvasの中のBossHPBarを選びます。 - チェックボックスを OFF にします。
- Hierarchy で
StageControllerを選びます。 - Boss に、Hierarchy の
Bossをドラッグします。 - Boss HP Bar に、Hierarchy の
BossHPBarをドラッグします。
非アクティブでもつなげる
非アクティブ(グレーの文字)のオブジェクトも、Hierarchy からドラッグしてつなぐことができます。
5. WARNING 表示
5-1. WARNING の文字を作る
- Hierarchy の
Canvasを右クリックし、UI > Text - TextMeshPro を選びます。 - 名前を
WarningTextにします。 - Anchor Presets で middle-center(真ん中)を選びます。
- Pos X を 0、Pos Y を 0、Width を 1000、Height を 200 にします。
- TextMeshPro - Text (UI) の文字の欄に
WARNING!!と入力します。 - Font Size を 120 くらいにします。
- Vertex Color を赤にします。
- Alignment で、横も縦も中央ぞろえのボタンを押します。
- Inspector の名前の左のチェックボックスを OFF にして、非アクティブにします。
- Hierarchy で
StageControllerを選び、Warning Text にWarningTextをドラッグします。 - シーンを保存します(Ctrl+S、Macは Cmd+S)。
5-2. 動作確認(ボスが出るまで)
- ▶ を押します。
- 2秒後に上のほうから敵が1機、4秒後に下のほうから1機、7秒後に波の敵が5機…と、表のとおりに出てくればOKです。
- 60秒たつと WARNING!! が3秒表示され、そのあとボスと体力バーが出てくればOKです。
- ▶ を押して止めます。
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 をコピーして作ります。
- Hierarchy で
Canvasの中のGameOverPanelを選びます。 - Ctrl+D(Macは Cmd+D)で複製します。
- 複製したものの名前を
ClearPanelにします。 ClearPanelの左の三角を開き、GAME OVERと書いてある文字のオブジェクトを選びます。- 文字を
STAGE CLEARに変えます。色も黄色などに変えるとクリアらしくなります。 ClearPanelの中のRetryButtonを選び、Delete キーで消します。ClearPanelの中のTitleButtonを選び、Button の On Click () にGameManagerの BackToTitle が入っていることを確認します。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 をつなぐ
- Hierarchy で
GameManagerを選びます。 - Clear Panel に、Hierarchy の
ClearPanelをドラッグします。 - シーンを保存します(Ctrl+S、Macは Cmd+S)。
6-5. 動作確認(通しプレイ)
StageControllerの Stage Data がStage1になっていることを確認します。- ▶ を押して、最初から最後まで遊んでみます。
- 表のとおりに敵が出て、60秒で WARNING!!、そのあとボスが出ればOKです。
- ボスを倒すと、爆発して体力バーが消え、STAGE CLEAR の画面が出ればOKです。
- TITLE ボタンを押すと、タイトル画面にもどればOKです。
- ▶ を押して止めます。
先生へ
通しプレイには約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が呼ばれ、クリア画面が出るようになりました