Verse - 프로시저럴 빌딩 템플릿은 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(장치) - 생성된 모든 사물을 참조합니다.
플레이어가 볼륨(Volume) 장치에 의해 트리거되는 build_zone에 들어가면, 해당 플레이어의 입력 및 복셀 편집을 처리하기 위해 player_data가 생성됩니다.
복셀로 빌드하기
이 템플릿에는 X, Y, Z [integer]() 좌표로 복셀을 나타내는 vector3i [type]()가 도입되었습니다.
아래는 vector3i를 사용하는 예시 스크립트입니다.
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 배열로 구현됩니다.
# Main array representing the 3D grid of voxels
var Cells<public> : []?voxel_cell = array{}기본적으로 복셀 셀 오브젝트에는 아래와 같이 복셀 타입의 [enum]()만 포함되어 있습니다.
# Category of voxel in our build grid
build_category<public> := enum:
Building
Park
NewBuildingType
# Structure stored for each occupied voxel
voxel_cell<public> := struct<computes>:
Category<public>:build_category위와 같이 열거형을 확장하여 더 많은 건물 복셀 카테고리를 추가할 수 있습니다.
아래 코드는 3D 복셀 좌표를 1D [index]()로 변환합니다.
# Gets the 1D array index from a 3D location in grid
GetVoxelIndex<public>(X:int, Y:int, Z:int)<transacts>:int=
return (X * (Size.Y*Size.Z)) + (Y * Size.Z) + ZSetVoxel 및 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 함수는 매 프레임마다 실행되어 대부분의 복셀 편집 및 커서 업데이트 로직을 처리합니다. 두 개의 [Input Trigger]() 장치는 발사 및 조준 버튼이 눌리는 시점을 파악하고 PlacePiece 및 DeletePiece 로직 변수를 설정하는 데 사용됩니다.
이 함수의 경우 공원 복셀이 표면으로만 간주되어 레이를 차단하지 못하고 입체 복셀(지면 또는 건물) 위에만 존재할 수 있으므로 추가 로직이 필요합니다. 표면으로만 존재할 새 카테고리를 추가하는 경우 CategoryIsSurfaceOnly 함수를 업데이트하면 됩니다.
발사 버튼을 길게 눌러 평면에 여러 개의 복셀을 빠르게 배치하는 빠른 건설도 지원됩니다. 이 함수는 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에서 다음과 같은 두 가지 유형의 프로시저럴 생성을 구현합니다. 형상 문법과 웨이브 함수 붕괴입니다. 형상 문법은 3D 건물에 적용되는 반면, 웨이브 함수 붕괴는 2D(평면) 영역에 사용됩니다.
위 이미지는 생성된 건물 사물의 예시입니다.
위 이미지는 생성된 공원 사물의 예시입니다.
두 기법 모두 모듈식 건물 유형 사물 세트를 생성해야 하며, 그러면 Verse가 런타임 시 스폰합니다. 이 코드는 결정론적이고, 필요에 따라서만 사물을 삭제 및 생성합니다.
형상 문법
형상 문법이라는 것을 적용하기 위해 한 카테고리의 모든 복셀이 더 큰 컨벡스 박스로 변환됩니다.
형상 문법은 간단한 규칙으로 구성되고, 각 규칙은 박스를 가져온 다음 후속 규칙을 위해 하나 이상의 하위 박스를 생성합니다.
예를 들어 어떤 규칙은 하나의 긴 박스를 복셀 1개 높이의 층 조각으로 나누는 한편, 모서리와 벽을 서로 다른 규칙에 할당할 수 있습니다. 어떤 특수 규칙은 박스와 동일한 크기와 위치로 사물을 생성할 수 있습니다.
각 규칙은 vo_base(볼륨 연산자) 클래스에서 파생되는 별도의 Verse 클래스로 정의되며, 규칙은 rs_base(규칙 세트) 파생 클래스 안에서 규칙 세트 트리로 구성됩니다.
이 방법을 사용하면 새로운 규칙의 생성을 간소화하고, 다양한 아이디어를 실험하고, 각 건물 유형에 독특한 스타일을 할당할 수 있습니다. 동일한 복셀 세트에 서로 다른 규칙을 적용하면 다양한 결과가 나옵니다.
아래는 ` vo_sizecheck` 볼륨 연산자의 단순한 예시입니다.
# Check all dimensions of a box are >= a certain size
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에 할당되어 있습니다.
# Rules for 'Building1' style
rs_building1<public> := class(rs_base):
RuleSetName<override>:string = "Building1"
SetupRules<override>(PT:piece_type_dir):void=
[...]
# Rules for one floor of the building
FloorRules := vo_cornerwallsplit:
VO_CornerLength1 := vo_corner:
VO_Corner := vo_prop:
Prop := PT.Building1_corner
이 방법으로 다양한 건물 카테고리에 새 연산자와 규칙 세트를 손쉽게 생성할 수 있습니다. 규칙 세트는 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 코드는 실시간으로 업데이트를 처리할 수 있을 정도로 빨라야 합니다. 큰 배열을 처리하면 퍼포먼스 문제로 이어질 수 있으므로, 다음 정보를 염두에 두는 것이 중요합니다.
엘리먼트를 하나씩 빌드하는 것보다
for루프를 사용해 배열을 반환하는 것이 더 빠른데, 엘리먼트가 추가될 때마다 배열이 복사되어 시간 복잡도가 O(N2)만큼 늘어나기 때문입니다. 예를 들면 다음과 같습니다.Verse* set OptionalArray := for(I := 0 .. ArraySize-1): false큰 배열을 값으로 전달하지 말고, 대신 오브젝트에 넣어 메서드를 호출하거나 오브젝트를 전달하세요.
다차원 배열은 첫 번째
[]연산자가 사본을 다음[]연산자로 전달하므로 속도가 느립니다.배열에서
.Length를 호출하면 실제로 그 순간에 배열 사본이 만들어지므로, 큰 배열의 크기를 직접 추적하는 것이 더 빠를 수 있습니다.
또한 코드의 어떤 부분에 가장 많은 시간이 소요되는지 정확히 파악하기 위해 profile 매크로를 사용하는 것도 아주 유용한 방법입니다.