
前回でゲームオーバーが作れました。でも今のところ、1分たっても10分たっても同じ敵が同じ数だけ出てきます。ヴァンサバ風のゲームが面白いのは、時間がたつほど画面が埋まっていって、さっきまで余裕だった場所が急に危なくなるところです。今回はヴァンサバ風サバイバーゲームのシリーズの10回目として、時間で敵の種類と数が変わるウェーブを作って、10分生き延びたらクリアにしてみたいと思います!
- シリーズの目次:ヴァンサバ風ゲームの作り方
- 前回:#9 HPとゲームオーバーを作ろう
今回作るもの
- 画面に経過時間を
05:32の形で出す - 敵を3種類に増やす(コゲクッキー・ドロドロチョコ・カチカチあめ)
- 時間で出る敵・出る間隔・強さが変わる
- 10分生き延びたらクリア
- 確認用に、時間を早送りできるようにする
作る・直すスクリプトは7つです。
| スクリプト | 役割 | 付ける場所 |
|---|---|---|
| EnemyData(新しく作る) | 敵1種類の性能(見た目・HP・速さ・触れたときのダメージ) | データのアセット |
| WaveData(新しく作る) | 「何分から・どの敵・何秒に1体・強さ何倍」の表 | データのアセット |
| WaveManager(新しく作る) | 経過時間を数えて、今のウェーブを決める。10分でクリア | WaveManager |
| GameClearView(新しく作る) | クリアのパネルを出す | Canvas |
| Enemy(直す) | データを見て、見た目とHPを変える | 敵のプレハブ |
| EnemySpawner(直す) | 今のウェーブの敵を、今の間隔で出す | EnemySpawner |
| StatusView(直す) | 経過時間を出す | Canvas |
前回の続きから
- Unityのバージョンは6.3 LTS
- 記事作成時点では6000.3.22f1で確認しています
- #9の状態:敵に触れられるとHPが減り、0でゲームオーバーになる
ScriptableObjectは12-8 ScriptableObjectで紹介しています。
敵の種類をデータにしよう(EnemyData)
今は敵のプレハブが1つで、HPも速さもそこに直接書いてあります。種類を増やすたびにプレハブを作って数字を入れ直す・・・のは大変なので、#5の武器と同じように、敵1種類ぶんをScriptableObjectのデータにします。
SurvivorsLike/Scriptsに「EnemyData」を作る
using UnityEngine;
// 敵1種類ぶんの性能
[CreateAssetMenu(fileName = "EnemyData", menuName = "SurvivorsLike/Enemy Data")]
public class EnemyData : ScriptableObject
{
public string enemyName = "敵";
public Sprite sprite; // 見た目
public int maxHp = 20; // 体力
public float moveSpeed = 1.5f; // 1秒に進むマス数
public int touchDamage = 10; // 触れたときに与えるダメージ
}
敵のプレハブは1つのままです。見た目もHPも出てくるときにデータから入れ直すので、プレハブは1種類で足ります。#3で作ったプール(ObjectPool)もそのまま使えて、種類ごとにプールを分けなくて済むのが、この作りの良いところです。
3種類ぶん作ろう
SurvivorsLike/DataにEnemiesフォルダを作って、右クリック>Create>SurvivorsLike>Enemy Dataで3つ作ります。
| アセット | Enemy Name | Sprite | Max Hp | Move Speed | Touch Damage |
|---|---|---|---|---|---|
| CookieData | コゲクッキー | enemy_cookie | 20 | 1.5 | 10 |
| ChocoData | ドロドロチョコ | enemy_choco | 15 | 2.6 | 10 |
| CandyData | カチカチあめ | enemy_candy | 60 | 0.9 | 20 |
3種類の性格をはっきり分けるのがコツです。ドロドロチョコは柔らかいけど速いので、離れていても追いつかれます。カチカチあめは硬くて遅いかわりに、触れると痛い。同じ「強い敵」でも、プレイヤーが取るべき行動が変わるようにしています。

Enemyにデータを読ませよう
Enemyに、出てくるときにデータを受け取る処理を足します。
[SerializeField] private SpriteRenderer spriteRenderer;
// 出す前に、EnemySpawnerから呼んでもらう。strength は時間による強さの倍率
public void Setup(EnemyData data, float strength)
{
spriteRenderer.sprite = data.sprite;
maxHp = Mathf.RoundToInt(data.maxHp * strength);
moveSpeed = data.moveSpeed;
touchDamage = Mathf.RoundToInt(data.touchDamage * strength);
}
- フィールドはそのまま
maxHp・moveSpeed・touchDamageは[SerializeField] privateのままでかまいません。同じクラスの中から書き換えています
spriteRendererの欄を足した- 見た目を種類ごとに差し替えるために、敵の
Sprite Rendererを持たせます
- 見た目を種類ごとに差し替えるために、敵の
SetupはSpawnより先に呼びます
Spawnの中でhp = maxHp;としているので、順番が逆だと使い回した敵が、前の種類のHPのまま出てきます。硬いカチカチあめのはずなのに一撃で倒れる・・・といった、原因の分かりにくいズレになるので注意してください。
プレハブに欄を入れる
- Projectウインドの
SurvivorsLike/PrefabsでEnemyをダブルクリックして開く - Enemyコンポーネントに増えたSprite Rendererの欄に、同じオブジェクトのSprite Rendererをドラッグする
- ここが空のままだと、敵を出すたびに
NullReferenceExceptionのエラーが出て、敵がちゃんと出てきません
- ここが空のままだと、敵を出すたびに
時間ごとの表を作ろう(WaveData)
「何分から・どの敵が・何秒に1体・強さ何倍」を、1つの表にまとめます。
SurvivorsLike/Scriptsに「WaveData」を作る
using UnityEngine;
// ウェーブ1つぶん(この時間から、この敵たちが、この間隔で出る)
[System.Serializable]
public class Wave
{
public float startMinutes; // 何分から
public EnemyData[] enemies; // 出る敵(この中からランダムに選ぶ)
public float spawnInterval = 1f; // 何秒に1体
public float strength = 1f; // HPと攻撃力の倍率
}
// ウェーブの表
[CreateAssetMenu(fileName = "WaveData", menuName = "SurvivorsLike/Wave Data")]
public class WaveData : ScriptableObject
{
public Wave[] waves;
// 経過時間(秒)から、今のウェーブを返す
public Wave Current(float seconds)
{
Wave current = waves[0];
foreach (Wave wave in waves)
{
if (seconds >= wave.startMinutes * 60f)
{
current = wave;
}
}
return current;
}
}
[System.Serializable]MonoBehaviourでもScriptableObjectでもないただのクラスは、これを付けるとInspectorに表示できるようになります- 付け忘れると、Wavesの欄に何も出てこなくて悩むことになります
Currentの書き方- 上から順に見て、開始時間を過ぎているものを上書きしていくだけです
- 最後に当てはまったものが選ばれるので、表は開始時間の早い順に並べておきます。1つ目(Element 0)は0分にしておくと、始まった直後から敵が出ます
表を作ろう
SurvivorsLike/Dataで右クリック>Create>SurvivorsLike>Wave Data、名前を「WaveData」にする- Wavesのサイズを4にして、次のように入れる
| # | Start Minutes | Enemies | Spawn Interval | Strength |
|---|---|---|---|---|
| 0 | 0 | CookieData | 1 | 1 |
| 1 | 3 | CookieData / ChocoData | 0.7 | 1.5 |
| 2 | 6 | CookieData / ChocoData / CandyData | 0.5 | 2 |
| 3 | 8 | CookieData / ChocoData / CandyData | 0.35 | 3 |
- 敵の出る間隔は 1秒 → 0.7秒 → 0.5秒 → 0.35秒 と縮んでいきます。8分からは、最初の約3倍の数が出てくる計算です
- StrengthはHPと触れたときのダメージにかかります。速さは種類ごとの性格なので、倍率をかけていません

これで、ゲームの難しさはスクリプトではなく、この表の数字で決まるようになりました。敵を強くしたい・数を増やしたいときも、スクリプトには触らずにWaveDataのInspectorを書き換えるだけです。
経過時間を数えよう(WaveManager)
SurvivorsLike/Scriptsに「WaveManager」を作る
using System;
using UnityEngine;
// 経過時間を数えて、今のウェーブを決める。10分たったらクリア
public class WaveManager : MonoBehaviour
{
[SerializeField] private WaveData waveData;
[SerializeField] private float clearMinutes = 10f;
[SerializeField] private float testSpeed = 1f; // 確認用。10にすると10倍の速さで進む
public float Elapsed { get; private set; }
public Wave Current => waveData.Current(Elapsed);
// 10分たったことを知らせる
public event Action Cleared;
private bool cleared;
private void Update()
{
if (cleared)
{
return;
}
Elapsed += Time.deltaTime * testSpeed;
if (Elapsed >= clearMinutes * 60f)
{
cleared = true;
Cleared?.Invoke();
}
}
}
Clearedはeventで知らせる- #9の
PlayerHealthのDiedと同じ形です。クリアしたときに何をするかは、聞いている側(このあと作るGameClearView)が決めます
- #9の
testSpeedを入れておく- 10分ぶんを毎回遊んで確かめるのは、現実的ではありません
- 10にすれば1分で10分ぶん進むので、後半のウェーブやクリアの確認がすぐ出来ます
- Hierarchyで右クリック>Create Empty、名前を「WaveManager」にして、Add Component>WaveManager
- Wave Dataに
WaveDataのアセットを入れる
- Wave Dataに
Test Speedは、確認が終わったら1に戻す
10のままビルドすると、遊ぶ人のゲームも10倍の速さで時間が進み、1分でクリアになってしまいます。敵の出る間隔はTime.deltaTimeのままなので、時間の表示とウェーブだけが早送りになります。
敵の出し方を、ウェーブに合わせよう
EnemySpawnerを直します。間隔も敵の種類も、毎回ウェーブに聞くようにします。
[SerializeField] private WaveManager waveManager;
Updateを次のようにします(spawnIntervalの欄は使わなくなるので消してかまいません)。
private void Update()
{
Wave wave = waveManager.Current;
timer += Time.deltaTime;
if (timer < wave.spawnInterval)
{
return;
}
timer -= wave.spawnInterval;
float angle = Random.value * Mathf.PI * 2f;
Vector2 around = new Vector2(Mathf.Cos(angle), Mathf.Sin(angle));
Vector2 position = (Vector2)target.position + around * spawnDistance;
EnemyData data = wave.enemies[Random.Range(0, wave.enemies.Length)];
Enemy enemy = pool.Get();
enemy.Setup(data, wave.strength);
enemy.Spawn(position, target, pool, gemSpawner, damageTexts);
}
- HierarchyでEnemySpawnerを選び、Wave ManagerにHierarchyの
WaveManagerを入れる
スライム後半は画面が焦げたお菓子だらけになりますね・・・



そこが気持ちいいところ!最初の1分をわざとヒマにしておくと、増えたときの差がもっと出るんだ
経過時間を画面に出そう
StatusViewに足します。
[SerializeField] private WaveManager waveManager;
[SerializeField] private TextMeshProUGUI timeText;
Updateに1行足します(hpBarFillの行の下あたり)。
timeText.text = TimeSpan.FromSeconds(waveManager.Elapsed).ToString(@"mm\:ss");
using System;を先頭に足してくださいTimeSpanで05:32の形にする- 秒を60で割って・・・と自分で計算しなくても、
TimeSpan.FromSecondsと書式指定でそのまま作れます @"mm\:ss"の@は「\をそのままの文字として扱う」という意味です
- 秒を60で割って・・・と自分で計算しなくても、
画面に置く
- Canvasの
LevelTextを選んでCtrl+Dで複製し、名前を「TimeText」にする- Anchor Presetsでtop・center、Pos X:0、Pos Y:-110、Width:300、Height:70
- Font Size:56、Alignment:中央・中央
- HierarchyでCanvasを選び、StatusViewに増えた2つの欄を入れる
- Wave Manager:WaveManager、Time Text:TimeText


10分生き延びたらクリアッ!
#9のゲームオーバーとほとんど同じものを、クリア用に作ります。
- CanvasのGameOverPanelを選んでCtrl+Dで複製し、名前を「ClearPanel」にする
- 中の
GameOverTextの名前を「ClearText」に、Textを「CLEAR!」、Colorを黄色(R:255、G:217、B:51 ぐらい)にする - 中の
RetryButtonの名前は「ClearRetryButton」にする - チェックを外して非表示にしておく
- 中の
SurvivorsLike/Scriptsに「GameClearView」を作る
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
// 10分たったらゲームを止めて、クリアのパネルを出す
public class GameClearView : MonoBehaviour
{
[SerializeField] private WaveManager waveManager;
[SerializeField] private GameObject panel;
[SerializeField] private Button retryButton;
private void OnEnable()
{
waveManager.Cleared += Show;
retryButton.onClick.AddListener(Retry);
}
private void OnDisable()
{
waveManager.Cleared -= Show;
retryButton.onClick.RemoveListener(Retry);
}
private void Show()
{
panel.SetActive(true);
Time.timeScale = 0f;
}
private void Retry()
{
Time.timeScale = 1f;
SceneManager.LoadScene(SceneManager.GetActiveScene().name);
}
}
- HierarchyでCanvasを選び、Add Component>GameClearView。3つの欄を入れる
- Wave Manager:WaveManager、Panel:ClearPanel、Retry Button:ClearRetryButton
見比べると、違うのは購読するイベントと出すパネルだけです。「まとめたほうがいいのでは?」と思った方は、その感覚で合っています。
ただ、この2つはこれから違うものに育ちます。クリアには「生き延びた時間」「倒した数」を出したいし、ゲームオーバーには「もう少しだった」の演出を入れたくなります。無理に共通化するのは、違いがはっきりしてからで十分です。次回、タイトルとリザルトを作るときに、あらためて整理します。
動かして確認!
WaveManagerのTest Speedを10にして再生すると、1分で10分ぶん進みます。


#10の動作確認
- 画面の上に経過時間が出て、
00:05のように増えていく - 3分を過ぎるとドロドロチョコ(速い敵)が混ざり始める
- 6分を過ぎるとカチカチあめ(硬い敵)が出てきて、倒すのに何発もかかる
- 後半は敵の数が明らかに増える(0.35秒に1体)
- 同じコゲクッキーでも、後半のほうがHPが高い(ダメージの数字を何回も当てる必要がある)
- 10分で止まって「CLEAR!」が出る
- Test Speedを1に戻すと、ふつうの速さに戻る





「CLEAR!」が出たら成功!確認が終わったら、Test Speedを1に戻すのを忘れないでね
難易度の決め方
数字をどう決めたかを書いておきます。正解はないので、考え方だけ参考にしてください。
| 時間 | ねらい |
|---|---|
| 0〜3分 | わざとヒマにする。強化を2〜3回選ばせて、武器を育てる時間 |
| 3〜6分 | 速い敵を混ぜて、立ち止まれなくする |
| 6〜8分 | 硬い敵で火力が足りているかを試す。足りないと押し切られる |
| 8〜10分 | 数で押す。ここまで育っていれば耐えられるように |
いちばん大事なのは最初の1分です。ここが忙しいと、強化を選ぶ余裕がないまま後半に入って、何も出来ずに終わります。最初はぬるすぎるぐらいでちょうどいいというのが、作ってみての実感でした。



数字をいじり始めると、気づいたら1時間たってます・・・
症状から、どの数字を触ればいいかを決めると原因が絞りやすいです。
- 後半に押し切られる →
strengthを下げる(敵を弱くする) - 途中でヒマ →
spawnIntervalを短くする(数を増やす) - 強化が間に合わない → 最初のウェーブを長くする(次のウェーブの
startMinutesを遅らせる)
一度に1つの数字だけ変えて、同じ遊び方で見比べるのがコツです。
ボスを出したい人へ
本家には、決まった時間に出てくる強い敵がいます。同じことをやるなら、EnemyDataをもう1つ作るだけでも「それっぽいもの」は作れます。
EnemyDataのアセットでBossDataを作って、Max Hpを2000ぐらいにするWaveにbossAtMinutesのような欄を足して、その時間になったら1体だけ出す- 倒したら特別なものを落とす →
Enemyの倒れる処理を分ける
ボスらしい動き(途中で止まる・取り巻きを呼ぶ)を入れ始めると、Enemy1つでは足りなくなります。Enemyを親にしてBossEnemyを作るのが、#5・#7でやってきたやり方(WeaponData・Weaponの親クラス)の続きになります。
あとがき
今回でゲームとして最初から最後まで遊べるようになりました。時間の表と数字を少し変えるだけで、難しさがガラッと変わるのが面白いところです。ぜひ自分が気持ちよく遊べる数字を探してみてください!次回はいよいよ最終回、タイトル画面とリザルトを付けて、Windowsで遊べる形にして配れるところまで作ります。
コメント
コメント一覧 (1件)
[…] 前回:#10 時間で強くなる敵のウェーブを作ろう […]