La plantilla Verse - Construcción procedimental es un proyecto de ejemplo de Reinventa Londres de Unreal Editor para Fortnite (UEFN) (código de isla: 1442-4257-4418) que muestra 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. Gracias a este sistema, puedes tener más control sobre cómo se componen las mallas en tus proyectos.
Con 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 desde el explorador de proyectos de UEFN, ve a Ejemplos de funciones > Ejemplos de juego > Verse - Construcción procedimental.
El sistema de construcción procedimental funciona en dos fases.
En la primera fase, se edita una cuadrícula de vóxeles, una cuadrícula 3D en la que cada celda es una caja, añadiendo o eliminando vóxeles.
La segunda fase se ejecuta cada vez que se modifica la cuadrícula de vóxeles. La modificación de la cuadrícula de vóxeles toma la información de los vóxeles y ejecuta un proceso que genera las mallas adecuadas en los lugares correctos.
Puedes añadir y eliminar las categorías de vóxeles Construcción y Parque. Cada categoría tiene diferentes reglas sobre cómo deben generarse las mallas. Estas reglas se refieren al grafo o árbol de operaciones que se realizarán para pasar de los vóxeles colocados a las mallas reales.
Cómo usar la plantilla
Cada nivel de tu proyecto tendrá una clase de nivel superior, que es la clase root_device. Cuando un jugador se une a la partida, la clase root_device crea un global_player_data para él que establece su información de IU.
Cada zona de construcción tiene un dispositivo build_zone que define las dimensiones del sitio en unidades de vóxel. La posición de este dispositivo define el origen de la zona. La zona de construcción utiliza un objeto build_system para controlar la mecánica de construcción y un spawner_device para generar mallas de elementos de construcción. La zona de construcción también contiene voxel_grid, mesh_grid y wfc_system.
Cada zona de construcción contendrá lo siguiente:
build_zone(dispositivo): define las dimensiones del sitio en unidades de vóxel.build_system: se encarga de la mecánica de construcción.spawner_device(dispositivo): genera mallas de elementos de construcción.spawner_asset_references(dispositivo): hace referencia a todos los elementos generados.
Cuando un jugador entra en una build_zone, que se activa con un dispositivo Volumen, se crea player_data para gestionar la entrada de ese jugador y la edición de vóxeles.
Construcción con vóxeles
Esta plantilla implementa un vector3i [tipo]() para representar los vóxeles por sus coordenadas [integer]() X, Y y Z.
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ículas 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 unidimensional de referencias opcionales a voxel_cell [objetos](), tal y como se muestra a continuación.
# Main array representing the 3D grid of voxels
var Cells<public> : []?voxel_cell = array{}Por defecto, un objeto de celda de vóxel contiene solo un [enum]() para el tipo de vóxel, tal y 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 edificios ampliando el enumerador como se muestra arriba.
El código siguiente convierte una coordenada 3D de vóxel en un [index]() unidimensional.
# 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.
Emisión de rayos
Una vez establecida la cuadrícula de vóxeles, el sistema realiza una comprobación de colisión de rayos para comprobar la cara o el lado del vóxel con el que choca el rayo. Cada vóxel tiene seis caras, de forma similar a un dado. Al proyectar rayos, tendrás que saber qué vóxel y qué cara impactas para dibujar el resaltado y generar un nuevo vóxel contra esa cara.
Las comprobaciones de colisión de rayos se gestionan principalmente en la clase ray_caster. Esto se hace determinando primero en qué vóxel empieza el rayo, transformando la ubicación de la cámara en el espacio local de la cuadrícula. A continuación, 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á a continuación el rayo, comprobando cada vez si el vóxel se considera sólido.
Entradas del sistema de construcción
La función SelectModeTick_Trace de player_data se ejecuta para cada fotograma y gestiona 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 pulsan los botones Disparar y Apuntar, y para establecer las variables lógicas PlacePiece y DeletePiece.
Esta función requiere una lógica adicional, ya que los vóxeles de parque se consideran solo de superficie, lo que significa que no bloquean los rayos y solo pueden encontrarse 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 compatible si se mantiene pulsado 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.
Generación de elementos
Este proyecto de ejemplo se basa en la generación de elementos en [runtime](). creative_prop_asset actualmente no se refleja automáticamente en los archivos de manifiesto de Verse. Por tanto, es necesario utilizar un objeto proxy (una instancia de piece_type en la clase piece_type_dir) para hacer referencia a elementos concretos en Verse.
A continuación, el dispositivo spawner_asset_references utiliza un campo @editable para cada elemento y una tabla de asignación del proxy al recurso real. Para añadir una nueva malla, primero debes crear un BuildingProp para ella, añadir un nuevo proxy, una propiedad al dispositivo y, a continuación, 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 al nuevo elemento.
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 colapso de la función de onda. La gramática de formas se aplica a los edificios 3D, mientras que el colapso de función de onda se utiliza para las zonas 2D (planas).
Arriba tienes un ejemplo de elementos de construcción generados.
Arriba tienes un ejemplo de elementos de parque generados.
Para ambas técnicas, debes crear un conjunto de elementos modulares de tipo construcción, que Verse genera en tiempo de ejecución. Este código es determinista, solo elimina y genera elementos cuando es necesario.
Gramática de formas
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 consta de reglas simples en las que cada regla toma una caja y genera una o más subcajas para reglas posteriores.
Por ejemplo, una regla puede 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 un elemento 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 conjuntos 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 construcción y permite la asignación de estilos distintos a cada tipo de construcción. Aplicar reglas diferentes 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 definida en la clase bass, 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 configurar 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, tal y 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 concreta en SelectRuleSet, ambos en build_system.
Colapso de la función de onda
El colapso de la función de onda (WFC, por sus siglas en inglés) es una técnica para generar aleatoriamente un área basada en reglas que determinan cómo pueden encajar las piezas.
En esta implementación, puedes utilizar un conjunto de teselas y luego especificar cuáles de ellas pueden ser adyacentes. Se aplican etiquetas a cada borde y solo se pueden colocar teselas con etiquetas coincidentes.
A continuación se muestra un ejemplo de 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, empezando por la dirección +Y.
La parte inferior de este ejemplo es «de agua a hierba» 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 de colapso de la función de onda, a tu juego.
El algoritmo de colapso de la función de onda selecciona una ubicación en la cuadrícula, elige aleatoriamente o contrae entre las opciones posibles y, a continuación, 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 todas las teselas. El operador de volumen especial vo_wfc lee el estado y genera las mallas correctas.
Posibles extensiones
A continuación te mostramos algunas formas en las que este proyecto de ejemplo podría modificarse para crear nuevas experiencias.
Añadir una nueva categoría de vóxel/gramática de formas
Amplía el enumerador
build_categoryActualiza todas las declaraciones
caseque deban gestionar la nueva categoría.Cree un nuevo conjunto de reglas derivado de
rs_baseActualiza
SelectRuleSetpara utilizar el nuevo conjunto de reglas de la nueva categoría.
Añadir nuevas mallas al modelo de colapso de la función de onda del parque
Crea un elemento de construcción para cada nueva malla (tal y como se describe en la sección Generación de elementos (más arriba).
Añade una nueva
wfc_edge_label(si quieres) alGetParkModelenwfc_model_factory.Añade una nueva instancia
wfc_meshpara cada nueva malla/elemento y define la etiqueta de cada borde.Llama a
Model.AddMeshpara cada nueva malla.
Añadir un nuevo modelo de colapso de la función de onda
Añade una nueva categoría de vóxeles para el nuevo modelo.
Añade una nueva función a
wfc_model_factorypara crear el nuevo modelo.Añade un nuevo miembro
wfc_modelabuild_system(comoPark_WFCModel).Agrega una nueva función como
AddParkWFCTileque añada teselas utilizando el nuevo modelo.Modifica
SelectModeTick_Tracepara llamar a la nueva función de la nueva categoría.
También puedes generar vóxeles de forma procedimental. En el proyecto de ejemplo, hay botones que añaden regiones de construcción o parques aleatoriamente a la zona de construcción. Utiliza las funciones ClearRegion y AddRegion de build_system y podría utilizarse como punto de partida para un sistema de generación aleatoria de niveles.
Rendimiento en Verse
El código de Verse de este proyecto de ejemplo garantiza que el código sea lo suficientemente rápido como para procesar las actualizaciones en tiempo real. Dado que trabajar con matrices grandes puede provocar problemas de rendimiento, es importante tener en cuenta la siguiente información:
Utilizar un bucle
forpara devolver una matriz es más rápido que construirla 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 vez de ello, 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 hace una copia de la matriz en ese momento, por lo que puede ser más rápido realizar un seguimiento del tamaño de las matrices grandes.
Asimismo, resulta bastante útil utilizar la macro profile para comprender exactamente qué partes del código tardan más tiempo.