【Unity 入門】《第2回》敵を自動生成する(乱数・インスタンス)|シューティングゲームを作ろう!
Unity入門のゲーム作成チュートリアル第4弾の「城防衛シューティングゲーム」です!
襲来してくる敵を大砲で迎撃し、城を守りきることができれば勝利というゲームを作成していきます。
※そのほかのゲーム作成記事に関する情報は以下記事をご参考ください。
参考記事)Unity入門オリジナルチュートリアル集
第2回の今回は「敵を自動生成する」方法をご紹介します。
敵を生成するロジックを作成していきます。
概要
前回の「《第1回》大砲で弾を撃つ」では以下の処理を作成してきました。
- ゲームを作成するための準備
- キー操作で標準を変える
- 大砲の弾を発射する
第1回をまだ完成させていない!という方は「《第1回》大砲で弾を撃つ」を参考にして処理を完成させてください。
第2回の今回は以下の処理を作成していきます。
- 敵を自動生成する
以上を中心に進めます。
手順としては、
- 敵プレハブの作成
- 敵生成スクリプト作成
の順番で進めます。1の敵プレハブの作成は手短に完了させて敵生成プレハブの部分について詳しく解説していきます。
今回もできる限りわかりやすく解説していきますので、ぜひご参考ください。
※第1回で使用していた「Debug」オブジェクトは事前に削除しておきます。すでに削除済みの場合は無視してください。
敵プレハブを作成する
まずは敵プレハブを作成します。
今回は敵のオブジェクトにはこだわらず、球体(Sphere)を敵オブジェクトに採用します。そのため流れとしては球体を作成してプレハブ化するのみです。
今後Blenderを用いてアニメーション付きの敵を作成する方法をご紹介しますので乞うご期待ください!
それでは作成していきます。
オブジェクトを作成する
まずはじめにSphereを作成して、名前をEnemyとしてください。
作成したEnemyのScaleを全て「5」にしてください。それ以外のTransform値は任意です(後々スクリプトで決定するためどんな値でも問題ありません)。
タグ付けをする
次に作成したEnemyオブジェクトを敵として認識するためにタグ付けをします。
Enemyオブジェクトを選択したままインスペクターウィンドウのTagを選択して、新しいタグ「Enemy」を追加します。
List is Emptyの右下の+を選択、「Enemy」を入力してSaveを選択するとタグが追加されます。
以下GIFを参考にEnemyタグを作成してみてください。
タグを追加できたら、EnemyオブジェクトのタグをEnemyに変更します。
再度Tagを選択すると、先ほど作成したEnemyタグが追加されていますので、そちらを選択してください。
以下のように変更されていればタグ付けは完了です。
プレハブ化する
最後に作成したEnemyをプレハブ化します。
ここまで読み進めていただいた方には解説不要かと思いますが、もう一度だけ方法をご紹介します。
ヒエラルキーウィンドウのEnemyをプロジェクトウィンドウのPrefabsフォルダにドラッグ&ドロップすれば、作成したオブジェクトをプレハブ化することができます。
プレハブ化したオブジェクトは上記GIFのようにオブジェクト横の立方体が青く変化します。
これでプレハブ化は完了です。
ヒエラルキーウィンドウに残っているEnemyは最初から出現していると邪魔ですので、削除しておいてください。
敵生成スクリプトを作成する
次に敵を自動生成するスクリプトを作成します。
敵を作成するロジックは以下のような仕様にします。
- 一定時間経過後(乱数で時間決定)、敵を生成する
- 正面方向に生成する(位置はランダム)
基本ロジックを作成する
まずは特定の時間間隔で敵プレハブを生成する部分を記述します。
新しいスクリプト「EnemyGenerator」をScriptsフォルダに作成して、以下のように変更してください。
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 |
using System.Collections; using System.Collections.Generic; using UnityEngine; public class EnemyGenerator : MonoBehaviour { //敵プレハブ public GameObject enemyPrefab; //敵生成時間間隔 private float interval; //経過時間 private float time = 0f; // Start is called before the first frame update void Start() { //時間間隔を決定する interval = 5f; } // Update is called once per frame void Update() { //時間計測 time += Time.deltaTime; //経過時間が生成時間になったとき(生成時間より大きくなったとき) if(time > interval) { //enemyをインスタンス化する(生成する) GameObject enemy = Instantiate(enemyPrefab); //生成した敵の座標を決定する(現状X=0,Y=10,Z=20の位置に出力) enemy.transform.position = new Vector3(0,10,20); //経過時間を初期化して再度時間計測を始める time = 0f; } } } |
プログラムの解説をします。
7行目〜12行目を見てください。
1 2 3 4 5 6 |
//敵プレハブ public GameObject enemyPrefab; //敵生成時間間隔 private float interval; //経過時間 private float time = 0f; |
敵プレハブを格納するためのGameObject型の「enemyPrefab」を作成します。publicにすることでエディタからプレハブをアタッチすることができます。
次に敵を生成する時間間隔を決めるfloat型の「interval」を作成します。こちらは後ほど乱数を生成することでランダムに敵を生成するようなロジックに変更します。
最後に経過時間を計測するfloat型の「time」を作成します。
18行目では作成したintervalの時間を設定しています。ここでは固定値ですが、後ほど乱数生成する処理に変更しますので数値は任意です。
25行目を見てください。
1 2 |
//時間計測 time += Time.deltaTime; |
時間を計測するためのロジックです。
Update関数ではUpdate関数一周にかかった時間を「Time.deltaTime」に保管しておきます。このTime.deltaTimeを「+=」演算子でtimeに加算することで経過時間を計測することができます。
参考記事)Time.deltaTimeを使って制限時間を設定する
参考記事)代入演算子について
27行目〜36行目を見てください。
1 2 3 4 5 6 7 8 9 10 |
//経過時間が生成時間になったとき(生成時間より大きくなったとき) if(time > interval) { //enemyをインスタンス化する(生成する) GameObject enemy = Instantiate(enemyPrefab); //生成した敵の座標を決定する(現状X=0,Y=10,Z=20の位置に出力) enemy.transform.position = new Vector3(0,10,20); //経過時間を初期化して再度時間計測を始める time = 0f; } |
ここでは経過時間timeが敵生成時間間隔intervalを超えたときにif文の中を実行します。
まずはじめに敵プレハブを「Instantiate」メソッドを利用してインスタンス化(オブジェクト生成)して、GameObject型の「enemy」に格納しています。これで敵を生成することができます。
参考記事)Prefabを使ったオブジェクト生成
これだけでは敵がどこに生成されるかがわかりませんので敵の座標を決定します。ここでは固定値ですが、後ほどランダムに位置を変更するためのロジックを作成します。
最後に経過時間を初期化して再び敵プレハブが生成されるようにします。初期化を忘れるとUpdate毎に敵プレハブが生成されてしまうのでご注意ください。
これで敵が自動的に生成されるようになりました。
時間間隔をランダムにする
次に敵プレハブを生成するタイミングをランダムにする処理を書き加えます。
先ほど作成したEnemyGeneratorスクリプトを以下のように変更してください。(変更部分をハイライトしています)
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 |
using System.Collections; using System.Collections.Generic; using UnityEngine; public class EnemyGenerator : MonoBehaviour { //敵プレハブ public GameObject enemyPrefab; //時間間隔の最小値 public float minTime = 2f; //時間間隔の最大値 public float maxTime = 5f; //敵生成時間間隔 private float interval; //経過時間 private float time = 0f; // Start is called before the first frame update void Start() { //時間間隔を決定する interval = GetRandomTime(); } // Update is called once per frame void Update() { //時間計測 time += Time.deltaTime; //経過時間が生成時間になったとき(生成時間より大きくなったとき) if(time > interval) { //enemyをインスタンス化する(生成する) GameObject enemy = Instantiate(enemyPrefab); //生成した敵の座標を決定する(現状X=0,Y=10,Z=20の位置に出力) enemy.transform.position = new Vector3(0,10,20); //経過時間を初期化して再度時間計測を始める time = 0f; //次に発生する時間間隔を決定する interval = GetRandomTime(); } } //ランダムな時間を生成する関数 private float GetRandomTime() { return Random.Range(minTime, maxTime); } } |
9行目〜12行目を見てください。
1 2 3 4 |
//時間間隔の最小値 public float minTime = 2f; //時間間隔の最大値 public float maxTime = 5f; |
ここでは時間間隔の最小値と最大値を定義しています。
これによってランダムに生成される時間間隔の幅を設定することができます。
少し飛んで45行目〜49行目を見てください。
1 2 3 4 5 |
//ランダムな時間を生成する関数 private float GetRandomTime() { return Random.Range(minTime, maxTime); } |
float型のメソッド「GetRandomTime」はminTimeとmaxTimeを利用して乱数を返すメソッドです。
「Random.Range(minTime,maxTime)」で指定した範囲の乱数を生成します。
参考記事)Unityで乱数を生成する
参考記事)メソッド(関数)とは
上記で定義したメソッドを22行目と41行目で呼び出すことで敵プレハブを生成するタイミングをランダムにすることができます。
出現位置をランダムにする
次に出現位置をランダムにする処理を書き加えます。
先ほど作成したEnemyGeneratorスクリプトを以下のように変更してください。(変更部分をハイライトしています)
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 |
using System.Collections; using System.Collections.Generic; using UnityEngine; public class EnemyGenerator : MonoBehaviour { //敵プレハブ public GameObject enemyPrefab; //時間間隔の最小値 public float minTime = 2f; //時間間隔の最大値 public float maxTime = 5f; //X座標の最小値 public float xMinPosition = -10f; //X座標の最大値 public float xMaxPosition = 10f; //Y座標の最小値 public float yMinPosition = 0f; //Y座標の最大値 public float yMaxPosition = 10f; //Z座標の最小値 public float zMinPosition = 10f; //Z座標の最大値 public float zMaxPosition = 20f; //敵生成時間間隔 private float interval; //経過時間 private float time = 0f; // Start is called before the first frame update void Start() { //時間間隔を決定する interval = GetRandomTime(); } // Update is called once per frame void Update() { //時間計測 time += Time.deltaTime; //経過時間が生成時間になったとき(生成時間より大きくなったとき) if(time > interval) { //enemyをインスタンス化する(生成する) GameObject enemy = Instantiate(enemyPrefab); //生成した敵の位置をランダムに設定する enemy.transform.position = GetRandomPosition(); //経過時間を初期化して再度時間計測を始める time = 0f; //次に発生する時間間隔を決定する interval = GetRandomTime(); } } //ランダムな時間を生成する関数 private float GetRandomTime() { return Random.Range(minTime, maxTime); } //ランダムな位置を生成する関数 private Vector3 GetRandomPosition() { //それぞれの座標をランダムに生成する float x = Random.Range(xMinPosition, xMaxPosition); float y = Random.Range(yMinPosition, yMaxPosition); float z = Random.Range(zMinPosition, zMaxPosition); //Vector3型のPositionを返す return new Vector3(x,y,z); } } |
13行目〜24行目を見てください。
ランダムな座標を設定するために、先ほど時間の最小値と最大値を決定したようにX~Zについて最小値と最大値をそれぞれ設定します。
変数をpublicに定義することでUnityエディタから値を変更することができます。
少し飛んで63行目〜73行目を見てください。
1 2 3 4 5 6 7 8 9 10 11 |
//ランダムな位置を生成する関数 private Vector3 GetRandomPosition() { //それぞれの座標をランダムに生成する float x = Random.Range(xMinPosition, xMaxPosition); float y = Random.Range(yMinPosition, yMaxPosition); float z = Random.Range(zMinPosition, zMaxPosition); //Vector3型のPositionを返す return new Vector3(x,y,z); } |
Vector3型のメソッド「GetRandomPosition」は先ほど作成したX~Zの最小値・最大値を利用してランダムな座標を生成します。
ロジックはいたってシンプルで、X~Zまでそれぞれランダムな値を生成してPositionの型であるVector3型で返すことでランダムな座標を生成します。
このメソッドを49行目で呼び出すことでランダムな座標を敵プレハブに設定します。
それではここまでの処理を実行して確認してみましょう。
時間と位置がランダムになっていることが確認できました。
Unityエディタ側のフィールド整理(おまけ1)
おまけ1としてUnityエディタ側のフィールド(変数)整理方法をご紹介します。
どういうことかと言うと、現状EnemyGeneratorのインスペクターウィンドウは以下のようになっています。
エディタ側から変数の値を変更できるものの、非常に見づらく、使いづらい状態になっています。
例えばXとYを間違えてしまったり、最大値と最小値を逆にしてしまったり、想定以上に値を大きくしすぎてしまったりと、様々なエラーが想定されます。
以上を改善するために「Header」と「RangeAttribute」を使います。
EnemyGeneratorスクリプトのフィールド部分を以下のように変更してください。
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 |
using System.Collections; using System.Collections.Generic; using UnityEngine; public class EnemyGenerator : MonoBehaviour { [Header("Set Enemy Prefab")] //敵プレハブ public GameObject enemyPrefab; [Header("Set Interval Min and Max")] //時間間隔の最小値 [Range(1f,3f)] public float minTime = 2f; //時間間隔の最大値 [Range(5f,10f)] public float maxTime = 5f; [Header("Set X Position Min and Max")] //X座標の最小値 [Range(-10f,0f)] public float xMinPosition = -10f; //X座標の最大値 [Range(0f,10f)] public float xMaxPosition = 10f; [Header("Set Y Position Min and Max")] //Y座標の最小値 [Range(-10f,0f)] public float yMinPosition = 0f; //Y座標の最大値 [Range(0f,20f)] public float yMaxPosition = 10f; [Header("Set Z Position Min and Max")] //Z座標の最小値 [Range(10f,20f)] public float zMinPosition = 10f; //Z座標の最大値 [Range(20f, 30f)] public float zMaxPosition = 20f; //敵生成時間間隔 private float interval; //経過時間 private float time = 0f; // Start is called before the first frame update void Start() { //時間間隔を決定する interval = GetRandomTime(); } ・・・・・ } |
解説する前にこれによってエディタがどうなったか確認してみましょう。
それぞれのフィールドの簡易的な説明がつき、フィールドの値をスライダーで変更できるようになったため、見やすく使いやすいフィールドになりました。
簡単にプログラムを説明します。
「[Header(“任意の文字列”)]」でフィールドの前に説明を入れることができます。これによってそれぞれのフィールドがどのような役割を持っているのかわかりやすくなります。
「[Range(最小値,最大値)]」で最小値と最大値の幅のスライダーを設定することができます。これによって想定外の数値の受付を拒否し、エラーを減らすことができます。
ただひたすらゲームを開発するだけでなく、このような工夫して自分以外の人も修正しやすくすることが大切です。
変数のアクセス制限(おまけ2)
おまけ2として変数のアクセス制限についてご紹介します。
現状のプログラムではエディタ側からフィールドの値を設定するためにフィールドに「public」という修飾子をつけています。
これは簡単に言うと、フィールドにアクセスすることができる範囲を決定するものです。
つまり、publicなフィールドとはどんな場所からでもそのフィールドにアクセスすることができるということです。
一見すると便利なように見えますが、実はとても危険な状態なのです。どのような状態なのか倉庫を例に説明します。
鍵のない倉庫(publicなフィールド)
まずはpublicなフィールドを「鍵のない倉庫」とします。そのフィールドを持つクラス「倉庫の所有者」と外部のクラス「隣人」が今回の登場人物です。
まずは以下の図を見てください。
倉庫の所有者は所有する倉庫の中身は「VRゴーグル」が「3個」入った状態であると認識しています。(正確にはこのように思っているのはクラスではなくプログラマー側ですが…)
では鍵のない倉庫だとどのような状態が起きるのか見ていきましょう。
まず隣人Aが倉庫にしまってある「VRゴーグル」をイベントで使うために全て回収(フィールドをNullに変更)してしまいました。
次に隣人Bが使い終わった「ARグラス」を倉庫にしまってしまいました。
倉庫の所有者は何も知らずに倉庫からVRゴーグルを取り出して使おうとしますが、中身がARグラスに変わってしまいどこを探しても見つからなくなってしまいました。
このような状態が起こってしまってはとても困ります。
ではどのようにすればこのような事態を防ぐことができるのでしょうか。答えは簡単です。フィールドを「private」で宣言すれば問題解決です。
鍵のついた倉庫(privateなフィールド)
次に「鍵のついた倉庫」(privateなフィールド)を持つAの例を見てみましょう。(わかりづらいので鍵のない倉庫の所有者をCさんとします)
まずは以下の図を見てください。
鍵のついた倉庫の所有者であるAは先ほど隣人Cの倉庫から取り出した「VRゴーグル」を「3個」保有しています。
当然鍵を持っている(アクセス権を持っている)のは所有者であるAのみです。
そこに無くなってしまった「VRゴーグル」を探しに来た隣人Cがやってきました。
Aの倉庫は鍵がついている(private)だったため中身を取り出すことはもちろん、中身を確認することすらできませんでした。
こうして無事にAは「VRゴーグル」を取り出してイベントで使うことができました。
フィールド(変数)をpublicにしておく危険性をご理解いただけましたでしょうか。
このような問題から、プログラムでは原則フィールドをprivateにしておく必要があります。
参考記事)アクセス修飾子について
まとめ
いかがでしたでしょうか。
今回は敵を自動生成する方法をご紹介してきました。
自動生成とは言うものの、基礎的な知識を利用するだけで実現することができます。
また、おまけとしてフィールドの整理やアクセス修飾子についてもご紹介してきました。このようなちょっとした工夫がプログラミングの世界では非常に役に立ちますのでぜひご参考ください。
基礎的な知識をしっかりと身につけてUnityライフを充実させていきましょう!
この記事はいかがでしたか?
もし「参考になった」「面白かった」という場合は、応援シェアお願いします!