La plantilla de Verse: construcción procedimental es un ejemplo de proyecto de Unreal Editor para Fortnite (UEFN), Re: Imagine London (Código de isla: 1442-4257-4418), que demuestra el sistema de construcción procedimental escrito en Verse.
El sistema de construcción procedimental está completamente implementado y creado en Verse en lugar de en Fortnite. Con este sistema, puedes tener más control sobre cómo se ensamblan las mallas en tus proyectos.
A través de Fortnite, puedes diseñar tu juego con piezas de construcción preestablecidas (suelo, techo y pared). El sistema de construcción procedimental amplía tus posibilidades para que puedas crear vóxeles con categorías que establecen sistemas subyacentes en los que puedes colocar rápidamente vóxeles adicionales a medida que el sistema coloca su malla.
Para acceder a esta plantilla en el explorador de proyectos de UEFN, ve a Ejemplos destacados > Ejemplos de juego > Verse: construcción procedimental.
El sistema de construcción procedimental funciona en dos fases.
Para la primera fase, se edita una cuadrícula de vóxeles, una cuadrícula 3D donde cada celda es un cuadro, mediante la adición o eliminación de vóxeles.
La segunda fase se ejecuta cada vez que se modifica la cuadrícula de vóxeles. Modificar la cuadrícula de vóxeles toma la información de los vóxeles y ejecuta un proceso que genera las mallas correctas en los lugares correctos.
Puedes añadir y eliminar las categorías de vóxeles de edificios y parques. Cada categoría tiene diferentes reglas sobre cómo deben aparecer las mallas. Estas reglas refieren al gráfico o árbol de operaciones que se realizará para pasar de los vóxeles colocados a las mallas reales.
Cómo utilizar la plantilla
Cada nivel dentro de tu proyecto tendrá una clase de nivel superior, que es la clase root_device. Cuando un jugador se une al juego, mediante la clase root_device se le crea un global_player_data en donde se establece su información de IU.
Cada zona de construcción contiene un dispositivo build_zone que define las dimensiones del sitio en unidades de vóxeles. La posición de este dispositivo define el origen del sitio. El área de construcción utiliza un objeto build_system para controlar la mecánica de construcción y un spawner_device para generar mallas de utilería de construcción. El área de construcción también contiene una voxel_grid, una mesh_grid y un wfc_system.
Cada área de construcción contendrá lo siguiente:
build_zone(dispositivo): define las dimensiones del sitio en unidades de vóxeles.build_system: controla la mecánica de construcción.spawner_device(dispositivo): genera mallas de utilería de construcción.spawner_asset_references(dispositivo): refiere a todas las utilerías generadas.
Cuando un jugador ingresa a una build_zone, que se activa con un dispositivo de volumen, se crea player_data para controlar la entrada y la edición de vóxeles de ese jugador.
Cómo construir con vóxeles
Esta plantilla presenta un vector3i [type]() para representar vóxeles mediante sus coordenadas X, Y, y Z [integer]().
A continuación, se muestra una secuencia de comandos de ejemplo que utiliza vector3i.
vector3i<public> := struct<computes><concrete>:
@editable
X<public>:int = 0
@editable
Y<public>:int = 0
@editable
Z<public>:int = 0Cuadrícula de vóxeles
Una cuadrícula de vóxeles es una cuadrícula 3D de celdas para cada zona de construcción que almacena información sobre el tipo de vóxel de construcción presente. Esto se implementa en la clase voxel_grid como una matriz 1D de referencias opcionales a voxel_cell[objects](), como se muestra a continuación.
# Main array representing the 3D grid of voxels
var Cells<public> : []?voxel_cell = array{}De forma predeterminada, un objeto de celda de vóxel contiene solo un [enum]() para el tipo de vóxel, como se muestra a continuación.
# 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_categoryPuedes añadir más categorías de vóxeles de construcción mediante la extensión del enum como se muestra arriba.
El siguiente código convierte una coordenada de voxel 3D en un [index]() 1D.
# 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 y ClearVoxel son funciones clave que modifican la cuadrícula de vóxeles.
Raycasting
Una vez establecida la cuadrícula de vóxeles, el sistema realiza una comprobación de colisión de rayos para comprobar la cara del vóxel o el lado del vóxel donde golpea el rayo. Cada vóxel tiene seis caras, similar a un dado. Al proyectar el rayo, necesitarás saber qué vóxel y qué cara golpeas para dibujar el resaltado y generar un nuevo vóxel contra esa cara.
Las comprobaciones de colisión de rayos se controlan principalmente en la clase ray_caster. Para hacer esto, primero se determina en qué vóxel comienza el rayo, mediante la transformación de la ubicación de la cámara en el espacio local de la cuadrícula. Luego, esto se divide por las dimensiones de vóxel, como se muestra a continuación.
CurrentVoxel := vector3i:
X := Floor[InitialPosition.X / GridSize.X]
Y := Floor[InitialPosition.Y / GridSize.Y]
Z := Floor[InitialPosition.Z / GridSize.Z]
La función [Next]() se llama repetidamente para determinar por qué vóxel pasará el rayo a continuación, y cada vez se comprueba si el vóxel se considera sólido.
Entradas del sistema de construcción
La función SelectModeTick_Trace en player_data se ejecuta para cada fotograma y controla la mayor parte de la lógica de edición de vóxeles y actualización del cursor. Se utilizan dos dispositivos [Input Trigger]() para saber cuándo se presionan los botones Disparar y Apuntar, y para establecer las variables lógicas PlacePiece y DeletePiece.
Esta función requiere lógica adicional, ya que los vóxeles de parque se consideran solo en la superficie, lo que significa que no bloquean los rayos y solo pueden existir sobre vóxeles sólidos (el suelo o los edificios). Puedes actualizar la función CategoryIsSurfaceOnly al añadir una nueva categoría que quieras que sea solo de superficie.
La construcción turbo también es posible si se mantiene presionado el botón Disparar para colocar rápidamente varios vóxeles en un plano. Esta función también comprueba si un jugador está dentro de un vóxel antes de añadirlo a la función CheckPlayerOverlapsVoxel.
Aparición de utilerías
Este proyecto de ejemplo se basa en la aparición de utilerías en [runtime](). Actualmente, creative_prop_asset no se refleja automáticamente en los archivos de manifiesto de Verse. Por lo tanto, necesitas usar un objeto proxy (una instancia de piece_type en la clase piece_type_dir) para referir a una utilería particular en Verse.
Luego, el dispositivo spawner_asset_references usa un campo @editable para cada utilería y una tabla de mapeo desde el proxy hasta el recurso real. Para añadir una nueva malla, primero debes crear una BuildingProp para ella, añadir un nuevo proxy, añadir una propiedad al dispositivo y, luego, actualizar la tabla de mapeo (como se muestra a continuación). Por último, vuelve a compilar y actualiza la nueva propiedad en el dispositivo para que apunte a la nueva utilería.
Building1_corner:piece_type := piece_type{}
@editable
BP_Building1_corner : creative_prop_asset = DefaultCreativePropAsset
PT.Building1_corner => BP_Building1_corner
Generación procedimental en Verse
Este ejemplo implementa dos tipos de generación procedimental en Verse: Gramática de formas y Contracción de la función de onda. La gramática de formas se aplica a los edificios 3D, mientras que la contracción de la función de onda se utiliza para las áreas 2D (planas).
Arriba se muestra un ejemplo de utilería de construcción generada.
Arriba se muestra un ejemplo de utilería de parque generada.
Para ambas técnicas, debes crear un conjunto de utilerías modulares de tipo construcción, que Verse genera durante el tiempo de ejecución. Este código es determinista, solo elimina y genera utilerías según sea necesario.
Gramática de forma
Todos los vóxeles de una categoría se transforman en cajas convexas más grandes para aplicar lo que se denomina “gramática de formas”.
La gramática de formas consiste en reglas simples donde cada regla toma una caja y genera una o más subcajas para las reglas subsiguientes.
Por ejemplo, una regla podría dividir una caja alta en una pieza de suelo de un vóxel de altura, mientras que las esquinas y las paredes se asignan a reglas diferentes. Una regla especial puede generar una utilería del mismo tamaño y ubicación que la caja.
Cada regla se define como una clase de Verse independiente, derivada de la clase vo_base (operador de volumen). Estas se ensamblan en un árbol de conjunto de reglas dentro de una clase derivada de rs_base (conjunto de reglas).
Este enfoque simplifica la creación de nuevas reglas, permite experimentar con diferentes ideas, asigna estilos distintos a cada tipo de edificio y habilita la asignación de estilos distintos a cada tipo de edificio. La aplicación de diferentes reglas al mismo conjunto de vóxeles produce resultados variados.
A continuación, se muestra un ejemplo sencillo de un operador de volumen ` 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)Anula la función Operate que está definida en la clase base, comprueba el tamaño de la caja entrante y decide a cuál de las siguientes reglas (vo_pass o vo_fail) llamar.
Los conjuntos de reglas que contienen muchos operadores de volumen son fáciles de definir en Verse. Puedes anular la función setupRules y declarar tus [operators]() y sus [parameters](). El punto inicial, u operador raíz, se asigna a VO_root como se muestra a continuación.
# 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
Este método facilita la creación de nuevos operadores y conjuntos de reglas para diferentes categorías de edificios. Los conjuntos de reglas se asignan en InitRuleSets y se seleccionan para una categoría particular en SelectRuleSet, ambos en build_system.
Colapso de la función de onda
El colapso de la función de onda (WFC) es una técnica para generar de forma aleatoria, un área basada en reglas que determinan cómo pueden encajar las piezas.
En esta implementación, puedes usar un conjunto de cuadros y luego especificar cuáles de ellos pueden ser adyacentes. Las etiquetas se aplican a cada borde, y solo se pueden colocar cuadros con etiquetas coincidentes.
A continuación, se muestra un ejemplo de una etiqueta de borde de la clase wfc_mode_factory.
WaterEL:wfc_edge_label := wfc_edge_label:
Name:="Water"
Symmetric := true
A continuación, se muestra un ejemplo de la definición de malla para el ejemplo anterior.
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
Puedes especificar etiquetas en el sentido de las agujas del reloj, a partir de la dirección +Y.
El borde inferior en este ejemplo es "agua a césped" porque cada borde se considera en el sentido de las agujas del reloj. Con este sistema, es fácil añadir nuevas etiquetas y mallas, o nuevos modelos WFC, a tu juego.
El algoritmo de WFC selecciona una ubicación en la cuadrícula, elige aleatoriamente o colapsa entre las opciones posibles y, luego, propaga las consecuencias de esa elección a las opciones posibles en otras ubicaciones. Este proceso continúa hasta que se genera toda la región.
La clase wfc_system contiene el estado actual de todos los cuadros. El operador de volumen especial vo_wfc lee el estado y genera las mallas correctas.
Posibles extensiones
A continuación, se muestran algunas formas en que este proyecto de ejemplo podría modificarse para la creación de nuevas experiencias.
Añade una nueva categoría de vóxeles o gramática de formas
Extiende la enum
build_categoryActualiza todas las declaraciones
caseque deban controlar la nueva categoría.Crea un nuevo conjunto de reglas derivado de
rs_baseActualiza
SelectRuleSetpara usar el nuevo conjunto de reglas para la nueva categoría
Añade nuevas mallas al modelo de la WFC del parque
Crea una utilería de construcción para cada malla nueva (como se describe en Aparición de utilerías más arriba).
Añade un nuevo
wfc_edge_label(si lo deseas) alGetParkModelenwfc_model_factory.Añade una nueva instancia de
wfc_meshpara cada nueva malla o utilería, y define la etiqueta para cada borde.Llama a
Model.AddMeshpara cada malla nueva.
Añade un nuevo modelo WFC
Añade una nueva categoría de vóxel para el nuevo modelo.
Añade una nueva función a
wfc_model_factorypara la creación del nuevo modelo.Añade un nuevo miembro del
wfc_modelabuild_system(comoPark_WFCModel).Añade una nueva función como
AddParkWFCTileque añade cuadros con el nuevo modelo.Modifica
SelectModeTick_Tracepara llamar a la nueva función para la nueva categoría.
También puedes generar vóxeles procedimentalmente. En el proyecto de ejemplo, hay botones que añaden edificios aleatorios o regiones de parque a la zona de construcción. Esto utiliza las funciones ClearRegion y AddRegion de build_system y podría usarse como punto de partida para un sistema de generación de niveles aleatorios.
Rendimiento de Verse
El código de Verse de este proyecto de ejemplo garantiza que sea lo suficientemente rápido como para procesar las actualizaciones en tiempo real. Dado que trabajar con matrices grandes puede generar problemas de rendimiento, es importante tener en cuenta la siguiente información:
El uso de un bucle
forpara devolver una matriz es más rápido que construirlo elemento por elemento, ya que cada adición copia la matriz para que sea O(N2). Por ejemplo:Verse* set OptionalArray := for(I := 0 .. ArraySize-1): falseNo pases matrices grandes por valor; en cambio, colócalas en un objeto y llama a métodos o pasa el objeto.
Las matrices multidimensionales son lentas porque el primer operador
[]pasa una copia al siguiente operador[].Llamar a
.Lengthen una matriz, en realidad, realiza una copia de la matriz en el momento, por lo que puede agilizar el seguimiento del tamaño de las matrices grandes.
También es muy útil utilizar la macro del perfil para comprender mejor qué partes del código están tomando más tiempo.