【ゲームの完成】ゲームループ処理の作成とレベルデザイン

Unity タワーディフェンスゲームの作り方


Unityの本格ゲーム制作講座はこちら
【30日間の全額返金保証付き】

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

前回の記事↓

武器や攻撃範囲の強化・武器の売買ができるお店システムの作り方
前回(第8回)では、プレイヤーによる弓の配置と、UI画面を作り始めました。 前回の記事↓ 今回はさらに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スクリプトを作成し、下記のように編集します。

メンバ変数は

  • どのEnemyManagerの情報を表示するか(Inspectorで設定)
  • EnemyManagerのWave情報を表示するためのText(Inspectorで設定)
  • EnemyManagerのEnemy数情報を表示するためのText(Inspectorで設定)

の3つです。 Update関数の中では UIのTextにWave情報と敵の数情報をセットしています。

ただ、この

enemyManager.EnemyCnt はまだEnemyManagerスクリプトに無いので EnemyCnt プロパティを追加します。

まだ現在のwaveで出現していない敵の数は waves[wave].patterns.Count になります。

そして、既に出現している敵(Enemy)は FindObjectsOfType<Enemy>().Length で取得し、それを足す事で敵残数を取得できるプロパティにしています(=>を使ったgetプロパティの省略記法です)。

注意点としては waves[wave].patternsListなのでCountで要素数を取得し、FindObjectsOfType<Enemy>()配列なのでLengthで要素数を取得しています。

2スクリプトを修正しましたので、忘れずに2つとも保存をしましょう。
UnityEditorのInspectorでEnemyManagerUIのEnemyManager、Text(WAVE)、Text(GOLD)をセットします。

それではプレイボタンを押して、確認してみましょう。

ENEMYを倒すたびに数字が減っているのが確認できます

GOLDの獲得とダメージ処理

大分ゲームが完成に近づいてきましたが、まだ

  • 敵を倒してもGOLDが増えない
  • 敵がゴール(本拠地)に到着してもHPが減らない

ので、そこもやってしまいましょう。

敵を倒したらGOLDを増やす

矢(Arrow)スクリプトで敵(Enemy)を倒した時に処理を追加します。

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)スクリプトを修正します。

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スクリプトを修正していきましょう。

ゲームループコルーチンの作成

まず、ゲームループの大外枠を作っていきます。

Start関数の中で StartCoroutine(GameLoop()); でGameLoop()を対象にコルーチンを開始しています。

では。GameLoop()関数はというと

内部では while(true) が使われています。これは終了条件が無いので無限ループですが、yield return null;で中断と再開をしています。タワーディフェンス講座第7回 でやった、弓(Arrow)が一定間隔で弾を撃つ処理と同じ仕組みです。 このwhile(true){} の中にゲームループを作っていきます。

ゲームステートの準備

次に、ゲームの状態(STATE:ステート)とその遷移(動き)を準備していきます。 再度この画像を持ってきました。 実はこの画像の遷移図は実際に今回のタワーディフェンスのゲームステートの流れを示したものです。 この、TITLEやGAME PLAYが状態(state)です。これらを全てenumで宣言してしまいます。

このGAME_STATE enumをメンバ変数で宣言します。

上記状態遷移の画像を見ると分かりますが、最初は TITLE から始まっています。 なので、Start関数の中でstateはTITLEにしておきましょう。

そして、この状態によってゲームの動きを変えていくので、GameLoop関数の中でstateによって処理が分岐できるようswitch文を入れます。

ここまでで、GameMainスクリプト全体は以下のようになります。

※今回はスクリプトが今まで以上に長くなってきますので、時々スクリプトの保存をして、エラーが無いことを確認しましょう。

ステートによる画面表示

先ほど用意したステート表示Textに、ステート毎にメッセージを表示するように追加をします。

要点を説明していきます。まず

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で表示メッセージを変更しています。

保存をして、UnityEditorでInspectorでState TextText(State)をセットします。

では、再生をして確認してみましょう。 再生中にInscpetorからStateを変更してみると、表示されるTextが変更するようになりました。

状態(State)の遷移

では、状態(State)を遷移条件に応じて遷移させていきます。

なんと3度目の登場、状態(State)の遷移図です。 遷移条件は矢印の近くに書いてありますね。

  • 敵を全て倒した
  • 次wave無し
  • HP0
  • キーが押された x 4

この中の「敵を全て倒した」・「次wave無し」はEnemyManagerが無いと分からないので、EnemyManagerオブジェクトをInspectorから指定できるようにメンバ変数を作ります。

次に 「HP0」 は PlayerのHPを表すので、PlayerオブジェクトをInspectorから指定できるようにメンバ変数を作ります。

そして「キーが押された」は、「何かキーが押されたら再開」という意味になるので、WaitUntil というクラスを使います。

yield return new WaitUntil( () => Input.anyKey );

このようにラムダ式(正確にはdelegate)という記述を使い、条件(今回はInput.anyKey→何かキーが押されるまで)を指定することでキー入力待ちが実現できます。(第7回講座でちょっと紹介していましたね)

ただ、今回はこの「キーが押された」が色んな状態遷移の条件になっているため、先にこのWaitUntilクラスを生成するプロパティを用意しておきます。

これで、コルーチンの中であれば yield return WaitAnyKey; とすることで、「中断し、キー入力で再開」という処理になります。

以下がスクリプトの追加位置の一例になります(関数内とかでなければどこでも構いませんが)

では、順にGameLoop関数のswitch(state)内のステートの遷移を組み立てていきます。 GAME_STATE.TITLE からは、「キーが押されたら」「WAVE_CHANGE」へ遷移なのでこうなります。

GAME_STATE.GAME_PLAY からは、「HPが0になったら」「GAME_OVER」へ遷移と「敵を全て倒したら」「WAVE_CLEAR」への遷移です。

GAME_STATE.WAVE_CHANGE からは、「キーが押されたら」「GAME_PLAY」への遷移です。

加えて、今から始まるのが第何Waveなのかを表示したいので、stateTextの表示も修正しておきます。

enemyManager.wave+1 をしているのは、配列は0から始まりますが、プレイヤーから見るとwaveは1から始まる方が自然なので補正している処理です。

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が増える」という事になり、レベルデザインの幅が広がります。

GAME_STATE.GAME_CLEARGAME_STATE.GAME_OVER からは、「キーが押されたら」「TITLE」へ遷移なのですが、単純にTITLEへ遷移してもEnemyManagerからは既に敵が出現しているでしょうし、Playerのhpも減っていたりします。

これらを全てリセットするように一生懸命スクリプトを書いても良いですが、もっと手っ取り早い方法があります。

それは、今のシーンをロードしなおしてしまう(SceneManager.LoadScene(0))ことです(ただし、SceneManagaerクラスを使う場合は、
using UnityEngine.SceneManagement;
をスクリプトの最初の方で呼び出す必要がありますので注意してください)。

これで一通りのステートの遷移を追加しました。

スクリプトを保存してUnityEditorでGameMainオブジェクトのInspectorで今回追加した EnemyManagerPlayerをそれぞれ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関数)を呼んであげるようにします。

保存をし、UnityEditorでHierarchyのEnemyManagerオブジェクトを選択し、Inspectorで、EnemyManagerコンポーネントのチェックを外します。

これで、EnemyManagerのUpdateは呼ばれなくなるため、GAME_PLAY状態の時だけ敵が生成されるようになりました。 (EnemyManagerスクリプトの Update関数を消すように修正しても構いません。)

では、確認してみましょう。 以下がGameMainスクリプトの最終状態になります。

ゲームの根幹かつ最後のスクリプトだけあって、今まで一番長くなりましたね。

スクリプトの修正は以上になります。お疲れ様でございました。

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も作ってみました)

https://unityroom.com/games/rakugakitowerdefense

参考になれば幸いです。

おさらい

これにて、ゲームは完成し10回に渡ってお付き合いしていただいたタワーディフェンス制作講座も(ひとまず)終わりになります。

ここで終わりにさせず、別シーンでタイトル画面を作ったり、ステージセレクトを追加したり、効果音や演出を適切に入れる事でよりゲームとして完成されていくと思います。

このタワーディフェンス作成講座をベースに素敵なゲームが完成した際には是非一報頂けると嬉しいです。 ここまで読んでいただき誠にありがとうございます。

タワーディフェンスゲームの作り方講座に戻る↓

【unityで防衛ゲーム】タワーディフェンスゲームの作り方
今回のunityゲーム開発講座では2DUnityを用いたタワーディフェンスゲームシステムの制作を行っていきます! 講座は全部で10回に分かれており、初めてunityを使ってゲームを作る人でもサクサク進められる講座になっています。 講座の中でunityエディターの使い方やUnity C#の活用法も学べるのでこれからunityでゲーム開発していきたい方はぜひ講座を見ながら実際にプログラムを書いていってください。 自分の好きなゲームステージを作成し、オリジナルのタワーディフェンスゲームを開発していきましょう!

コメント

  1. サウスケイ より:

    一応完成しました!
    内容もわかりやすくまとめていてとても理解しやすかったです!
    これからUnityでゲームを作っていく上で参考にしていきたいと思います!
    ありがとうございました!

    • ばこ より:

      おお!完成おめでとうございます!
      完成報告いただけてとてもうれしいです。
      これからも新しい講座どんどん作っていくのでまたサイト見に来てもらえたらうれしいです。
      こちらこそ最後まで読んでいただきありがとうございました!^^

  2. おこめ より:

    ここ5日くらい、このブログを人生で初めてゲーム作りを完遂させました! 今までも何度か挑戦しようとしては挫折してを繰り返していたので、完成出来てめちゃくちゃ嬉しかったです!
    せっかくなのでこのままUnityゲームジャムに何かしらのゲームを投稿してみようかと思います!

    すばらしいコンテンツをありがとーございました!

    • ばこ より:

      おおー!初のゲーム完成おめでとうございます!^^
      この講座作った甲斐がありました。コメントもいただけてとてもうれしいです。
      ちょうどunityゲームジャムも始まりましたもんね!
      おこめさんの作るゲーム楽しみにしてます。
      次回作の講座ももうすぐ出来上がるのでまた読んでやってください(*’ω’*)

  3. bananan より:

    講座ありがとうございました!

    3週間くらいかけてじっくり読み込みながら作りました
    プログラムの書きかたがとても分かりやすく
    挫折せずに開発まで作りあげるが出来ました

    せっかく作ったので、忘れないうちにオリジナルのタワーディフェンス製作を
    やってみたいと思います!!
    目指せ! Kingdom Rush 超え!!(大きく出たな

    この講座がなければやってみようと思いませんでした
    本当に分かりやすい講座でした
    ありがとうございます!!

    • ばこ より:

      うれしいコメントありがとうございます!^^

      最後まで挫折せずに作れたとのことで講座制作者冥利に尽きます!
      プログラムの書き方なども今後もさらにブラッシュアップさせながら講座制作続けていきます。

      ぜひオリジナルゲーム完成まで進めていただければと思います。
      ゲーム完成したらまたコメント欄などでいつでも教えてくださいね!

      こちらこそ最後まで読んでいただき、実際に手を動かして作っていただきありがとうございました!
      いただいた声を励みにこれからもいろんなゲームプログラミング講座作っていきます!

  4. TETSU より:

    最後まで行くことは出来ましたが、敵が弓矢の射程に入ったら弓矢が飛んでいく矢ごと消えてしまいます。VECTOR3 position関連なのでで三次元座標の値がおかしいのだと思いますが講座通りにプログラムを打ち込んでるのに治らないので、解決方法を教えていただけると幸いです。よろしくお願いします

  5. Unity入門の森 より:

    既に完成させている方も出てきてますし、ちゃんと作ればそうはならないはずですね。

    TETSUさんのコードがどこか間違えてるんでしょうね。
    デバッグもゲームプログラミングにつきものですからね。
    コードやUnityのオブジェクトへのアタッチの過程にミスがないか確認してやってみてください。
    ファイトです!

タイトルとURLをコピーしました