本投稿は2024年9月時点の内容になります。アップデートにより変更となる場合があります。また環境によって違いがあると思いますのであくまで参考として、ご了承ください。
様々な書籍、ブログや動画を参考にさせていただきました。多すぎて一つ一つは紹介できませんが感謝です。
初心者の自分がUnity ソフトウエアでゲームを作ってみました。とりあえずシンプルなものということでモグラたたきに挑戦です。ゲーム作ってみるかという感じになったときに、いいタイミングで某ゲームのイベントシナリオ内ミニゲームにモグラたたきが実装されていたのでUIとかエフェクトとか、諸々の仕様をぱくって参考にして作ってみましたよ。様々なHowToの中の選択肢のひとつとして、同じ初心者さんの参考になればよいです。
\ チェック /
目次
前回につづきスポーンをやっていきます
前回も「13.プレハブPrefabとスポーン」の続きで、スポーンに関するいろいろなことを解説します。
本記事のポイント
- コルーチンを使ったスポーン
- Listを使ったスポーンの管理
コルーチンを使って一定のインターバルでスポーンする方法
コルーチンを使った簡単なインターバル
- IEnumerator の後にコルーチンを使いたいメソッドfooを書く
- メソッドfoo内の whileループの中に yield return new WaitForSeconds(bar)を記載 bar秒間処理を待つ
- StartCoroutine(foo());でコルーチンfooを開始する
public class EnemyManager : MonoBehaviour
{
[SerializeField]GameObject enemyPrefab;
[SerializeField] BattleManager battleManager;
//スポーンを実行、停止するフラグ
bool spawns;
//コルーチンの間隔を決める変数
[SerializeField] float spawnSpan = 3f;
public void StartRepeatSpawnEnemy()
{
//コルーチンの開始
spawns = true;
StartCoroutine(RepeatSpawnEnemy());
}
//コルーチンするスポーン関数
IEnumerator RepeatSpawnEnemy()
{
//動作確認用 確認後削除
int i = 0;
while (spawns)//Spawnsがtrueの間実行
{
GameObject enemyObj = Instantiate(enemyPrefab, new Vector3(0, 0, 0), Quaternion.identity);
Enemy enemy = enemyObj.GetComponent<Enemy>();
enemy.battleManager = battleManager;
//動作確認用5回繰り返したら止まる 確認後削除
i++;
Debug.Log(i+"回目");
if (i == 5)
{
StopRepeatSpawnEnemy();
}
//spawnSpan秒間隔で実行
yield return new WaitForSeconds(spawnSpan);
}
}
public void StopRepeatSpawnEnemy()
{
spawns = false;
}
// Start is called before the first frame update
void Start()
{
StartRepeatSpawnEnemy();
}
// Update is called once per frame
void Update()
{
}
private void OnEnable()
{
spawns=false;
}
}
スクリプトざっくり解説
- (7)スポーンの実行停止を決めるフラグ
- (9)スポーンのインターバルを決める変数
- (12-17)スポーンを開始するメソッド
- (20-39)コルーチンする内容
- (41-44)スポーンを止めるメソッド
3秒間隔で5回スポーンして止まればOKです。確認用に書いた部分は削除します。
Listを使ってスポーンを便利に管理しよう
9つの位置からランダムにスポーンさせる
モグラたたきなので、9つの位置からランダムにスポーンできるようにします。やりたいことを整理しながらロジックを固めていきます。
9つの位置からランダムにスポーンさせるには
- 決まった座標を9つのスポーン位置を決める
- 9つのスポーン位置からランダムな1個を決める、ただし二重のスポーンは禁止(敵が配置されているところは除く)
- その位置からスポーンさせる
スポーン位置の座標を配列、スポーンの可、不可をListで管理すると良さそうです。途中ですが一旦ここまでを実装してみます。
public class EnemyManager : MonoBehaviour
{
//省略
//スポーン座標
float[,] spawnPoint = new float[9, 2]
{
{-2.5f,-6f},{0,-6f},{2.5f,-6f},{-2.5f,-3.75f},{0,-3.75f},{2.5f,-3.75f},{-2.5f,-1.5f},{0,-1.5f},{2.5f,-1.5f}
};
//スポーン可能な穴のリスト
[SerializeField] List<int> availableHoleList = new List<int>{ 0, 1, 2, 3, 4, 5, 6, 7, 8 };
//省略
//コルーチンするスポーン関数
IEnumerator RepeatSpawnEnemy()
{
int spawnHoleIndex;
while (spawns)
{
//穴の抽選
spawnHoleIndex = DrawHoleIndex();
//抽選された穴を取り除く
RemoveFromHoleList(spawnHoleIndex);
//スポーン
SetEnemy(spawnHoleIndex);
yield return new WaitForSeconds(spawnSpan);
}
}
public void SetEnemy(int spawnHoleIndex)
{
//穴の番号に対応する座標を取得
float x = spawnPoint[spawnHoleIndex, 0];
float y = spawnPoint[spawnHoleIndex, 1];
//取得した座標にスポーン
GameObject enemyObj = Instantiate(enemyPrefab, new Vector3(x, y, 0), Quaternion.identity);
Enemy enemy = enemyObj.GetComponent<EnemyController>();
enemyController.SetManager(battleManager);
}
//スポーンする穴を抽選する
public int DrawHoleIndex()
{
int holeindex;
int rand;
//ランダムに抽選
if (availableHoleList?.Count > 0)
{
rand = Random.Range(0, availableHoleList.Count);
}
else
{
Debug.Log("スポーン可能な穴がありません");
return -1;
}
holeindex = availableHoleList[rand];
//戻り値抽選された番号
return holeindex;
}
//穴番号をavailableHoleListから取り除くメソッド
public void RemoveFromHoleList(int holeIndex)
{
availableHoleList.Remove(holeIndex);
}
//省略
}
スクリプトざっくり解説
- (6-8)スポーンする穴の座標(上記ロジック1)
- (12)スポーン可能な穴のListリスト
- (19)スポーンする穴の番号
- (45-63)スポーンする穴を抽選するメソッド ランダムに抽選し
- (65-68)指定した穴の番号をListリストから取り除く
- (33-42)番号の穴に敵をセット(スポーンするメソッド)
- (17-31)反復スポーン処理
- (24,26)ロジック2
- (28)ロジック3
ゲーム再生で確認します。
いい感じでスポーンされていますが、途中で終わってしまいます。コンソールに「スポーン可能な穴がありません」と出ています。「EnemyManager」のインスペクターを確認すると「availableHoleList」が空になっているのがわかります。
当たり前ですよね。抽選しっぱなしで破棄された敵の穴の番号を「availableHoleList」に戻してないせいです。
「availableHoleList」に穴の番号を戻す
「availableHoleList」に穴の番号を戻すため、何がしたいか整理していきます。
「availableHoleList」に穴の番号を戻す
- 破棄された敵のいた穴の番号を取得したい→敵に穴の番号に対応する背番号をつけてみたらどうか?
- 破棄された敵をスムーズに取得するにはどうするか?→探す?
- 破棄された敵をスムーズに取得するにはどうするか?→コライダーを使って検知する?
- 破棄された敵をスムーズに取得するにはどうするか?→「Enemy」や「BattleManager」から送ってもらう?
- 敵が破棄されたときにその背番号を取得する方法は?
- 取得した番号をリストに戻す
破棄された敵を取得するのにBattleManagerから送ってもらいます(コライダーでの検知も試してみましたが、こちらの方がスムーズでコードがわかりやすいので)。それを踏まえると
実行したいロジック
- 敵に穴の番号に対応する背番号をつける
- バトル終了後に「BattleManager」から敵を受け取る
- 受け取った敵から番号を取得する
- 「availableHoleList」に番号を戻す
- 受け取った敵を破棄する
//省略
//Regexを使うためのnamespace
using System.Text.RegularExpressions;
public class EnemyManager : MonoBehaviour
{
//省略
public void SetEnemy(int spawnHoleIndex)
{
//穴の番号に対応する座標を取得
float x = spawnPoint[spawnHoleIndex, 0];
float y = spawnPoint[spawnHoleIndex, 1];
//取得した座標にスポーン
GameObject enemyObj = Instantiate(enemyPrefab, new Vector3(x, y, 0), Quaternion.identity);
Enemy enemy = enemyObj.GetComponent<Enemy>();
//敵の名前に背番号として穴の番号をつける
enemy.enemyStatus.Name = enemy.enemyStatus.Name +spawnHoleIndex;
//そのオブジェクトにも同じ名前をつける
enemyObj.name = enemy.enemyStatus.Name;
enemyController.SetManager(battleManager);
}
//敵を破棄するメソッド 「BattleManager」から実行
public void RemoveEnemy(Enemy enemy)
{
//enemyの名前から数字以外を取り除いて整数型に変換する 正規表現というのを使ってます
int holeindex = int.Parse(Regex.Replace(enemy.enemyStatus.Name, @"[^0-9]", ""));
//番号を穴のリストに戻す
AddToHoleList(holeindex);
//敵のゲームオブジェクトを破棄する
Destroy(enemy.gameObject);
}
//省略
//リストに加えるメソッド
public void AddToHoleList(int holeIndex)
{
availableHoleList.Add(holeIndex);
}
//省略
}
スクリプトざっくり解説
- (3)Regex.Replaceを使うためのnamespace
- (19)敵の名前に背番号をつける(ロジック1)
- (21)一応ゲームオブジェクトの方にも同じ名前を付けてます(ロジック2)
- (27-35)敵オブジェクトを破棄するメソッド 「BattleManager」から実行
- (27)破棄対象の「enemy」を「BattleManager」から引数として受取ります
- (30)「enemy」の名前から穴の番号を取得(ロジック3)
- (32)取得した番号を「availableHoleList」に戻す(ロジック4)
- (34)敵のゲームオブジェクトを破棄 「enemy」ではなく「enemy」を含むゲームオブジェクトを破棄します(ロジック5)
public void EndBattles(Enemy enemy)
{
enemyManager.RemoveEnemy(enemy);
}
スクリプトざっくり解説
- 「BattleManager」の「EndBattles」メソッド変更 「EnemyManager」の「RemoveEnemy」を実行
ゲーム再生してみます。穴の番号がリストに戻っているのが確認できます。
Listリストを使って数種類の敵をランダムにスポーンしたい
Listを使って出現する敵の種類を増やしていきます。
実行したいロジック
- 数種類の敵をリストで管理
- リストからランダムに敵を取得
- 取得した敵をスポーンする
public class EnemyManager : MonoBehaviour
{
GameObject enemyPrefab;
//敵キャラのプレハブを管理するリスト
[SerializeField] List<GameObject> enemyPrefabList = new List<GameObject>();
//省略
//コルーチンするスポーン関数
IEnumerator RepeatSpawnEnemy()
{
int spawnHoleIndex;
while (Spawns)
{
//敵プレハブの選択
RandomEnemySelect();
//穴の抽選
spawnHoleIndex = DrawHoleIndex();
//抽選された穴を取り除く
RemoveFromHoleList(spawnHoleIndex);
//スポーン
SetEnemy(spawnHoleIndex);
yield return new WaitForSeconds(spawnSpan);
}
}
//省略
//敵プレハブの中からランダムに選ぶメソッド
public void RandomEnemySelect()
{
if (enemyPrefabList?.Count > 0)
{
enemyPrefab = enemyPrefabList[Random.Range(0, enemyPrefabList.Count)];
}
else
{
Debug.Log("EnemyPrefabListがありません");
}
}
//省略
}
スクリプトざっくり解説
- (5)敵プレハブの管理用リスト
- (16,31-41)敵プレハブリストからランダムに選択する
- インスペクターで「EnemyPrefabList」にプレハブをアサインする。(今はUnitychanしかないのでUnitychanをアサインします)
- ついでにavailableHoleListに付けていた[SerializeField]は外しておきましょう(Unity エディターが表示エラー吐きまくるので)
ゲーム再生してみましょう。スポーンが滞りなく実行出来てればOKです。
おまけ 敵のオブジェクトを親オブジェクトにまとめる
ヒエラルキーの整理と、敵オブジェクトを探す作業が発生した場合に便利なようにインスタンス化された敵を親オブジェクトにまとめておきます。
敵オブジェクトを親オブジェクトの子にインスタンス化する
- 以下「EnemyManager」のコードの変更点です
public class EnemyManager : MonoBehaviour
{
//省略
public void SetEnemy(int spawnHoleIndex)
{
//穴の番号に対応する座標を取得
float x = spawnPoint[spawnHoleIndex, 0];
float y = spawnPoint[spawnHoleIndex, 1];
//取得した座標にスポーン
//第4引数に親のTransformを入れる
GameObject enemyObj = Instantiate(enemyPrefab, new Vector3(x, y, 0), Quaternion.identity,this.gameObject.transform);
Enemy enemy = enemyObj.GetComponent<Enemy>();
//敵の名前に背番号として穴の番号をつける
enemy.EnemyStatus.Name = enemy.EnemyStatus.Name +spawnHoleIndex;
//そのオブジェクトにも同じ名前をつける
enemyObj.name = enemy.EnemyStatus.Name;
enemy.BattleManager = battleManager;
}
//省略
}
スクリプトざっくり解説
- (19)Instantiateの第4引数に親にしたいゲームオブジェクトに「EnemyManager」自身のTransform 「this.Transform」を入れます。ポイントはオブジェクトそのものではなく位置を表すTransformを入れることです。
ゲーム再生すると「EnemyManager」の子にUnitychanがスポーンされていると思います。
まとめ
今回は中身のわりに、かなり長かったですね。
マケイヌも初心者なので至らないところが多くて、これが正解ではないですが、少しずつロジックをコーディングで形にしていく様子が多少なりとも伝わったらうれしいですね。
まとめ
- 一定周期でスポーンしたいときはコルーチンを使うといい
- Listはうまく使うとスポーンが便利になるときがある
- あるゲームオブジェクトの子としてインスタンス化したいときは、Instantiateの第4引数に親にしたいゲームオブジェクトのTransformを入れる
\ 初学に使った書籍です /
動画×解説でかんたん理解! Unityゲームプログラミング超入門
大角 茂之/大角 美緒
\ Unityのスクリプトを書くのに役立ちます /
もっと早く教えてほしかった!Unity C#入門
MARU
おすすめ記事
Unity ソフトウエアでゲーム制作#1モグラたたき編(28.ゲームに効果音…
Unity ソフトウエアでゲーム制作#1モグラたたき編(27.エフェクト2パ…
Unity ソフトウエアでゲーム制作#1モグラたたき編(26.エフェクトその1)
Unity ソフトウエアでゲーム制作#1モグラたたき編(25.プレハブバリア…
Unity ソフトウエアでゲーム制作#1モグラたたき編(24.Buttonで…
Unity ソフトウエアでゲーム制作#1モグラたたき編(23.シンプルなアニ…