今回はエディターウィザードを使ってTerrainの地面に合わせて指定した数のゲームオブジェクトを配置するツールを作成します。
今回のツールを作成したのはTerrainの木を配置する機能が思うように動作しなかった為です。
エディターウィザードに関しては
の記事を参考にしてください。
ゲームオブジェクト配置ツール
Terrainでは内部データとして木のデータを保持していて、本来はそこにアクセスして木をうまく配置出来るようにしてみようと思いましたが、
うまく動作しなかったので諦めました。(^_^;)
木をランダムに回転して配置する場合はLOD Groupを取り付ける必要があり、取り付けるとランダムに回転はするのですが、Wind Zoneの風で木と葉が揺れなくなってしまいます。
LOD Groupを取り付けない場合はWind Zoneによる風の影響は受けますが木がランダムな回転をせず全ての木が同じ角度になってしまいます。
そこでTerrain内部のデータとして木は作成出来ませんが、シーン上に木のプレハブをTerrainの地形に沿って指定数設置出来る機能を作成していきます。
単純に木のゲームオブジェクトだけでなく建物のゲームオブジェクトや石のゲームオブジェクト等も配置出来ます。
地形に合わせてゲームオブジェクトを回転させるだけでなく、ゲームオブジェクトをY軸でランダムに回転させ全て同じ方向を向かないようにします。
ツールスクリプトを作成していきます。
名前はObjectPlacementToolという名前にしました。
Toolというnamespaceの中にObjectPlacementToolクラスを作成します。
このクラスに処理を追加していきます。
ObjectPlacementToolクラスはEditorフォルダ内に移動させます。
設定用のフィールドの作成とエディターウィザードの表示
まずは設定用のフィールドを宣言するところまで記述します。
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 | using UnityEngine; using System.Collections; using UnityEditor; namespace Tool { public class ObjectPlacementTool : ScriptableWizard { // テレイン [SerializeField] private Terrain terrain; // 親にするゲームオブジェクトの名前 [SerializeField] private string parentName; // 設置するゲームオブジェクト [SerializeField] private GameObject placementObj; // 重なりチェックを厳しくするか簡易にするか [SerializeField] private bool doubleCheckMode; // シードを変更したランダムを生成するか? [SerializeField] private bool randomMode; // 地面のTerrainに設定したレイヤー名 [SerializeField] private LayerMask fieldLayer; // 設置するゲームオブジェクトに設定したレイヤー名 [SerializeField] private LayerMask placementObjLayer; // デフォルトのFieldLayerの名前 private LayerMask defaultFieldLayer; // デフォルトのPlacementObjLayerの名前 private LayerMask defaultPlacementObjLayer; // 設置する数 [SerializeField] private int count = 1; // 地形の斜めに合わせるかどうか [SerializeField] private bool slope; // ベースが下にない時用オフセット値上に移動させる [SerializeField] private float offset = 0.0f; // 木のスケールをランダムに変更するかどうか [SerializeField] private bool useScale; // 木の最小スケール [SerializeField] private float minScale = 0.1f; // 木の最大スケール [SerializeField] private float maxScale = 1.0f; private GameObject ins; void Awake() { if (EditorPrefs.HasKey("ObjectPlacementToolData")) { Debug.Log("Awake"); LoadData(); } else { fieldLayer = LayerMask.GetMask("Field"); placementObjLayer = LayerMask.GetMask("Obj"); // デフォルトのレイヤーを設定 defaultFieldLayer = fieldLayer; defaultPlacementObjLayer = placementObjLayer; // ヒエラルキー上からTerrainを取得 terrain = GameObject.FindObjectOfType<Terrain>(); } } [MenuItem("ObjectPlacementTool/ObjectPlacementTool")] static void CreateWizard() { // ウィザードを表示 ScriptableWizard.DisplayWizard<ObjectPlacementTool>("自動ゲームオブジェクト設置ツール", "ゲームオブジェクトを設置", "データの初期化"); } |
Toolというnamespaceの中にObjectPlacementToolクラスをScriptableWizardクラスを継承して作成します。
フィールドの説明はコメントで記述しているのでわかると思いますが、いくつか説明しておきます。
doubleCheckModeは設置するゲームオブジェクトと同じゲームオブジェクトが地面に設置されている時のチェックをきびしくするか簡易にするかの設定です。
チェックすると設置するゲームオブジェクトをインスタンス化した後、周りに同じゲームオブジェクトがないかチェックしあればインスタンス化したゲームオブジェクトを削除します。
その為この項目をチェックすると指定した数よりも設置されるゲームオブジェクトの数が減ります。
チェックを入れない時はランダムに作成した位置からレイを飛ばし簡易的にチェックし同じゲームオブジェクトがなければ設置します。
こちらの場合はインスタンス化する前にチェックし、スケール調整とゲームオブジェクトのサイズを考慮していない為ゲームオブジェクト同士が重なり合います。
randomModeはランダム値を生成する時にシードを変更するかしないかの設定です。
fieldLayerはゲームオブジェクトを設置する時に地面の設置点を取得する必要があるのでその時の設置点であるTerrainのゲームオブジェクトを取得出来なければいけません。
そこでTerrainゲームオブジェクトには何らかのレイヤーを設定しておき、そのレイヤーを使って設置点を取得します。
Terrainのレイヤー、fieldLayerに同じものが設定されていないとゲームオブジェクトを配置する事が出来ません。
placementObjLayerは設置するゲームオブジェクトに設定するレイヤーで複数の同じゲームオブジェクトを配置する時に重なって設置されない為に指定します。
slopeは地面の角度と設置するゲームオブジェクトの角度を合わせるかどうかの設定です。
傾けたくない場合はチェックしないようにします。
offsetはゲームオブジェクトと地面との距離を調整する為に設定する値です。
人型キャラクター等は足元が基点になっているので0で大丈夫ですが、Cube等を使った場合真ん中が基点となっています。
その為Cube等の足元が基点となっていない場合はoffset値を調整します。
Awakeメソッドではエディターウィザードを開いた時の初期値を設定しています。
EditorPrefsでObjectPlacementToolDataという名前のキーが存在していればLoadDataメソッドを呼んでデータを取得し初期値を設定します。
データが存在していなければデフォルト値を設定します。
CreateWizardメソッドはAttributeを取りつけUnityのメニュー項目にメニューアイテムを表示し選択した時に実行されます。
ジェネリクスの部分には自身のクラスを指定します。
これでツールで設定出来る項目の作成とエディターウィザードの表示部分が出来上がりました。
データの初期化ボタンを押した時の処理
ScriptableWizard.DisplayWizardで指定した第3引数の文字列は『他のボタン』に表示される文字列になります。
この『他のボタン』を押した時に実行されるメソッドがOnWizardOtherButtonメソッドになります。
ボタンの名前を『データの初期化』としたのでOnWizardOtherButtonメソッドではフィールド値を初期化する処理を記述します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | // ウィザードの他のボタンを押した時に実行 void OnWizardOtherButton() { if(EditorPrefs.HasKey("ObjectPlacementToolData")) { EditorPrefs.DeleteKey("ObjectPlacementToolData"); } terrain = GameObject.FindObjectOfType<Terrain>(); placementObj = null; doubleCheckMode = false; randomMode = false; count = 1; slope = false; offset = 0.0f; useScale = false; minScale = 0.1f; maxScale = 1.0f; parentName = ""; fieldLayer = defaultFieldLayer; placementObjLayer = defaultPlacementObjLayer; } |
データの初期化をするのでEditorPrefsでObjectPlacementToolDataが存在していればそのデータを削除します。
EditorPrefsではEditorPrefs.DeleteAllというメソッドがありますが、これを使用すると自身で作成したEditorPrefsのデータのみならず、デフォルトのUnityの初期設定データすらも削除してしまうので使わないことをお勧めします。
わたくしは実際にDeleteAllメソッドを使ってしまってUnityが正常に動作しなくなって焦りました・・・・(^_^;)
その他はフィールド値を初期化しているだけなので問題はないと思います。
エディターウィザードで更新があった時の処理
エディターウィザードの設定項目を変更したりするとOnWizardUpdateメソッドが呼ばれます。
ここでは不正な設定値が設定された時にパラメータを制限内に設定し直す処理を記述しておきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | // ウィザードで更新があった時に実行 void OnWizardUpdate () { // 設置する木の制限を加える if (count > 10000) { count = 10000; } else if (count <= 0) { count = 1; } // スケールが0以下に設定されていたら0.1fにする if (minScale <= 0) { minScale = 0.1f; } } |
countは一度に設置するゲームオブジェクトの数で10000を越えたら10000に設定しなおします。
また0以下の場合は実行する意味がないので1に設定しています。
minScaleは設置するゲームオブジェクトのスケールの最小値を指定しますが、0以下の値が設定されるとおかしいので最小値は0.1に設定しています。
ゲームオブジェクトを設置ボタンを押した時の処理
『作成ボタン』である『ゲームオブジェクトを設置』ボタンを押すとOnWizardCreateメソッドが呼ばれます。
その為『ゲームオブジェクトを設置』ボタンを押したら実際にゲームオブジェクトを配置する処理を記述します。
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 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 | // ウィザードの作成ボタンを押した時に実行 void OnWizardCreate() { // テレインがセットされていなければエラー if (terrain == null) { Debug.LogError("not set terrain", this); return; // オブジェクトがセットされていなければエラー } else if (placementObj == null) { Debug.LogError("not set placementObj", this); return; } else if (count <= 0) { Debug.LogError("A numerical value less than or equal to 0 is set"); return; } else if (fieldLayer == 0) { Debug.LogError("not set fieldLayer"); return; } else if(placementObjLayer == 0) { Debug.LogError("not set placementObjLayer"); return; } // テレインデータを確保 Vector3 terrainPos = terrain.GetPosition(); TerrainData terrainData = terrain.terrainData; // 親の作成 GameObject parentObj = new GameObject(); if (parentName == "") { parentName = "parentObj"; } parentObj.name = parentName; // 親要素のUndo登録をして親を消す事で子も全部消えるようにしておく Undo.RegisterCreatedObjectUndo(parentObj, "Create " + parentObj.name); // シードを指定してランダムクラスを作成 System.Random rand = new System.Random(Time.time.ToString().GetHashCode()); for (var i = 0; i < count; i++) { // X軸の位置 float x; // Z軸の位置 float z; // 角度 Quaternion rot; // Terrainの大きさからランダム値を計算 if (randomMode) { x = (float)(terrainPos.x + rand.NextDouble() * terrainData.size.x); z = (float)(terrainPos.z + rand.NextDouble() * terrainData.size.z); // Y軸をランダムに回転 rot = Quaternion.Euler(0, (float)(rand.NextDouble() * 360), 0); } else { x = Random.Range(terrainPos.x, terrainPos.x + terrainData.size.x); z = Random.Range(terrainPos.z, terrainPos.z + terrainData.size.z); // Y軸をランダムに回転 rot = Quaternion.Euler(0, Random.Range(0f, 360f), 0); } // ランダムな位置を作成 Vector3 pos = new Vector3(x, terrainPos.y + terrainData.size.y, z); int rePosCount = 0; bool RePos = false; // 自身のレイヤー名が設定されていて他の木にぶつかった場合は位置を再設定 if (!doubleCheckMode) { while (true) { if (Physics.Raycast(pos, Vector3.down, terrainPos.y + terrainData.size.y + 100f, placementObjLayer)) { if (randomMode) { x = (float)(terrainPos.x + rand.NextDouble() * (terrainPos.x + terrainData.size.x)); z = (float)(terrainPos.z + rand.NextDouble() * (terrainPos.z + terrainData.size.z)); } else { x = Random.Range(terrainPos.x, terrainPos.x + terrainData.size.x); z = Random.Range(terrainPos.z, terrainPos.z + terrainData.size.z); } pos = new Vector3(x, terrainPos.y + terrainData.size.y, z); Debug.Log("他の木にぶつかった"); } else { break; } rePosCount++; // 3回位置を直したらもう直さない if (rePosCount > 3) { RePos = true; break; } } } RaycastHit hit; // 地面の位置とゲームオブジェクトの位置を合わせる if (!RePos && Physics.Raycast(pos, Vector3.down, out hit, terrainPos.y + terrainData.size.y + 100f, fieldLayer)) { // 高さを地面に合わせる pos.y = hit.point.y; // 木のインスタンスの作成 ins = Instantiate<GameObject>(placementObj, pos, rot); // 地面の傾斜に合わせて木を傾ける設定の場合は木を回転させる if (slope) { ins.transform.rotation = Quaternion.FromToRotation(ins.transform.up, hit.normal) * ins.transform.rotation; } // ランダムに木のスケールを変える場合 if (useScale) { float randomScale = 1f; if (randomMode) { randomScale = (float)(minScale + rand.NextDouble() * maxScale); } else { randomScale = Random.Range(minScale, maxScale); } ins.transform.localScale = new Vector3(randomScale, randomScale, randomScale); ins.transform.position += (ins.transform.up * offset) * randomScale; } // ある程度重なり合っている場合は削除(指定した設置数より大幅に減る) if (doubleCheckMode) { Collider[] hitCol = Physics.OverlapSphere(ins.transform.position, maxScale / 2f, placementObjLayer); // 自身以外の同じレイヤー名のものにヒットしたら残念ながらインスタンスを削除する if (hitCol != null) { bool check = false; foreach (var col in hitCol) { if (col.gameObject.GetInstanceID() != ins.gameObject.GetInstanceID()) { check = true; } } if (check) { DestroyImmediate(ins.gameObject); } } } if (ins != null) { ins.transform.SetParent(parentObj.transform); } } } SaveData(); } |
非常に処理が多くて解り辛いですねぇ・・・・。
細かく見ていきましょう。
設定値の確認
まずは設定がうまくされていない時のエラー処理部分です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | // テレインがセットされていなければエラー if (terrain == null) { Debug.LogError("not set terrain", this); return; // オブジェクトがセットされていなければエラー } else if (placementObj == null) { Debug.LogError("not set placementObj", this); return; } else if (count <= 0) { Debug.LogError("A numerical value less than or equal to 0 is set"); return; } else if (fieldLayer == 0) { Debug.LogError("not set fieldLayer"); return; } else if(placementObjLayer == 0) { Debug.LogError("not set placementObjLayer"); return; } |
terrainやplacementObjが設定されていなければ問題なのでDebug.LogErrorを使ってコンソールにエラー表示をしたあとreturnを使ってメソッドを抜けます。
OnWizardUpdateで処理を記述しているのでcountは0以下にはならないと思いますが一応エラー処理を加えています。
fieldLayer、placementObjLayerは地面や設定するゲームオブジェクトを把握する為に必ず必要なので設定されていなければエラーにします。
ここでは0と比較していますが、0の場合はレイヤーが設定されていない状態の時です。
Terrainデータの取得、親のゲームオブジェクトの作成、Undo操作
次はTerrainデータの取得と設置するゲームオブジェクトの空の親ゲームオブジェクトの作成、Undo操作の登録をしている部分です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | // テレインデータを確保 Vector3 terrainPos = terrain.GetPosition(); TerrainData terrainData = terrain.terrainData; // 親の作成 GameObject parentObj = new GameObject(); if (parentName == "") { parentName = "parentObj"; } parentObj.name = parentName; // 親要素のUndo登録をして親を消す事で子も全部消えるようにしておく Undo.RegisterCreatedObjectUndo(parentObj, "Create " + parentObj.name); |
Terrainの位置はterrain.GetPosition()で取得出来ます。
これはTerrainのTransformの位置情報(Vector3)が得られるので、Terrainの位置を動かした場合でも対応出来るように利用します。
Terrainの横幅や奥の幅などはterrain.terrainDataで取得出来ます。
設置するゲームオブジェクトはTerrainの横と奥の幅の範囲内で設置しなければいけないのでこのデータを利用します。
設置するゲームオブジェクトをそのままシーン上に配置すると大変な事になるので、設置するゲームオブジェクトの親のゲームオブジェクトを作成し、子要素に設置するゲームオブジェクトを配置していくようにします。
親のゲームオブジェクトの名前が設定されていなければparentObjという名前にします。
作成した親ゲームオブジェクトはparentObjに参照が入っているので、parentObj.nameに親オブジェクトの名前を入れるとゲームオブジェクトの名前が変更されます。
親ゲームオブジェクトを作成した事をUndoに登録しておきCtrl+Zキーを押した時に元に戻す事が出来るようにしておきます。
親ゲームオブジェクトを元に戻せば子要素も消えるので子ゲームオブジェクト個々にはUndo登録をしません。
ゲームオブジェクトを配置する
for文の中身がメインのゲームオブジェクトを配置する処理です。
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 99 100 101 102 103 104 105 106 107 108 109 110 111 | // シードを指定してランダムクラスを作成 System.Random rand = new System.Random(Time.time.ToString().GetHashCode()); for (var i = 0; i < count; i++) { // X軸の位置 float x; // Z軸の位置 float z; // 角度 Quaternion rot; // Terrainの大きさからランダム値を計算 if (randomMode) { x = (float)(terrainPos.x + rand.NextDouble() * terrainData.size.x); z = (float)(terrainPos.z + rand.NextDouble() * terrainData.size.z); // Y軸をランダムに回転 rot = Quaternion.Euler(0, (float)(rand.NextDouble() * 360), 0); } else { x = Random.Range(terrainPos.x, terrainPos.x + terrainData.size.x); z = Random.Range(terrainPos.z, terrainPos.z + terrainData.size.z); // Y軸をランダムに回転 rot = Quaternion.Euler(0, Random.Range(0f, 360f), 0); } // ランダムな位置を作成 Vector3 pos = new Vector3(x, terrainPos.y + terrainData.size.y, z); int rePosCount = 0; bool RePos = false; // 自身のレイヤー名が設定されていて他の木にぶつかった場合は位置を再設定 if (!doubleCheckMode) { while (true) { if (Physics.Raycast(pos, Vector3.down, terrainPos.y + terrainData.size.y + 100f, placementObjLayer)) { if (randomMode) { x = (float)(terrainPos.x + rand.NextDouble() * (terrainPos.x + terrainData.size.x)); z = (float)(terrainPos.z + rand.NextDouble() * (terrainPos.z + terrainData.size.z)); } else { x = Random.Range(terrainPos.x, terrainPos.x + terrainData.size.x); z = Random.Range(terrainPos.z, terrainPos.z + terrainData.size.z); } pos = new Vector3(x, terrainPos.y + terrainData.size.y, z); Debug.Log("他の木にぶつかった"); } else { break; } rePosCount++; // 3回位置を直したらもう直さない if (rePosCount > 3) { RePos = true; break; } } } RaycastHit hit; // 地面の位置とゲームオブジェクトの位置を合わせる if (!RePos && Physics.Raycast(pos, Vector3.down, out hit, terrainPos.y + terrainData.size.y + 100f, fieldLayer)) { // 高さを地面に合わせる pos.y = hit.point.y; // 木のインスタンスの作成 ins = Instantiate<GameObject>(placementObj, pos, rot); // 地面の傾斜に合わせて木を傾ける設定の場合は木を回転させる if (slope) { ins.transform.rotation = Quaternion.FromToRotation(ins.transform.up, hit.normal) * ins.transform.rotation; } // ランダムに木のスケールを変える場合 if (useScale) { float randomScale = 1f; if (randomMode) { randomScale = (float)(minScale + rand.NextDouble() * maxScale); } else { randomScale = Random.Range(minScale, maxScale); } ins.transform.localScale = new Vector3(randomScale, randomScale, randomScale); ins.transform.position += (ins.transform.up * offset) * randomScale; } // ある程度重なり合っている場合は削除(指定した設置数より大幅に減る) if (doubleCheckMode) { Collider[] hitCol = Physics.OverlapSphere(ins.transform.position, maxScale / 2f, placementObjLayer); // 自身以外の同じレイヤー名のものにヒットしたら残念ながらインスタンスを削除する if (hitCol != null) { bool check = false; foreach (var col in hitCol) { if (col.gameObject.GetInstanceID() != ins.gameObject.GetInstanceID()) { check = true; } } if (check) { DestroyImmediate(ins.gameObject); } } } if (ins != null) { ins.transform.SetParent(parentObj.transform); } } } SaveData(); |
ランダムな数値はRandom.Rangeで取得出来ますが、Unity5.3.4f1だとシードが設定出来ない為System.Randomクラスを使ってランダム値を計算します。
UnityEngine.RandomクラスのシードはRandom.InitStateで指定出来るので先のUnityのバージョンを使っている人はそこでシードを指定し通常どおりRandom.Rangeを使うといいかもしれません。
Terrainのサイズからランダムな位置とY軸をランダムに回転させた角度を作成します。
Y軸の位置はとりあえずTerrainの中に食い込まない位置で設定しています。
doubleCheckModeがfalseで設置するゲームオブジェクトのレイヤー名が指定されていたら作成したランダムな位置から下向きにレイを飛ばし、そのレイヤー名のゲームオブジェクトとぶつかった場合は、
再度ランダムな位置を計算します。
3回位置を変えてもぶつかった場合はこの回はゲームオブジェクトを配置しないようRePosにtrueを設定します。
この処理の仕方だとツールのcountの設置値の数分すべてを設置するとは限りません。
Debug.Logで『他の木にぶつかった』という文字列を表示していますが、これは当初は木のゲームオブジェクトを配置する事だけを考えていた時の名残りなので気にしないでください。
(^_^;)
RaycastHit変数以下の処理が実際にゲームオブジェクトを配置している処理になります。
ランダムに作成した位置から下方向にレイを飛ばしfieldLayerが設定されているTerrainとの接触点(hit.point)を探します。
Instantiateメソッドでゲームオブジェクトをランダムな位置、角度でインスタンス化します。
slopeがtrue、つまり地面の角度とゲームオブジェクトの角度を合わせる場合はQuaternion.FromToRotationを使用しインスタンスの上向きと接触面の向きを使って角度を計算します。
この辺りは
で詳しく解説しています。
useScaleがtrueの場合は設置するゲームオブジェクトのScaleをランダムに変更する処理を加えます。
offset値分ゲームオブジェクトを移動した後にランダムなScaleをかけてどんなスケール値でも移動値が調整されるようにしています。
doubleCheckModeがtrueだった時は自身の位置を中心にmaxScaleの半分の値を半径にしてplacementObjLayerレイヤーとの接触をしているかどうかを調べます。
接触していてそれが自身とは違う場合は他の同じゲームオブジェクトと接触している為、インスタンス化した自分自身を削除します。
接触していたら削除しているのでツールのcountの設定で指定した数すべてがシーン上に配置されません。
他の同じゲームオブジェクトとの接触を判定する円の半径にmaxScaleの半分の値を使っているので大ざっぱな当たり判定範囲になります。
最後にSaveDataメソッドを呼び出して設定を保存します。
設定の保存と読み込み
設定の保存と読み込みをしている処理を追加します。
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 | void SaveData() { Data data = new Data(terrain, placementObj, doubleCheckMode, randomMode, count, slope, offset, useScale, minScale, maxScale, parentName, fieldLayer, placementObjLayer); var jsonData = JsonUtility.ToJson(data); EditorPrefs.SetString("ObjectPlacementToolData", jsonData); } void LoadData() { var data = JsonUtility.FromJson<Data>(EditorPrefs.GetString("ObjectPlacementToolData")); terrain = data.terrain; placementObj = data.placementObj; doubleCheckMode = data.doubleCheckMode; randomMode = data.randomMode; count = data.count; slope = data.slope; offset = data.offset; useScale = data.useScale; minScale = data.minScale; maxScale = data.maxScale; parentName = data.parentName; fieldLayer = data.fieldLayer; placementObjLayer = data.placementObjLayer; } [System.Serializable] class Data { public Terrain terrain; public GameObject placementObj; public bool doubleCheckMode; public bool randomMode; public int count; public bool slope; public float offset; public bool useScale; public float minScale; public float maxScale; public string parentName; public LayerMask fieldLayer; public LayerMask placementObjLayer; public Data() { } public Data(Terrain terrain, GameObject placementObj, bool doubleCheckMode, bool randomMode, int count, bool slope, float offset, bool useScale, float minScale, float maxScale, string parentName, LayerMask fieldLayer, LayerMask placementObjLayer) { this.terrain = terrain; this.placementObj = placementObj; this.doubleCheckMode = doubleCheckMode; this.randomMode = randomMode; this.count = count; this.slope = slope; this.offset = offset; this.useScale = useScale; this.minScale = minScale; this.maxScale = maxScale; this.parentName = parentName; this.fieldLayer = fieldLayer; this.placementObjLayer = placementObjLayer; } } |
DataクラスをSerializableアトリビュートを取り付けて作成しインスタンス化をする時にデータをフィールドに設定出来るようにします。
SaveDataメソッドではDataクラスのインスタンス化をする時に現在の設定をコンストラクタに渡します。
その後JsonUtilityのToJsonメソッドを使用して作成したDataクラスのインスタンスからJSON形式のデータを作成します。
作成したJSON形式のデータをEditorPrefsを使ってObjectPlacementToolDataという名前のキーに保存しています。
LoadDataメソッドはEditorPrefsに保存したObjectPlacementToolDataからJSON形式のデータを取得し、JsonUtilityのFromJsonメソッドでJSONデータからDataクラスを作成します。
取得したデータを設定値に入れています。
これで自動でゲームオブジェクトを配置するツールが完成しました!
自動ゲームオブジェクト配置ツールを使用してみる
ゲームオブジェクトを配置する
機能が完成したので実際にTerrainにゲームオブジェクトを配置してみましょう。
Terrainを作成し、設定をほどこしたらレイヤーにFieldを設定します(なければ作成し設定します)。
↑のようにTerrainのサイズや位置、レイヤーの設定をしました。
次に設定するCubeゲームオブジェクトのインスペクタにレイヤーを設定します。
↑のようにObjレイヤーを設定しました。
スクリプトを作成しているのですでにUnityのメニューにObjectPlacementTool→ObjectPlacementToolが追加されているはずです。
選択しCubeゲームオブジェクトをTerrainに配置してみます。
Cubeの中心はゲームオブジェクトの真ん中なのでoffsetに0.5を設定します。
とりあえず2000個を配置する設定にしますが、doubleCheckModeにチェックを入れている為実際に生成される数はかなり少なくなります。
またslopeにチェックを入れているので地形に沿って角度が変わります。
↑のようにCubeがTerrainの地形に合わせて設置されました。
次に木を設置してみます。
あらかじめコライダを設定した木を作成しておきレイヤーにObjを設定し、配置します。
設置するゲームオブジェクトはシーン上のものも出来ますが、Assetsフォルダにあるプレハブも同様に設定する事が出来ます。
↑のように木も設置出来ました。
作成したゲームオブジェクトは設定した親ゲームオブジェクトの子要素に作られます。
↑のように指定した名前の親の子要素に全てのゲームオブジェクトが作られています。
ゲームオブジェクトが重なっている部分やいらない部分は個別にシーン上から削除し最終的な調整をするといいかもしれません。
キャラクターを操作してどのように設置されているか確認
ゲームオブジェクトを配置したのでキャラクターを動かしてどのように配置されているか見てみましょう。
シーン上にはWindZoneを設定し木の葉が揺れるようにもしておきます。
WindZoneに関しては
を参照してください。
↑のようになりました。
自動で配置したゲームオブジェクトですがそれなりの景色になっていますね。(^_^)v
自動ゲームオブジェクト配置ツールのダウンロード
せっかく作ったのでパッケージ化し配布したいと思います。
使用に関して何らかの不具合が発生したとしても責任は負えませんのでご自分の判断で使用してください。
ダウンロードしたら解凍し、UnityのAssetsフォルダで右クリック→Import Package→Custom PackageでObjectPlacementToolパッケージを指定してください。
インポートが終了するとUnityのメニュー項目にMyMenuが登場します。
パッケージに含まれるReadMeテキストファイルも確認しておいてください。
すでにTerrainに建物等を配置し作りこんでいる場合は使用しない方がいいかもしれません。
今までの苦労が水の泡となるかもしれないので・・・・(‘_’)