Verse - プロシージャル建築 テンプレートは、Unreal Editor for Fortnite (UEFN) の Re: Imagine London (島コード: 1442-4257-4418) のサンプル プロジェクトであり、Verse で記述されたプロシージャル建築システムを紹介するものです。
プロシージャル建築システムの実装と作成はすべて、フォートナイトではなく Verse で行います。このシステムを使用すると、プロジェクト内でメッシュがどのように組み立てられるかをより細かく制御できます。
フォートナイトでは、事前に設定された建築物のピース (床、天井、壁) を使用してゲームプレイを設計できます。プロシージャル建築システムを使用すると、可能性がより広がり、システムがメッシュを配置するときに追加のボクセルをすばやく配置可能な基礎システムを設定するカテゴリを持つ ボクセル を作成できるようになります。
UEFN プロジェクト ブラウザ 内でこのテンプレートにアクセスするには、[Feature Examples (機能例)] > [Game Examples (ゲーム サンプル)] > [Verse - Procedural Building (Verse - プロシージャル建築)] に移動します。
プロシージャル建築システムは 2 つのフェーズで動作します。
-
最初のフェーズでは、ボクセルを追加または削除して ボクセル グリッド (各セルがボックスである 3D グリッド) を編集します。
-
2 番目のフェーズは、ボクセル グリッドが変更されるたびに実行されます。ボクセル グリッドを変更するとボクセル情報が取得され、適切な場所に適切なメッシュをスポーンするプロセスが実行されます。
Building と Park の両方のボクセル カテゴリを追加および削除できます。各カテゴリには、メッシュのスポーン方法について異なる ルール が用意されています。これらのルールは、配置されたボクセルから実際のメッシュに移行するために実行される操作のグラフまたはツリーを参照します。
テンプレートを使用する
プロジェクト内の各レベルには、root_device class
という 1 つの最上位クラスがあります。プレイヤーがゲームに参加すると、root_device
クラスはそのプレイヤーの UI 情報を設定する global_player_data
を作成します。
各建築ゾーンには、サイトの寸法をボクセル単位で定義する build_zone
仕掛けがあります。この仕掛けの位置によってサイトの原点が定義されます。建築物エリアでは、建築物のメカニクスを処理するために build_system
オブジェクトを使用し、建築物小道具メッシュをスポーンするために spawner_device
を使用します。建築物エリアには、voxel_grid
、mesh_grid
、wfc_system
も含まれています。
各建築物エリアには次のものが保持されます。
-
build_zone
(仕掛け) - サイトの寸法をボクセル単位で定義します。 -
build_system
- 建築物のメカニクスを処理します。 -
spawner_device
(仕掛け) - 建築物小道具メッシュをスポーンします。 -
spawner_asset_references
(仕掛け) - スポーンされたすべての小道具を参照します。
プレイヤーが ボリューム仕掛けによってトリガーされる build_zone
に入ると、そのプレイヤーの入力とボクセル編集を処理するための player_data
が作成されます。
ボクセルを使った建築物
このテンプレートは、ボクセルを X
、Y
、Z
[integer]()
座標で表す vector3i [type]()
を追加します。
以下は vector3i
を使用したスクリプトの例です。
# `int` コンポーネントを持つ 3 次元ベクトル
vector3i<public> := struct<computes><concrete>:
@editable
X<public>:int = 0
@editable
Y<public>:int = 0
@editable
Z<public>:int = 0
ボクセル グリッド
ボクセル グリッドは、各建築ゾーンのセルの 3D グリッドであり、存在する建築物ボクセルの種類に関する情報を保存します。これは、以下に示すように、voxel_grid
クラスで voxel_cell [objects]()
へのオプションの参照の 1 次元配列として実装されています。
# ボクセルの 3D グリッドを表すメイン配列
var Cells<public> : []?voxel_cell = array{}
デフォルトでは、ボクセル セル オブジェクトには以下に示すようにボクセル タイプの [enum]()
のみが含まれます。
# 建築物グリッド内のボクセルのカテゴリ
build_category<public> := enum:
Building
Park
NewBuildingType
# 占有されているボクセルごとに保存される構造
voxel_cell<public> := struct<computes>:
Category<public>:build_category
上記のように列挙型を拡張することで、建築物のボクセル カテゴリをさらに追加できます。
以下のコードは、3D ボクセル座標を 1D の [index]()
に変換します。
# グリッド内の 3D 位置から 1D 配列インデックスを取得します
GetVoxelIndex<public>(X:int, Y:int, Z:int)<transacts>:int=
return (X * (Size.Y*Size.Z)) + (Y * Size.Z) + Z
SetVoxel
および ClearVoxel
ボクセル グリッドを変更する重要な関数です。
レイキャスト
ボクセル グリッドが設定されると、システムはレイ コリジョン チェックを実行して、ボクセルの面またはレイが当たるボクセルの側面をチェックします。各ボクセルにはサイコロと同様に 6 つの面があります。レイキャストを行うときは、ハイライトを描画しその面に対して新しいボクセルをスポーンするために、どのボクセルとどの面をヒットしたかの両方を知る必要があります。
レイ コリジョン チェックは主に ray_caster
クラスで処理されます。これは、カメラの位置をグリッドのローカル空間に変換して、最初にレイがどのボクセルから始まるかを決定することによって行われます。このボクセルは以下に示すようにボクセルの寸法で分割されます。
CurrentVoxel := vector3i:
X := Floor[InitialPosition.X / GridSize.X]
Y := Floor[InitialPosition.Y / GridSize.Y]
Z := Floor[InitialPosition.Z / GridSize.Z]
[Next]()
関数は、レイが次に通過するボクセルを決定するために繰り返し呼び出され、そのたびにボクセルがソリッドであるかどうかがチェックされます。
建築物システムの入力
player_data
の SelectModeTick_Trace
関数はフレームごとに実行され、ボクセル編集とカーソル更新ロジックの大部分を処理します。2 つの [Input Trigger]()
仕掛けは、Fire (発射) ボタンと Aim (照準) ボタンが押されたタイミングを認識し、PlacePiece
ロジック変数と DeletePiece
ロジック変数を設定するために使用されます。
この関数には追加のロジックが必要となります。Park ボクセルはサーフェスのみとみなされるため、レイをブロックせずソリッド ボクセル (地面または建築物) の上にのみ存在できます。サーフェスのみに表示したい新しいカテゴリを追加する際に、CategoryIsSurfaceOnly
関数を更新できます。
ターボ ビルド もサポートされており、[Fire] ボタンを押し続けると複数のボクセルを平面にすばやく配置できます。この関数は、プレイヤーが CheckPlayerOverlapsVoxel
関数に追加される前にプレイヤーがボクセル内にいるかどうかもチェックします。
小道具をスポーンする
このサンプル プロジェクトは、[runtime]()
での小道具のスポーンに依存しています。現在、creative_prop_asset
は Verse マニフェスト ファイルに自動的に反映されません。したがって、Verse 内の特定の小道具を参照するには、プロキシ オブジェクト (piece_type_dir
クラスの piece_type
のインスタンス) を使用する必要があります。
次に、spawner_asset_references
仕掛けは、各小道具に対して @editable
フィールドとプロキシから実際のアセットへのマッピング テーブルを使用します。新しいメッシュを追加するには、まずそのメッシュの BuildingProp
を作成し、新しいプロキシを追加し、仕掛けにプロパティを追加して、マッピング テーブルを更新する必要があります (以下を参照)。最後に、仕掛け上の新しいプロパティを再コンパイルして更新し、新しい小道具を指すようにします。
Building1_corner:piece_type := piece_type{}
@editable
BP_Building1_corner : creative_prop_asset = DefaultCreativePropAsset
PT.Building1_corner => BP_Building1_corner
Verse でのプロシージャル生成
この例では、Verse の 2 種類のプロシージャル生成である 形状文法 と 波動関数崩壊 を実装しています。形状文法は 3D の建築物に適用されます。一方、波動関数崩壊は 2D (フラットな) 領域に使用されます。
どちらの手法でも、モジュール式の建築物タイプの小道具のセットを作成し、それを実行時に Verse がスポーンする必要があります。コードは決定論的であり、必要に応じて小道具を削除およびスポーンすることのみが行われます。
形状文法
カテゴリのすべてのボクセルは、形状文法と呼ばれるものを適用するためにより大きな凸型ボックスに変換されます。
形状文法は、各ルールがボックスの形式を取り、以降のルールに 1 つ以上のサブボックスを生成する単純なルールで構成されます。
たとえば、1 つのルールで背の高いボックスを 1 ボクセルの高さの床のピースにスライスし、コーナーと壁には別のルールを割り当てることができます。特別なルールにより、ボックスと同じサイズと場所に小道具をスポーンできます。
各ルールは、vo_base
(ボリューム演算子) クラスから派生した個別の Verse クラスとして定義されます。これらは、rs_base
(ルール セット) 派生クラス内のルール セット ツリーに組み立てられます。
このアプローチを用いることで、新しいルールの作成の簡素化、さまざまなアイデアの実験、各タイプの建築物に対する異なるスタイルの割り当てが可能となります。同じボクセル セットに異なるルールを適用すると、さまざまな結果が得られます。
以下は、vo_sizecheck
ボリューム演算子の簡単な例です。
# ボックスのすべての寸法が特定のサイズ以上であることを確認します
vo_sizecheck := class(vo_base):
Size:vector3i = vector3i{X:=0, Y:=0, Z:=0}
VO_Pass:vo_base = vo_base{}
VO_Fail:vo_base = vo_base{}
Operate<override>(Box:voxel_box, Rot:prop_yaw, BuildSystem:build_system):void=
if(Box.Size.X >= Size.X and Box.Size.Y >= Size.Y and Box.Size.Z >= Size.Z):
VO_Pass.Operate(Box, Rot, BuildSystem)
else:
VO_Fail.Operate(Box, Rot, BuildSystem)
基本クラスで定義されている Operate
関数をオーバーライドし、入力ボックスのサイズをチェックして、次のルール (vo_pass
または vo_fail
) のどちらを呼び出すかを決定します。
Verse では、多くのボリューム演算子を含むルール セットを簡単に設定できます。setupRules
関数をオーバーライドして、[operators]()
とその ()
を宣言[parameters]できます。開始点、つまりルート演算子は、以下に示すように VO_root
に割り当てられます。
# 「Building1」スタイルのルール
rs_building1<public> := class(rs_base):
RuleSetName<override>:string = "Building1"
SetupRules<override>(PT:piece_type_dir):void=
[...]
# 建築物の 1 フロアのルール
FloorRules := vo_cornerwallsplit:
VO_CornerLength1 := vo_corner:
VO_Corner := vo_prop:
Prop := PT.Building1_corner
VO_Face := vo_prop:
Prop := PT.Building1_face
VO_Covered := vo_prop:
Prop := PT.Building1_box
VO_Wall := vo_wall:
VO_Width1 := vo_facecoveragecheck_1voxel:
VO_Clear := vo_prop:
Prop := PT.Building1_face
VO_Covered := vo_prop:
Prop := PT.Building1_box
VO_Centre := RoofTileCheck
VO_TooSmall := vo_tilefill:
VO_Tile := FourWay
set VO_Root = vo_floorsplit:
VO_FloorHeight1 := FloorRules
このメソッドにより、さまざまな建築物のカテゴリに対して新しい演算子とルール セットを簡単に作成できます。ルール セットは InitRuleSets
に割り当てられ、SelectRuleSet
の特定のカテゴリに対して選択されます。これらは両方とも build_system
にあります。
波動関数崩壊
波動関数崩壊 (WFC) は、ルールに基づいてランダムに領域を生成する手法であり、ピース同士の組み合わせを決定します。
この実装では、どのタイルが隣接できるかをタイルのセットを使用して指定できます。各エッジにラベルが適用され、一致するラベルを持つタイルのみが配置されます。
以下は、wfc_mode_factory
クラスのエッジ ラベルの例です。
WaterEL:wfc_edge_label := wfc_edge_label:
Name:="Water"
Symmetric := true
以下は、上記の例のメッシュ定義の例です。
Park_Grass_Water_InCorn:wfc_mesh := wfc_mesh:
Props := array:
PT.Park_Grass_Water_Incorn
Name := "Park_Grass_Water_Incorn"
Edges := array:
WaterEL
WaterEL
WaterToGrassEL
GrassToWaterEL
+Y 方向から始めて時計回りにラベルを指定できます。
この例では各エッジが時計回りにカウントされるため、下端は「water-to-grass」となります。このシステムを使用すると、新しいラベルやメッシュ、または新しい WFC モデルをゲームプレイに簡単に追加できます。
WFC アルゴリズムによってグリッド上の場所が選択され、可能な選択肢がランダムに選択または破壊され、他の場所の可能な選択肢にその選択結果が反映されます。このプロセスは、領域全体が生成されるまで継続します。
wfc_system
クラスには、すべてのタイルの現在の状態が含まれています。特殊なボリューム演算子 vo_wfc
は状態を読み取り、正しいメッシュをスポーンします。
可能な拡張
以下に、このサンプル プロジェクトを新しい体験に変更する方法をいくつか示します。
新しいボクセル カテゴリ / 形状文法を追加する
-
build_category
列挙型を拡張する -
新しいカテゴリを処理する必要がある
case
ステートメントを更新する -
rs_base
から派生した新しいルール セットを作成する -
新しいカテゴリに新しいルール セットを使用するように
SelectRuleSet
を更新する
Park WFC モデルに新しいメッシュを追加する
-
新しいメッシュごとに建築物小道具を作成する (上記の 小道具をスポーンする で説明されているとおり)。
-
wfc_model_factory
のGetParkModel
に新しいwfc_edge_label
を追加する (必要に応じて) 。 -
新しいメッシュ / 小道具ごとに新しい
wfc_mesh
インスタンスを追加し、各エッジのラベルを定義する。 -
新しいメッシュごとに
Model.AddMesh
を呼び出す。
新しい WFC モデルを追加する
-
新しいモデルに新しいボクセル カテゴリを追加する。
-
新しいモデルを作成する
wfc_model_factory
に新しい関数を追加する。 -
build_system
に新しいwfc_model
メンバーを追加する (Park_WFCModel
など)。 -
新しいモデルを使用してタイルを追加する
AddParkWFCTile
などの新しい関数を追加する。 -
新しいカテゴリの新しい関数を呼び出すように
SelectModeTick_Trace
を変更する。
ボクセルをプロシージャルに生成することもできます。サンプル プロジェクトには、建築ゾーンにランダムな建築物または公園の領域を追加するボタンがあります。これは build_system
の ClearRegion
および AddRegion
関数を使用しており、ランダム レベル生成システムの開始点として使用できます。
Verse のパフォーマンス
このサンプル プロジェクトの Verse コードにより、コードがリアルタイムで更新を処理できるほど高速であることが保証されます。大規模な配列を扱うとパフォーマンスの問題が発生する可能性があるため、次の情報に留意することが重要です。
for
ループを使用して配列を返すと、要素ごとに配列を構築するよりも高速になります。これは、各追加で配列がコピーされるため、O(N2) になる可能性があるからです。次に例を示します。
* set OptionalArray := for(I := 0 .. ArraySize-1):
false
-
大きな配列を値で渡さずに、オブジェクトに入れてメソッドを呼び出すか、オブジェクトを渡します。
-
多次元配列は、最初の
[]
演算子がコピーを次の[]
演算子に渡すため遅くなります。 -
配列に対して
.Length
を呼び出すと、その時点で配列のコピーが実際に作成されます。そのため、大きな配列のサイズについては自分で追跡する方が早い場合があります。
profile
マクロを使用すると、コードのどの部分で最も時間がかかっているかを正確に把握しやすくなります。