前回(9回)は弓関連のUIを追加し、レベルアップや売却が実際に出来るようになりました。
前回の記事↓

今回はついにこのタワーディフェンス講座も最終回。タイトルやゲームクリア・ゲームオーバーなどの「ゲームループ」部分を主に作って完成させていきます。
EnemyManagerUIの作成
ゲームループの前に、EnemyManagerの情報表示を作りましょう。
第8回で作ったPlayerStatusUI オブジェクトを複製(CTRL+D)して作られるPlayerStatusUI (1) を EnemyManagerUIと名前を変更し、同じく複製されているPlayerStatusUIスクリプトは削除(右クリックしてRemoveComponent)します。
位置も重なってしまっているので Pos Yを下に下げます。 -500ぐらいが適当です。
そのまま複製され子要素の Text(HP)と Text(GOLD)も名前をText(WAVE)とText(ENEMY)に変更します。
EnemyManagerUI に AddComponent→New Script → EnemyManagerUI と選択し、EnemyManagerUIスクリプトを作成し、下記のように編集します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; public class EnemyManagerUI : MonoBehaviour { public EnemyManager enemyManager; public Text waveText; public Text enemyText; void Update() { waveText.text = $"WAVE:{enemyManager.wave + 1}/{enemyManager.waves.Length}"; enemyText.text = $"ENEMY:{enemyManager.EnemyCnt}"; } } |
メンバ変数は
- どのEnemyManagerの情報を表示するか(Inspectorで設定)
- EnemyManagerのWave情報を表示するためのText(Inspectorで設定)
- EnemyManagerのEnemy数情報を表示するためのText(Inspectorで設定)
の3つです。
Update関数の中では UIのTextにWave情報と敵の数情報をセットしています。
ただ、この
1 |
enemyText.text = $"ENEMY:{enemyManager.EnemyCnt}"; |
のenemyManager.EnemyCnt
はまだEnemyManagerスクリプトに無いので EnemyCnt
プロパティを追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
using System; using System.Collections; using System.Collections.Generic; using UnityEngine; public class EnemyManager : MonoBehaviour { public Wave[] waves; public int wave; public float time; //まだこのwaveで出現してない敵+画面上の敵の数 public int EnemyCnt => waves[wave].patterns.Count + FindObjectsOfType<Enemy>().Length; ~~以下略~~ |
まだ現在のwaveで出現していない敵の数は waves[wave].patterns.Count
になります。
そして、既に出現している敵(Enemy)は FindObjectsOfType<Enemy>().Length
で取得し、それを足す事で敵残数を取得できるプロパティにしています(=>
を使ったgetプロパティの省略記法です)
注意点としては waves[wave].patterns
はListなのでCount
で要素数を取得し、FindObjectsOfType<Enemy>()
は配列なのでLength
で要素数を取得しています。
2スクリプトを修正しましたので、忘れずに2つとも保存をしましょう。
UnityEditorのInspectorでEnemyManagerUIのEnemyManager、Text(WAVE)、Text(GOLD)をセットします。
それではプレイボタンを押して、確認してみましょう。

ENEMYを倒すたびに数字が減っているのが確認できます
GOLDの獲得とダメージ処理
大分ゲームが完成に近づいてきましたが、まだ
- 敵を倒してもGOLDが増えない
- 敵がゴール(本拠地)に到着してもHPが減らない
ので、そこもやってしまいましょう。
敵を倒したらGOLDを増やす
矢(Arrow)スクリプトで敵(Enemy)を倒した時に処理を追加します。
23 24 25 26 27 28 |
targetEnemy.hp -= 1; if (targetEnemy.hp <= 0) { Destroy(targetEnemy.gameObject); FindObjectOfType<Player>().gold += targetEnemy.gold; } |
FindObjectOfType<Player>()
でPlayerオブジェクトを探し、gold
を倒した敵の金額(targetEnemy.gold
)分増やしています。
最初からHierarchyに置いてあるオブジェクトであれば、InscpectorからPlayerオブジェクトをセットしておけばいいのですが、動的に生成(Instantiate)されるArrowオブジェクトから、どうやってPlayerオブジェクトのgoldを増やすのか(どうやってPlayerオブジェクトを認識するのか)が結構悩みどころです(講座を作るにあたっても悩みどころです)
これには以下のような色々な方法
- Arrowを発射するBowに設置をしたPlayerオブジェクトを保持させ、バケツリレーのように渡していく
- Playerオブジェクトは1つしかないので Playerオブジェクトをstatic を使ったシングルトンオブジェクトにする
- Find系関数を使ってPlayerオブジェクトを探す
等々があるのですが、今回はFind系関数である FindObjectOfType<探したい型>() を使っていますが深い理由はありません(強いて言えば修正するスクリプトが少ない方が混乱が少ないという講座制作上の理由になります)他の方法を使っても問題ありません。
敵がゴール(本拠地)に到着したらHPを減らす
次に敵(Enemy)スクリプトを修正します。
34 35 36 37 38 39 |
if (pointIndex >= route.points.Length - 1)//最後まで到達した { Destroy(gameObject); //プレイヤーにダメージ FindObjectOfType<Player>().hp--; } |
FindObjectOfType<Player>()
でPlayerオブジェクトを探し、hp
を減らしています。
ArrowスクリプトとEnemyスクリプト、保存をしてUnityEditorで変更を確認してみます。

GOLDが増えHPが減るようになりました。
ゲームループの作成
今更ですがゲームループとは
このように、ゲームの状態(ステート:TITLE や WAVE_CHANGEなど)の遷移をループ状にすることで繰り返しゲームがプレイ出来る状態を言います。
ステート表示Text
さて。ゲームステート(状態)とゲームループを作っていくんですが、最初にゲームステートを表示するためのTextを追加します。
HierarchyビューのCanvasを右クリックしてUI→Textと選択し、Textを追加します。
- 名前:Text(State)
- Anchor Presets:stretch/stretch (SHIFT+ALT)
- Right:256
- Font : Assets/Fonts/ShigotoMemogaki-Regular-1-01
- Text : “らくがきたわーでぃふぇんす”
- Font Style:BOLD
- Font Size:90
- Alignment:中央寄せ

このようになります。位置はSceneビューで修正しても構いません。
GameMainスクリプトの作成
Hierarchyビューに、新規オブジェクト(CTRL+SHIFT+N)を作成し、新規ScriptもAddComponent→NewScriptで作成します。
- 名前:GameMain
- スクリプト名:GameMain
早速GameMainスクリプトを修正していきましょう。
ゲームループコルーチンの作成
まず、ゲームループの大外枠を作っていきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
using System.Collections; using System.Collections.Generic; using UnityEngine; public class GameMain : MonoBehaviour { void Start() { StartCoroutine(GameLoop()); } private IEnumerator GameLoop() { while (true) { yield return null; } } } |
Start関数の中で StartCoroutine(GameLoop())
; でGameLoop()
を対象にコルーチンを開始しています。
では。GameLoop()関数はというと
14 15 16 17 |
while (true) { yield return null; } |
内部では while(true) が使われています。これは終了条件が無いので無限ループですが、yield return null;
で中断と再開をしています。タワーディフェンス講座第7回 でやった、弓(Arrow)が一定間隔で弾を撃つ処理と同じ仕組みです。
このwhile(true){}
の中にゲームループを作っていきます。
ゲームステートの準備
次に、ゲームの状態(STATE:ステート)とその遷移(動き)を準備していきます。
再度この画像を持ってきました。
実はこの画像の遷移図は実際に今回のタワーディフェンスのゲームステートの流れを示したものです。
この、TITLEやGAME PLAYが状態(state)です。これらを全てenumで宣言してしまいます。
1 2 3 4 5 6 7 8 9 |
enum GAME_STATE { TITLE, GAME_PLAY, WAVE_CHANGE, WAVE_CLEAR, GAME_CLEAR, GAME_OVER } |
このGAME_STATE enumをメンバ変数で宣言します。
1 |
private GAME_STATE state; |
上記状態遷移の画像を見ると分かりますが、最初は TITLE から始まっています。
なので、Start関数の中でstateはTITLEにしておきましょう。
1 2 3 4 5 |
void Start() { state = GAME_STATE.TITLE; StartCoroutine(GameLoop()); } |
そして、この状態によってゲームの動きを変えていくので、GameLoop関数の中でstate
によって処理が分岐できるようswitch文を入れます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
private IEnumerator GameLoop() { while (true) { switch (state) { case GAME_STATE.TITLE: break; case GAME_STATE.GAME_PLAY: break; case GAME_STATE.WAVE_CHANGE: break; case GAME_STATE.WAVE_CLEAR: break; case GAME_STATE.GAME_CLEAR: break; case GAME_STATE.GAME_OVER: break; } yield return null; } } |
ここまでで、GameMainスクリプト全体は以下のようになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
using System; using System.Collections; using System.Collections.Generic; using UnityEngine; public class GameMain : MonoBehaviour { enum GAME_STATE { TITLE, GAME_PLAY, WAVE_CHANGE, WAVE_CLEAR, GAME_CLEAR, GAME_OVER } private GAME_STATE state; void Start() { state = GAME_STATE.TITLE; StartCoroutine(GameLoop()); } private IEnumerator GameLoop() { while (true) { switch (state) { case GAME_STATE.TITLE: break; case GAME_STATE.GAME_PLAY: break; case GAME_STATE.WAVE_CHANGE: break; case GAME_STATE.WAVE_CLEAR: break; case GAME_STATE.GAME_CLEAR: break; case GAME_STATE.GAME_OVER: break; } yield return null; } } } |
※今回はスクリプトが今まで以上に長くなってきますので、時々スクリプトの保存をして、エラーが無いことを確認しましょう。
ステートによる画面表示
先ほど用意したステート表示Textに、ステート毎にメッセージを表示するように追加をします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
using System; using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; public class GameMain : MonoBehaviour { enum GAME_STATE { TITLE, GAME_PLAY, WAVE_CHANGE, WAVE_CLEAR, GAME_CLEAR, GAME_OVER } [SerializeField] private GAME_STATE state; [SerializeField] private Text stateText; void Start() { state = GAME_STATE.TITLE; StartCoroutine(GameLoop()); } private IEnumerator GameLoop() { while (true) { switch (state) { case GAME_STATE.TITLE: stateText.text = "らくがきたわーでぃふぇんす"; break; case GAME_STATE.GAME_PLAY: stateText.text = ""; break; case GAME_STATE.WAVE_CHANGE: stateText.text = "WAVE CHANGE"; break; case GAME_STATE.WAVE_CLEAR: stateText.text = "WAVE CLEAR"; break; case GAME_STATE.GAME_CLEAR: stateText.text = "GAME CLEAR"; break; case GAME_STATE.GAME_OVER: stateText.text = "GAME OVER"; break; } yield return null; } } } |
要点を説明していきます。まず
1 2 3 4 |
[SerializeField] private GAME_STATE state; [SerializeField] private Text stateText; |
はprivate Text stateText;
は先ほど用意した、ステート表示用TextをInspectorからセットするために用意した変数です。
[SerializeField]
急に[SerializeField]
という記述が出てきましたが、これはC#でのクラス内のメンバ変数(メンバ関数)のprivateやpublic等のアクセス制限(カプセル化・データ隠蔽)の話と密接な関係があります。
簡単に説明をするとpublic は、一番弱いアクセス制限(というか制限無し)でどこからでもアクセスを許す、他のクラスから見てノーガードなメンバ変数になります。また、publicにするとunityエディタのInspectorで値がセット出来るようになります。
対してprivate は、強固なアクセス制限で、「自分自身から」しかアクセスを許さず、外からのアクセスは出来ません、雑に言うとガードが硬くなります。そのため、外部からアクセスしてほしくない大事なメンバはprivateで宣言します。
メンバ変数は適切にprivateで宣言することで、その変数を変更できる場所を減らし、何かの間違いで想定しない値を入れてしまうことを防ぐことが出来、結果バグを減らす効果があります。(何かしら変数の値が原因でバグがあった時なども、privateで宣言してある変数は外から変更される事が無い=外部からの影響を考慮することが減るという事が重要です。例えば、publicだと同じ名前の変数を別プログラムで値を変えてバグを作っても気付けないなどの問題が発生します。)
そして、privateなメンバ変数はエディタのInspectorで値がセット出来ません。アクセス権のガードが堅いためです。
しかしこの private
なメンバ変数も[SerializeField]
を付ける事でInspectorで値が変更できるようになります。private
と[SerializeField]
を併用することで、安全性を担保しつつUnityEditor上からは値が変更できるようにし、利便性だけを高めることが出来ます。
そう考えると、今までもInspectorから値をセットしたいだけのメンバ変数がpublicで宣言されていたりしていたような・・・? 適切にprivate
と[SerializeField]
に直してみるのも良い経験になると思います。
ぜひ、この記事を読み終わったらこれまでの記事を復習しつつプログラムを改良してみてください。
private GAME_STATE state;
もInspectorから値を変更したいので[SerializeField]
を付けました。
そして、stateのswitch
で表示メッセージを変更しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
switch (state) { case GAME_STATE.TITLE: stateText.text = "らくがきたわーでぃふぇんす"; break; case GAME_STATE.GAME_PLAY: stateText.text = ""; break; case GAME_STATE.WAVE_CHANGE: stateText.text = "WAVE CHANGE"; break; case GAME_STATE.WAVE_CLEAR: stateText.text = "WAVE CLEAR"; break; case GAME_STATE.GAME_CLEAR: stateText.text = "GAME CLEAR"; break; case GAME_STATE.GAME_OVER: stateText.text = "GAME OVER"; break; } |
保存をして、UnityEditorでInspectorでState Text に Text(State)をセットします。
では、再生をして確認してみましょう。 再生中にInscpetorからStateを変更してみると、表示されるTextが変更するようになりました。
状態(State)の遷移
では、状態(State)を遷移条件に応じて遷移させていきます。
なんと3度目の登場、状態(State)の遷移図です。
遷移条件は矢印の近くに書いてありますね。
- 敵を全て倒した
- 次wave無し
- HP0
- キーが押された x 4
この中の「敵を全て倒した」・「次wave無し」はEnemyManagerが無いと分からないので、EnemyManagerオブジェクトをInspectorから指定できるようにメンバ変数を作ります。
1 2 |
[SerializeField] private EnemyManager enemyManager; |
次に 「HP0」 は PlayerのHPを表すので、PlayerオブジェクトをInspectorから指定できるようにメンバ変数を作ります。
1 2 |
[SerializeField] private Player player; |
そして「キーが押された」は、「何かキーが押されたら再開」という意味になるので、WaitUntil というクラスを使います。
yield return new WaitUntil( () => Input.anyKey );
このようにラムダ式(正確にはdelegate)という記述を使い、条件(今回はInput.anyKey
→何かキーが押されるまで)を指定することでキー入力待ちが実現できます。(第7回講座でちょっと紹介していましたね)
ただ、今回はこの「キーが押された」が色んな状態遷移の条件になっているため、先にこのWaitUntilクラスを生成するプロパティを用意しておきます。
1 |
private WaitUntil WaitAnyKey => new WaitUntil(() => Input.anyKeyDown); |
これで、コルーチンの中であれば
yield return WaitAnyKey;
とすることで、「中断し、キー入力で再開」という処理になります。
以下がスクリプトの追加位置の一例になります(関数内とかでなければどこでも構いませんが)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
~省略~ public class GameMain : MonoBehaviour { enum GAME_STATE { TITLE, GAME_PLAY, WAVE_CHANGE, WAVE_CLEAR, GAME_CLEAR, GAME_OVER } [SerializeField] private GAME_STATE state; [SerializeField] private Text stateText; [SerializeField] private EnemyManager enemyManager; [SerializeField] private Player player; private WaitUntil WaitAnyKey => new WaitUntil(() => Input.anyKeyDown); void Start() { state = GAME_STATE.TITLE; StartCoroutine(GameLoop()); } ~省略~ |
では、順にGameLoop関数のswitch(state)
内のステートの遷移を組み立てていきます。
GAME_STATE.TITLE
からは、「キーが押されたら」「WAVE_CHANGE」へ遷移なのでこうなります。
1 2 3 4 5 |
case GAME_STATE.TITLE: stateText.text = "らくがきたわーでぃふぇんす"; yield return WaitAnyKey; state = GAME_STATE.WAVE_CHANGE; break; |
GAME_STATE.GAME_PLAY
からは、「HPが0になったら」「GAME_OVER」へ遷移と「敵を全て倒したら」「WAVE_CLEAR」への遷移です。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
case GAME_STATE.GAME_PLAY: stateText.text = ""; if (player.hp <= 0) { state = GAME_STATE.GAME_OVER; } else if (enemyManager.EnemyCnt == 0) { state = GAME_STATE.WAVE_CLEAR; } break; |
GAME_STATE.WAVE_CHANGE
からは、「キーが押されたら」「GAME_PLAY」への遷移です。
加えて、今から始まるのが第何Waveなのかを表示したいので、stateTextの表示も修正しておきます。
enemyManager.wave
に +1
をしているのは、配列は0から始まりますが、プレイヤーから見るとwaveは1から始まる方が自然なので補正している処理です。
1 2 3 4 5 |
case GAME_STATE.WAVE_CHANGE: stateText.text = $"WAVE {enemyManager.wave+1}"; yield return WaitAnyKey; state = GAME_STATE.GAME_PLAY; break; |
GAME_STATE.WAVE_CLEAR
からは、「次wave無し」は「GAME_CLEAR」へ遷移します。
「次wave無し」は、今のwaveがwave配列(waves)の最終要素にもうなっているかどうか。という判断をすればよいので if (enemyManager.wave == enemyManager.waves.Length-1)
というif文で分岐をしています。
それ以外は「キーが押されたら」「WAVE_CHANGE」へ遷移します。
その際にただ遷移するのではなく、enemyManagerのwaveを+1して次のwaveに変更しています。
またtimeを0にしておく必要があります。
また、ウェーブをクリアした時の残GOLDの20%をBONUSとして付与します。
こうすることで、「なるべく弓を設置せず、GOLDを温存して敵を倒しきるギリギリの数で倒した方が使えるGOLDが増える」という事になり、レベルデザインの幅が広がります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
case GAME_STATE.WAVE_CLEAR: if (enemyManager.wave == enemyManager.waves.Length-1) { state = GAME_STATE.GAME_CLEAR; } else { stateText.text = $"WAVE CLEAR\nBONUS +{player.gold*20/100}"; yield return WaitAnyKey; enemyManager.wave++; enemyManager.time=0; player.gold += player.gold * 20 / 100; state = GAME_STATE.WAVE_CHANGE; } break; |
GAME_STATE.GAME_CLEAR
と GAME_STATE.GAME_OVER
からは、「キーが押されたら」「TITLE」へ遷移なのですが、単純にTITLEへ遷移してもEnemyManagerからは既に敵が出現しているでしょうし、Playerのhpも減っていたりします。
これらを全てリセットするように一生懸命スクリプトを書いても良いですが、もっと手っ取り早い方法があります。
それは、今のシーンをロードしなおしてしまう(SceneManager.LoadScene(0)
)ことです(ただし、SceneManagaerクラスを使う場合は、using UnityEngine.SceneManagement;をスクリプトの最初の方で呼び出す必要がありますので注意してください)。
1 2 3 4 5 6 7 8 9 10 |
case GAME_STATE.GAME_CLEAR: stateText.text = "GAME CLEAR"; yield return WaitAnyKey; SceneManager.LoadScene(0); break; case GAME_STATE.GAME_OVER: stateText.text = "GAME OVER"; yield return WaitAnyKey; SceneManager.LoadScene(0); break; |
これで一通りのステートの遷移を追加しました。
スクリプトを保存してUnityEditorでGameMainオブジェクトのInspectorで今回追加した EnemyManagerとPlayerをそれぞれHierarchyのEnemyManagerオブジェクトとPlayerオブジェクトをドラッグアンドロップしてセットし再生してみましょう。
どうでしょうか。上の例ではGAME_CLEARのみですが、そのまま弓オブジェクトを設置せずに、HPが0になるとGAME_OVERになり、何かキーを押すことでまたTITLEに戻る事で、「ゲームループが回った」状態になったと思います。
ただ、ちょっとおかしいですね。
TITLE画面やWAVE開始前の表示(WAVE_CHANGE)なのに敵が関係なしに動いてしまっています。
これは、本当はGAME_PLAY状態の時だけ敵を生成しないようにしなければいけないところ、EnemyManagerのUpdate内でテスト用のコードが動いてしまっているからです。(タワーディフェンス講座 6参照)
そのため、まずGAME_STATE.GAME_PLAY
で、enemyManagerの時間(time)の更新と、敵の生成(CreateEnemy関数)を呼んであげるようにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
case GAME_STATE.GAME_PLAY: stateText.text = ""; enemyManager.time += Time.deltaTime; enemyManager.CreateEnemy(); if (player.hp <= 0) { state = GAME_STATE.GAME_OVER; } else if (enemyManager.EnemyCnt == 0) { state = GAME_STATE.WAVE_CLEAR; } break; |
保存をし、UnityEditorでHierarchyのEnemyManagerオブジェクトを選択し、Inspectorで、EnemyManagerコンポーネントのチェックを外します。
これで、EnemyManagerのUpdateは呼ばれなくなるため、GAME_PLAY状態の時だけ敵が生成されるようになりました。
(EnemyManagerスクリプトの Update関数を消すように修正しても構いません。)
では、確認してみましょう。
以下がGameMainスクリプトの最終状態になります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 |
using System; using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.SceneManagement; using UnityEngine.UI; public class GameMain : MonoBehaviour { enum GAME_STATE { TITLE, GAME_PLAY, WAVE_CHANGE, WAVE_CLEAR, GAME_CLEAR, GAME_OVER } [SerializeField] private GAME_STATE state; [SerializeField] private Text stateText; [SerializeField] private EnemyManager enemyManager; [SerializeField] private Player player; private WaitUntil WaitAnyKey => new WaitUntil(() => Input.anyKeyDown); void Start() { state = GAME_STATE.TITLE; StartCoroutine(GameLoop()); } private IEnumerator GameLoop() { while (true) { switch (state) { case GAME_STATE.TITLE: stateText.text = "らくがきたわーでぃふぇんす"; yield return WaitAnyKey; state = GAME_STATE.WAVE_CHANGE; break; case GAME_STATE.GAME_PLAY: stateText.text = ""; enemyManager.time += Time.deltaTime; enemyManager.CreateEnemy(); if (player.hp <= 0) { state = GAME_STATE.GAME_OVER; } else if (enemyManager.EnemyCnt == 0) { state = GAME_STATE.WAVE_CLEAR; } break; case GAME_STATE.WAVE_CHANGE: stateText.text = $"WAVE {enemyManager.wave+1}"; yield return WaitAnyKey; state = GAME_STATE.GAME_PLAY; break; case GAME_STATE.WAVE_CLEAR: if (enemyManager.wave == enemyManager.waves.Length-1) { state = GAME_STATE.GAME_CLEAR; } else { stateText.text = $"WAVE CLEAR\nBONUS +{player.gold*20/100}"; yield return WaitAnyKey; player.gold += player.gold * 20 / 100; enemyManager.wave++; enemyManager.time=0; state = GAME_STATE.WAVE_CHANGE; } break; case GAME_STATE.GAME_CLEAR: stateText.text = "GAME CLEAR"; yield return WaitAnyKey; SceneManager.LoadScene(0); break; case GAME_STATE.GAME_OVER: stateText.text = "GAME OVER"; yield return WaitAnyKey; SceneManager.LoadScene(0); break; } yield return null; } } } |
ゲームの根幹かつ最後のスクリプトだけあって、今まで一番長くなりましたね。スクリプトの修正は以上になります。お疲れ様でございました。
Waveの追加・レベルデザイン
これで完成!! と言いたいところですが、この例だとWaveが一つしかないですね。
最後にWaveの追加方法と、さらなるレベルデザイン例を提示してこのタワーディフェンス制作講座の締めくくりとしたいと思います。(既にWaveを自分で量産している方は読み流してください)
敵のWaveを管理しているのは、EnemyManagerオブジェクトでしたね。(タワーディフェンス講座 6/10参照)
今は、Waves の Size が 1 となっているので、1waveで終わってしまいます。
なので Element 0 となっているところを右クリックし、「Dupulicate Array Element」を選択して複製することで、Element 1が増えます。 このWaveの複製を繰り返すことで何Wave構成にもできます。
なお、Element 0 の中にあった Patterns の中身もElement 1に複製されるため、多数Waveの複製をした状態で再生をして確認をすると全てのwaveが全く同じ敵構成の内容ということになります。
もちろんそれでは面白みが無いので、削除したり内容を変更していきます。
Wave2以降も作ってみたら、実際に敵の動きを確認してみたいですよね。
しかし、毎回Wave1から開始して敵を倒してやっとWave2の実際の動きを確認。 では辛いです。
そんな時は EnemyManager のInscpectorビューで Wave を テストしたいwave番号に変更しましょう。指定Waveから開始することが出来るため時間を短縮できます。(もちろん通しでWaveをプレイするのも大事です)
テストが終わったら0に戻すのを忘れないようにしましょう。
レベルデザインの一例
例として簡単に5wave構成のレベルデザインを考えてみましょう。
- wave1~3 は1wave内では各敵1種類だけ出現させ、「この色の敵は、このような特徴がある」というのをプレイヤーに示す、自己紹介waveとし、
- wave4 で、複数の敵が混合で来ることで、難易度をちょっと上げていき、
- wave5 で、計画的に弓を配置していないとゲームオーバーになってしまうような敵の量、経路で敵を配置。
という感じでしょうか。
もちろんもっとWaveを多くしても良いですし、ブロックの配置やゴール、経路も好きに変えてしまいましょう。もっと硬いEnemyオブジェクトを新規で作っても良いです(ちょっと大きくして、最終Waveにボスとして出すというのも良いですね。)
Enemyの落とすGOLDの量を調整する事も必要かもしれません。
Playerの初期HPと所持金も非常に重要なレベルデザインになります。
ちょっと変則ですが、旗(敵からみたゴール)が複数あるステージも考えられます。
是非工夫を凝らしたステキなレベルデザインをしてみてください。
これは敵の速度やHP、GOLDを調整してみたレベルデザイン例になります。(ボスっぽい大型Enemyも作ってみました)
参考になれば幸いです。
おさらい
これにて、ゲームは完成し10回に渡ってお付き合いしていただいたタワーディフェンス制作講座も(ひとまず)終わりになります。
ここで終わりにさせず、別シーンでタイトル画面を作ったり、ステージセレクトを追加したり、効果音や演出を適切に入れる事でよりゲームとして完成されていくと思います。
このタワーディフェンス作成講座をベースに素敵なゲームが完成した際には是非一報頂けると嬉しいです。
ここまで読んでいただき誠にありがとうございます。
タワーディフェンスゲームの作り方講座に戻る↓

コメント
一応完成しました!
内容もわかりやすくまとめていてとても理解しやすかったです!
これからUnityでゲームを作っていく上で参考にしていきたいと思います!
ありがとうございました!
おお!完成おめでとうございます!
完成報告いただけてとてもうれしいです。
これからも新しい講座どんどん作っていくのでまたサイト見に来てもらえたらうれしいです。
こちらこそ最後まで読んでいただきありがとうございました!^^
ここ5日くらい、このブログを人生で初めてゲーム作りを完遂させました! 今までも何度か挑戦しようとしては挫折してを繰り返していたので、完成出来てめちゃくちゃ嬉しかったです!
せっかくなのでこのままUnityゲームジャムに何かしらのゲームを投稿してみようかと思います!
すばらしいコンテンツをありがとーございました!
おおー!初のゲーム完成おめでとうございます!^^
この講座作った甲斐がありました。コメントもいただけてとてもうれしいです。
ちょうどunityゲームジャムも始まりましたもんね!
おこめさんの作るゲーム楽しみにしてます。
次回作の講座ももうすぐ出来上がるのでまた読んでやってください(*’ω’*)