今回は、RPGゲーム等のワールドマップに自動で敵を生成する機能を作成したいと思います。
今回の機能は、ワールドマップシーン→戦闘シーン→ワールドマップシーンというRPGの流れを作ってみようと思ったんですが、全てを1つの記事にすると長くなってしまうので、
まずは第1弾として、ワールドマップシーンが読み込まれた時に敵を自動で配置する機能だけをやってみることにしました。
敵を配置するには、ワールドマップ上に敵が出現する場所をあらかじめ作っておき、そこから敵を生成する方法もあります。
こちらの方が特定の場所から敵を出現させる事が出来るので、不具合が発生しませんし、管理がしやすいかもしれません。
今回作成する機能は、ワールドマップシーンが読み込まれた時に毎回新しく敵をマップに配置する機能です。
その為、戦闘シーンへと遷移した後にワールドマップシーンが読み込まれたら前の状態は破棄されています。
前の状態を維持したい場合は、ScriptableObjectを使って敵の配置データを記憶してワールドマップシーン読み込み時にその場所に敵を配置したりする必要があります。
今回は毎回新規に配置するということで・・・・。
今回の機能は以前作成したUnityEditor上で、ゲームオブジェクトを自動配置する機能をゲーム中に使うよう改造します。
ワールドマップの作成
まずはワールドマップを作成していきます。
ワールドマップはTerrainを使って作成します。
Terrainの使い方は
を参照してください。
作成したワールドマップのTerrainの子要素にサイズを調整したCubeを配置します。
↑のようなヒエラルキーになります。
ワールドマップは、
↑のようになります。
Cubeは建物に見立てて設置しました。
Terrain自身のレイヤーにはField、子要素のCubeにはBlockというレイヤーを作成し、設定しておきます。
今回のTerrainは設定でTerrain Widthを50、Terrain Lengthを50に設定しました。
その為、Terrainの位置をXを-25、Zを-25にして、Terrainの真ん中がX、Y、Zで0の位置にくるようにします。
配置する敵をプレハブ化する
あらかじめ敵として配置するゲームオブジェクトをAssetsフォルダにドラッグ&ドロップしてプレハブ化しておきます。
↑のように敵をプレハブ化しました。
本来であればワールドマップで敵が動く為にCharacterControllerを取り付けたり、動くスクリプトを取り付けた物をプレハブ化しますが、
今回は設置するだけなので特別な機能は取り付けません。
敵のプレハブのレイヤーにはMonsterを作成し、設定しておきます。
敵を自動配置するスクリプトの作成
それでは敵を自動配置するスクリプトを作成します。
敵を自動生成するスクリプト全文
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 | using UnityEngine; using System.Collections; public class GenerateEnemy : MonoBehaviour { // 地面のゲームオブジェクト public Terrain terrain; // モンスターの最大数 public int maxNum; // モンスターのプレハブ public GameObject[] monsters; // 他のキャラとの距離 public float radius; public void InstantiateEnemy () { // 配置する敵の親のゲームオブジェクトを生成する GameObject parentObj = new GameObject("Enemys"); // 配置する最大数分繰り返し for (int i = 0; i < maxNum; i++) { // インスタンス化が成功したかどうか? bool check = false; RaycastHit hit; // ランダム値を入れる変数 float randX; float randZ; // 敵や主人公と位置が重なったらカウントする数字 int count = 0; // 敵の配置が出来たか、繰り返しが3回を越えたら終了 while(!check && count < 3) { // Terrainのサイズに合わせてランダム値を作成 randX = Random.Range (terrain.GetPosition ().x, terrain.GetPosition ().x + terrain.terrainData.size.x); randZ = Random.Range (terrain.GetPosition ().z, terrain.GetPosition ().z + terrain.terrainData.size.z); // Terrainと接触した位置を探す if(Physics.Raycast (new Vector3(randX, terrain.GetPosition ().y + terrain.terrainData.size.y, randZ), Vector3.down, out hit, terrain.GetPosition ().y + terrain.terrainData.size.y + 100f, LayerMask.GetMask ("Field"))) { // Player、Monster、Blockという名前のレイヤーと接触してなければ地面の接触ポイントに敵を配置 if (!Physics.SphereCast (new Vector3 (randX, terrain.GetPosition ().y + terrain.terrainData.size.y, randZ), radius, Vector3.down, out hit, terrain.GetPosition ().y + terrain.terrainData.size.y + 100f, LayerMask.GetMask ("Player", "Monster", "Block"))) { GameObject tempObj = Instantiate (monsters [Random.Range (0, monsters.Length)], hit.point, Quaternion.identity) as GameObject; tempObj.transform.SetParent (parentObj.transform); check = true; } else { count++; } } } } // どれだけの敵が配置されたか確認 Debug.Log (parentObj.transform.childCount); } } |
スクリプトの設定部分
terrainには先ほど作成したTerrainを設定します。
maxNumは敵を生成を試みる最大数を設定します。試みる数なのでこの数分だけ必ず敵が生成されるわけではありません。
monstersには配置する敵のプレハブを全て設定しておきます。
radiusは他の敵と接触を計算する時の半径を指定します。
地面にはField、家等の建物にはBlock、敵にはMonsterというレイヤーを設定しましたが、敵自身には主人公キャラクターとの接触を判定する為のコライダを設定する必要があるので、敵を生成した直後に主人公と接触判定をされてしまうと不具合が発生してしまうので、radiusにはその範囲よりも大きい値を指定します。
例えば、敵が主人公と接触したと判定するコライダをSphereで作成しその半径を1としていた場合は、このスクリプトのradiusには1より上の数値を指定します。
敵を生成するInstantiateEnemyメソッド
InstantiateEnemyメソッド内では、生成した敵の親のゲームオブジェクトを生成し、敵のゲームオブジェクトを全てこのゲームオブジェクトの子要素にします。
for文を使ってmaxNum回の敵の生成を試みます。
Terrainの位置とサイズからランダム値を計算し、Y座標はTerrainから上に100に設定し、その位置から下側に球のレイを飛ばします。
Fieldレイヤーと接触していたら、次の条件を判定します。
次の条件は同じ位置から球のレイを飛ばし、Player、Monster、Blockレイヤーが設定されているゲームオブジェクトにぶつかっていないかどうかを判定します。
ぶつかっていなければ地面の接触ポイントに敵をインスタンス化し配置しています。
2つめのif文では接触していない時を判定しているので,out hitという引数はいらないような気がしますが、ない場合のコンストラクタが定義されていなかったので、
記述してます。
判定条件としては接触していない時に実行し、hitの値は変わらないと思うので、地面の接触点が取得出来ると思います。
インスタンス化したゲームオブジェクトはUnityのスクリプトリファレンスにあるとおり、同時に親の設定も出来ます。
ですが、わたくしのUnity5.3.4f1だと定義されていないっぽいので、個別に親のゲームオブジェクトを設定しています。
出来る方はInstantiateの引数に指定した方がいいです。
今回の敵のプレハブの角度は全てQuaternion.identityと親の要素の角度と同じに設定していますが、例えばY軸の角度だけ変更して、別の方向を向かせる事も可能です。
Quaternion.identityの部分をQuaternion.Euler(0f, Random.Range(0f, 360f), 0f)
とすればY軸の角度だけをランダムにし設定する事が出来ます。
これで自動生成スクリプトが完成しました。
敵を自動生成してみる
スクリプトが出来たので何らかのゲームオブジェクトに取り付けておいて、GenerateEnemyスクリプトのInstantiateEnemyメソッドを実行してみてください。
確認するだけならInstantiateEnemyメソッドの部分をStartに書き換えて、スクリプトを設定したゲームオブジェクトが出現した時に自動実行させた方が楽かもしれません。
次回以降にScriptableObjectを使ったシーン間の移動の機能を作成しますが、そこに今回の機能を使う場合は少しやり方を変える必要があります。
GenerateEnemyのインスペクタは
↑のように設定しました。
実行すると、
↑のように敵が地面に配置されました。
真ん中辺りに主人公役のEthanもいるんです。(^^)/
建物であるCubeはBlockレイヤーを指定し、敵はMonster、主人公はPlayerを設定しているので、敵を生成する時はそこには配置されていません。
レイで地面との接触位置を調べているので、
↑のようにTerrainのデコボコに合わせて敵も配置されます。
敵を設置したくないエリアには透明なゲームオブジェクトを配置し、レイヤーをBlockに設定すればそのエリアには敵が配置されないようにも出来るので、敵を単純に配置したい時は便利かもしれません。
これで第1弾終了です。