UnityのTerrainにゲームオブジェクトを自動配置するツールを作成する

記事内に広告が含まれています。

今回はエディターウィザードを使ってTerrainの地面に合わせて指定した数のゲームオブジェクトを配置するツールを作成します。

今回のツールを作成したのはTerrainの木を配置する機能が思うように動作しなかった為です。

エディターウィザードに関しては

UnityのScriptableWizardを使って独自のエディターウィザードを開く
UnityでScriptableWizardを使ってメニューアイテムからエディターウィザードを開きゲームオブジェクトのパラメータを操作します。

の記事を参考にしてください。

スポンサーリンク

ゲームオブジェクト配置ツール

Terrainでは内部データとして木のデータを保持していて、本来はそこにアクセスして木をうまく配置出来るようにしてみようと思いましたが、

うまく動作しなかったので諦めました。(^_^;)

木をランダムに回転して配置する場合はLOD Groupを取り付ける必要があり、取り付けるとランダムに回転はするのですが、Wind Zoneの風で木と葉が揺れなくなってしまいます。

LOD Groupを取り付けない場合はWind Zoneによる風の影響は受けますが木がランダムな回転をせず全ての木が同じ角度になってしまいます。

そこでTerrain内部のデータとして木は作成出来ませんが、シーン上に木のプレハブをTerrainの地形に沿って指定数設置出来る機能を作成していきます。

単純に木のゲームオブジェクトだけでなく建物のゲームオブジェクトや石のゲームオブジェクト等も配置出来ます。

地形に合わせてゲームオブジェクトを回転させるだけでなく、ゲームオブジェクトをY軸でランダムに回転させ全て同じ方向を向かないようにします。

ツールスクリプトを作成していきます。

名前はObjectPlacementToolという名前にしました。

Toolというnamespaceの中にObjectPlacementToolクラスを作成します。

このクラスに処理を追加していきます。

ObjectPlacementToolクラスはEditorフォルダ内に移動させます。

設定用のフィールドの作成とエディターウィザードの表示

まずは設定用のフィールドを宣言するところまで記述します。

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メソッドではフィールド値を初期化する処理を記述します。

データの初期化をするのでEditorPrefsでObjectPlacementToolDataが存在していればそのデータを削除します。

EditorPrefsではEditorPrefs.DeleteAllというメソッドがありますが、これを使用すると自身で作成したEditorPrefsのデータのみならず、デフォルトのUnityの初期設定データすらも削除してしまうので使わないことをお勧めします。

わたくしは実際にDeleteAllメソッドを使ってしまってUnityが正常に動作しなくなって焦りました・・・・(^_^;)

その他はフィールド値を初期化しているだけなので問題はないと思います。

エディターウィザードで更新があった時の処理

エディターウィザードの設定項目を変更したりするとOnWizardUpdateメソッドが呼ばれます。

ここでは不正な設定値が設定された時にパラメータを制限内に設定し直す処理を記述しておきます。

countは一度に設置するゲームオブジェクトの数で10000を越えたら10000に設定しなおします。

また0以下の場合は実行する意味がないので1に設定しています。

minScaleは設置するゲームオブジェクトのスケールの最小値を指定しますが、0以下の値が設定されるとおかしいので最小値は0.1に設定しています。

ゲームオブジェクトを設置ボタンを押した時の処理

『作成ボタン』である『ゲームオブジェクトを設置』ボタンを押すとOnWizardCreateメソッドが呼ばれます。

その為『ゲームオブジェクトを設置』ボタンを押したら実際にゲームオブジェクトを配置する処理を記述します。

非常に処理が多くて解り辛いですねぇ・・・・。

細かく見ていきましょう。

設定値の確認

まずは設定がうまくされていない時のエラー処理部分です。

terrainやplacementObjが設定されていなければ問題なのでDebug.LogErrorを使ってコンソールにエラー表示をしたあとreturnを使ってメソッドを抜けます。

OnWizardUpdateで処理を記述しているのでcountは0以下にはならないと思いますが一応エラー処理を加えています。

fieldLayer、placementObjLayerは地面や設定するゲームオブジェクトを把握する為に必ず必要なので設定されていなければエラーにします。

ここでは0と比較していますが、0の場合はレイヤーが設定されていない状態の時です。

Terrainデータの取得、親のゲームオブジェクトの作成、Undo操作

次はTerrainデータの取得と設置するゲームオブジェクトの空の親ゲームオブジェクトの作成、Undo操作の登録をしている部分です。

Terrainの位置はterrain.GetPosition()で取得出来ます。

これはTerrainのTransformの位置情報(Vector3)が得られるので、Terrainの位置を動かした場合でも対応出来るように利用します。

Terrainの横幅や奥の幅などはterrain.terrainDataで取得出来ます。

設置するゲームオブジェクトはTerrainの横と奥の幅の範囲内で設置しなければいけないのでこのデータを利用します。

設置するゲームオブジェクトをそのままシーン上に配置すると大変な事になるので、設置するゲームオブジェクトの親のゲームオブジェクトを作成し、子要素に設置するゲームオブジェクトを配置していくようにします。

親のゲームオブジェクトの名前が設定されていなければparentObjという名前にします。

作成した親ゲームオブジェクトはparentObjに参照が入っているので、parentObj.nameに親オブジェクトの名前を入れるとゲームオブジェクトの名前が変更されます。

親ゲームオブジェクトを作成した事をUndoに登録しておきCtrl+Zキーを押した時に元に戻す事が出来るようにしておきます。

親ゲームオブジェクトを元に戻せば子要素も消えるので子ゲームオブジェクト個々にはUndo登録をしません。

ゲームオブジェクトを配置する

for文の中身がメインのゲームオブジェクトを配置する処理です。

ランダムな数値は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を使用しインスタンスの上向きと接触面の向きを使って角度を計算します。

この辺りは

UnityのRaycastHit.normalとQuaternion.FromToRotationを使いこなす
UnityのQuaternion.FromToRotationで2つの方向から角度を求める事が出来ますが、その時の方向にPhysics.Raycastで得られたRaycastHit.normalの値を使う事が多々あります。このRaycastHit.normalとはなんなのか?を調べました。

で詳しく解説しています。

useScaleがtrueの場合は設置するゲームオブジェクトのScaleをランダムに変更する処理を加えます。

offset値分ゲームオブジェクトを移動した後にランダムなScaleをかけてどんなスケール値でも移動値が調整されるようにしています。

doubleCheckModeがtrueだった時は自身の位置を中心にmaxScaleの半分の値を半径にしてplacementObjLayerレイヤーとの接触をしているかどうかを調べます。

接触していてそれが自身とは違う場合は他の同じゲームオブジェクトと接触している為、インスタンス化した自分自身を削除します。

接触していたら削除しているのでツールのcountの設定で指定した数すべてがシーン上に配置されません。

他の同じゲームオブジェクトとの接触を判定する円の半径にmaxScaleの半分の値を使っているので大ざっぱな当たり判定範囲になります。

最後にSaveDataメソッドを呼び出して設定を保存します。

設定の保存と読み込み

設定の保存と読み込みをしている処理を追加します。

DataクラスをSerializableアトリビュートを取り付けて作成しインスタンス化をする時にデータをフィールドに設定出来るようにします。

SaveDataメソッドではDataクラスのインスタンス化をする時に現在の設定をコンストラクタに渡します。

その後JsonUtilityのToJsonメソッドを使用して作成したDataクラスのインスタンスからJSON形式のデータを作成します。

作成したJSON形式のデータをEditorPrefsを使ってObjectPlacementToolDataという名前のキーに保存しています。

LoadDataメソッドはEditorPrefsに保存したObjectPlacementToolDataからJSON形式のデータを取得し、JsonUtilityのFromJsonメソッドでJSONデータからDataクラスを作成します。

取得したデータを設定値に入れています。

これで自動でゲームオブジェクトを配置するツールが完成しました!

自動ゲームオブジェクト配置ツールを使用してみる

ゲームオブジェクトを配置する

機能が完成したので実際にTerrainにゲームオブジェクトを配置してみましょう。

Terrainを作成し、設定をほどこしたらレイヤーにFieldを設定します(なければ作成し設定します)。

ゲームオブジェクトを配置するTerrainのインスペクタ

↑のようにTerrainのサイズや位置、レイヤーの設定をしました。

次に設定するCubeゲームオブジェクトのインスペクタにレイヤーを設定します。

設置するCubeのインスペクタ

↑のようにObjレイヤーを設定しました。

スクリプトを作成しているのですでにUnityのメニューにObjectPlacementTool→ObjectPlacementToolが追加されているはずです。

自作のUnityメニューが表示された

選択しCubeゲームオブジェクトをTerrainに配置してみます。

Cubeを設置する時のObjectPlacementToolの設定

Cubeの中心はゲームオブジェクトの真ん中なのでoffsetに0.5を設定します。

とりあえず2000個を配置する設定にしますが、doubleCheckModeにチェックを入れている為実際に生成される数はかなり少なくなります。

またslopeにチェックを入れているので地形に沿って角度が変わります。

Cubeを設置した時のサンプル

↑のようにCubeがTerrainの地形に合わせて設置されました。

次に木を設置してみます。

あらかじめコライダを設定した木を作成しておきレイヤーにObjを設定し、配置します。

木を設置する時のObjectPlacementToolの設定

設置するゲームオブジェクトはシーン上のものも出来ますが、Assetsフォルダにあるプレハブも同様に設定する事が出来ます。

Cubeと木が設定された

↑のように木も設置出来ました。

作成したゲームオブジェクトは設定した親ゲームオブジェクトの子要素に作られます。

ヒエラルキーに親の子要素としてゲームオブジェクトを配置

↑のように指定した名前の親の子要素に全てのゲームオブジェクトが作られています。

ゲームオブジェクトが重なっている部分やいらない部分は個別にシーン上から削除し最終的な調整をするといいかもしれません。

キャラクターを操作してどのように設置されているか確認

ゲームオブジェクトを配置したのでキャラクターを動かしてどのように配置されているか見てみましょう。

シーン上にはWindZoneを設定し木の葉が揺れるようにもしておきます。

WindZoneに関しては

Unityで木が風になびく機能を追加する
Unityで木が風になびく機能を追加する

を参照してください。

↑のようになりました。

自動で配置したゲームオブジェクトですがそれなりの景色になっていますね。(^_^)v

自動ゲームオブジェクト配置ツールのダウンロード

せっかく作ったのでパッケージ化し配布したいと思います。

使用に関して何らかの不具合が発生したとしても責任は負えませんのでご自分の判断で使用してください。

自動ゲームオブジェクト設置ツール

ダウンロードしたら解凍し、UnityのAssetsフォルダで右クリック→Import Package→Custom PackageでObjectPlacementToolパッケージを指定してください。

インポートが終了するとUnityのメニュー項目にMyMenuが登場します。

パッケージに含まれるReadMeテキストファイルも確認しておいてください。

すでにTerrainに建物等を配置し作りこんでいる場合は使用しない方がいいかもしれません。

今までの苦労が水の泡となるかもしれないので・・・・(‘_’)

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