Verse - 프로시저럴 빌딩(Verse - Procedural Building) 템플릿은 Verse로 작성된 프로시저럴 빌딩 시스템을 선보이는 포트나이트 언리얼 에디터 (Unreal Editor for Fortnite, UEFN)의 Re: Imagine London(섬 코드: 1442-4257-4418)에서 가져온 샘플 프로젝트입니다.
프로시저럴 빌딩 시스템은 포트나이트가 아닌 Verse에서 전적으로 구현되고 생성됩니다. 이 시스템을 활용하면 프로젝트에서 메시를 조합하는 방식을 보다 세부적으로 제어할 수 있습니다.
포트나이트에서는 프리셋 단일 구조물(바닥, 천장, 벽)로 게임플레이를 디자인할 수 있습니다. 프로시저럴 빌딩 시스템은 기본 시스템을 설정하는 카테고리로 복셀을 생성할 수 있는 가능성을 확장해 줍니다. 이러한 기본 시스템은 시스템이 메시를 배치함에 따라 빠르게 추가 복셀을 배치할 수 있도록 합니다.
UEFN 내에서 이 템플릿에 액세스하려면 프로젝트 브라우저(Project Browser) 에서 기능 예시(Feature Examples) > 게임 예시(Game Examples) > Verse - 프로시저럴 빌딩(Verse - Procedural Building) 으로 이동하면 됩니다.

프로시저럴 빌딩 시스템은 두 단계로 작동합니다.
-
첫 번째 단계에서는 복셀을 추가하거나 제거하여 각 셀이 박스로 되어 있는 3D 그리드인 복셀 그리드를 편집합니다.
-
두 번째 단계는 복셀 그리드가 수정될 때마다 실행됩니다. 복셀 그리드를 수정하면, 복셀 정보를 가져와 적절한 장소에 적절한 메시를 생성하는 프로세스가 실행됩니다.
건물(Building) 및 공원(Park) 복셀 카테고리를 모두 추가하고 제거할 수 있습니다. 각 카테고리에는 메시가 생성되는 방법에 대한 서로 다른 규칙이 있습니다. 이러한 규칙은 배치된 복셀에서 실제 메시로 변환하기 위해 수행되는 연산의 그래프 또는 트리를 가리킵니다.
템플릿 사용하기
프로젝트 내 각 레벨에는 하나의 최상위 클래스가 있으며, 이는 root_device class
라고 합니다. 플레이어가 게임에 참가하면 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]()
에 대한 선택적 레퍼런스의 1D 배열로 구현됩니다.
# 복셀의 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
로직 변수를 설정하는 데 사용됩니다.
이 함수의 경우 공원 복셀이 표면으로만 간주되어 레이를 차단하지 못하고 입체 복셀(지면 또는 건물) 위에만 존재할 수 있으므로 추가 로직이 필요합니다. 표면으로만 존재하도록 할 새 카테고리를 추가할 때 CategoryIsSurfaceOnly
함수를 업데이트하면 됩니다.
발사(Fire) 버튼을 길게 눌러 평면에 여러 개의 복셀을 빠르게 배치하는 빠른 건설(Turbo building) 도 지원됩니다. 또한 이 함수는 CheckPlayerOverlapsVoxel
함수에 추가되기 전에 플레이어가 복셀 내부에 있는지 여부도 확인합니다.
사물 생성하기
이 샘플 프로젝트는 [runtime]()
시 사물 생성에 의존합니다. creative_prop_asset
은 현재 Verse 매니페스트 파일에 자동으로 반영되지 않습니다. 따라서 프록시 오브젝트(piece_type_dir
클래스 내 piece_type
의 인스턴스)를 사용하여 Verse에서 특정 사물을 레퍼런스해야 합니다.
그런 다음 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에서 다음과 같은 두 가지 유형의 프로시저럴 생성을 구현합니다. 형상 문법(Shape Grammar) 과 웨이브 함수 붕괴(Wave Function Collapse) 입니다. 형상 문법은 3D 건물에 적용되는 반면, 웨이브 함수 붕괴는 2D(평면) 영역에 사용됩니다.

위 이미지는 생성된 건물 사물의 예시입니다.

위 이미지는 생성된 공원 사물의 예시입니다.
두 기법 모두 모듈식 건물 유형 사물 세트를 생성해야 하며, 그러면 Verse가 런타임 시 스폰합니다. 이 코드는 결정론적이고, 필요에 따라서만 사물을 삭제 및 생성합니다.
형상 문법
형상 문법이라는 것을 적용하기 위해 한 카테고리의 모든 복셀이 더 큰 컨벡스 박스로 변환됩니다.

형상 문법은 간단한 규칙으로 구성되고, 각 규칙은 박스를 가져온 다음 후속 규칙을 위해 하나 이상의 하위 박스를 생성합니다.

예를 들어 어떤 규칙은 하나의 긴 박스를 복셀 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=
[...]
# 건물 한 층에 대한 규칙
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
내에 있습니다.
웨이브 함수 붕괴
웨이브 함수 붕괴(Wave Function Collapse, 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
에Park_WFCModel
과 같은 새wfc_model
멤버를 추가합니다. -
새 모델을 사용하여 타일을 추가하는
AddParkWFCTile
과 같은 새 함수를 추가합니다. -
새 카테고리에 대해 새 함수를 호출하도록
SelectModeTick_Trace
를 수정합니다.

또한 절차적으로 복셀을 생성할 수 있습니다. 샘플 프로젝트에는 랜덤 건물 또는 공원 지역을 건설 구역에 추가하는 버튼이 있습니다. 이 버튼은 build_system
의 ClearRegion
및 AddRegion
함수를 사용하며, 랜덤 레벨 생성 시스템의 시작점으로 사용될 수 있습니다.
Verse 퍼포먼스
이 샘플 프로젝트의 Verse 코드는 실시간으로 업데이트를 처리할 수 있을 정도로 빨라야 합니다. 큰 배열을 처리하면 퍼포먼스 문제로 이어질 수 있으므로, 다음 정보를 염두에 두는 것이 중요합니다.
- 추가될 때마다 배열이 복사되어 시간 복잡도가 O(N2)가 될 수 있는 엘리먼트별 빌드보다
for
루프를 사용하여 배열을 반환하는 것이 더 빠릅니다. 예를 들면 다음과 같습니다.
* set OptionalArray := for(I := 0 .. ArraySize-1):
false
-
큰 배열을 값으로 전달하지 말고, 대신 오브젝트에 넣어 메서드를 호출하거나 오브젝트를 전달하세요.
-
다차원 배열은 첫 번째
[]
연산자가 사본을 다음[]
연산자로 전달하므로 속도가 느립니다. -
배열에서
.Length
를 호출하면 실제로 그 순간에 배열 사본이 만들어지므로, 큰 배열의 크기를 직접 트래킹하는 것이 더 빠를 수 있습니다.
또한 코드의 어떤 부분에 가장 많은 시간이 소요되는지 정확히 파악하기 위해 profile
매크로를 사용하는 것도 매우 도움이 됩니다.