Trucos avanzados de creación de scripts para editores que te ahorrarán tiempo (parte 2)

¡Estoy de vuelta para la segunda parte! Si te perdiste la primera entrega de mis trucos avanzados para crear scripts de edición, échale un vistazo aquí. Este artículo de dos partes está diseñado para guiarlo a través de consejos avanzados del editor para mejorar los flujos de trabajo para que su próximo proyecto se desarrolle mejor que el anterior.
Cada hack se basa en un prototipo demostrativo que he creado, similar a un RTS, donde las unidades de un equipo atacan automáticamente los edificios enemigos y otras unidades. Para refrescar la memoria, aquí está el prototipo inicial:
En el artículo anterior, compartí las mejores prácticas sobre cómo importar y configurar los recursos artísticos en el proyecto. Ahora comencemos a utilizar esos recursos en el juego, mientras ahorramos tanto tiempo como sea posible.
Comencemos analizando los elementos del juego. Al configurar los elementos de un juego, a menudo nos encontramos con el siguiente escenario:
Por un lado, tenemos Prefabs que provienen del equipo de arte, ya sea un Prefab generado por el Importador FBX, o un Prefab que ha sido cuidadosamente configurado con todos los materiales y animaciones apropiados, agregando accesorios a la Jerarquía, etc. Para usar este Prefab en el juego, tiene sentido crear una variante de Prefab y agregar allí todos los componentes relacionados con el juego. De esta manera, el equipo de arte puede modificar y actualizar el Prefab, y todos los cambios se reflejan inmediatamente en el juego. Si bien este enfoque funciona si el elemento solo requiere un par de componentes con configuraciones simples, puede agregar mucho trabajo si necesita configurar algo complejo desde cero cada vez.
Por otro lado, muchos de los artículos tendrán los mismos componentes con valores similares, como todos los Prefabricados de Coches o Prefabricados para enemigos similares. Tiene sentido que todas sean variantes del mismo prefabricado base. Dicho esto, este enfoque es ideal si configurar el arte del Prefab es sencillo (es decir, configurar la malla y sus materiales).
A continuación, veamos cómo simplificar la configuración de los componentes del juego, para que podamos agregarlos rápidamente a nuestros Prefabs de arte y usarlos directamente en el juego.
La configuración más común que he visto para elementos complejos en un juego es tener un componente "principal" (como "enemigo", "recolección" o "puerta") que se comporta como una interfaz para comunicarse con el objeto, y una serie de componentes pequeños y reutilizables que implementan la funcionalidad en sí; cosas como "seleccionable", "CharacterMovement" o "UnitHealth", y componentes integrados de Unity, como renderizadores y colisionadores.
Algunos de los componentes dependen de otros componentes para funcionar. Por ejemplo, el movimiento del personaje podría necesitar un agente NavMesh. Es por eso que Unity tiene el atributo RequireComponent listo para definir todas estas dependencias. Entonces, si hay un componente “principal” para un tipo determinado de objeto, puedes usar el atributo RequireComponent para agregar todos los componentes que este tipo de objeto necesita tener.
Por ejemplo, las unidades de mi prototipo tienen estos atributos:
Tipo de bloque desconocido "codeBlock", especifique un serializador para él en la propiedad `serializers.types`
Además de establecer una ubicación fácil de encontrar en AddComponentMenu, incluya todos los componentes adicionales que necesita. En este caso, agregué la Locomoción para moverse y el Componente de Ataque para atacar a otras unidades.
Además, la unidad de clase base (que se comparte con los edificios) tiene otros atributos RequireComponent que son heredados por esta clase, como el componente Salud. Con esto solo necesito agregar el componente Soldier a un GameObject para que todos los demás componentes se agreguen automáticamente. Si agrego un nuevo atributo RequireComponent a un componente, Unity actualizará todos los GameObjects existentes con el nuevo componente, lo que facilita la extensión de los objetos existentes.
RequireComponent también tiene un beneficio más sutil: Si tenemos un “componente A” que requiere un “componente B”, entonces agregar A a un GameObject no solo garantiza que B también se agregue, sino que en realidad garantiza que B se agregue antes que A. Esto significa que cuando se llama al método Reset para el componente A, el componente B ya existirá y tendremos acceso a él fácilmente. Esto nos permite establecer referencias a los componentes, registrar UnityEventspersistentes y cualquier otra cosa que necesitemos hacer para configurar el objeto. Al combinar el atributo RequireComponent y el método Reset , podemos configurar completamente el objeto agregando un solo componente.
El principal inconveniente del método mostrado arriba es que, si decidimos cambiar un valor, necesitaremos cambiarlo manualmente para cada objeto. Y si toda la configuración se realiza mediante código, resulta difícil para los diseñadores modificarlo.
En el artículo anterior, vimos cómo usar AssetPostprocessor para agregar dependencias y modificar objetos en el momento de la importación. Ahora usemos esto para imponer algunos valores en nuestros Prefabs.
Para que a los diseñadores les resulte más fácil modificar esos valores, leeremos los valores desde un Prefab. Al hacerlo, los diseñadores pueden modificar fácilmente ese Prefab para cambiar los valores de todo el proyecto.
Si está escribiendo código de Editor, puede copiar los valores de un componente de un objeto a otro aprovechando la clase Preset .
Crea un ajuste preestablecido a partir del componente original y aplícalo a los otros componentes de la siguiente manera:
Tipo de bloque desconocido "codeBlock", especifique un serializador para él en la propiedad `serializers.types`
Tal como está, anulará todos los valores del Prefab, pero probablemente no sea eso lo que queremos que haga. En su lugar, copie sólo algunos valores, manteniendo el resto intacto. Para ello, utilice otra anulación de Preset.ApplyTo que tome una lista de las propiedades que debe aplicar. Por supuesto, podríamos crear fácilmente una lista codificada de las propiedades que queremos anular, lo que funcionaría bien para la mayoría de los proyectos, pero veamos cómo hacer que esto sea completamente genérico.
Básicamente, creé un Prefab base con todos los componentes y luego creé una Variante para usar como plantilla. Luego decidí qué valores aplicar de la lista de anulaciones en la Variante.
Para obtener las anulaciones, utilice PrefabUtility.GetPropertyModifications. Esto le proporciona todas las anulaciones en todo el Prefab, así que filtre solo las necesarias para apuntar a este componente. Algo a tener en cuenta aquí es que el objetivo de la modificación es el componente del Prefab base, no el componente de la Variante, por lo que debemos obtener la referencia a él mediante GetCorrespondingObjectFromSource:
Tipo de bloque desconocido "codeBlock", especifique un serializador para él en la propiedad `serializers.types`
Ahora esto aplicará todas las anulaciones de la plantilla a nuestros Prefabs. El único detalle que queda es que la plantilla podría ser una variante de una variante, y querremos aplicar las anulaciones de esa variante también.
Para ello solo necesitamos hacer esto recursivamente:
Tipo de bloque desconocido "codeBlock", especifique un serializador para él en la propiedad `serializers.types`
A continuación, busquemos la plantilla para nuestros Prefabs. Lo ideal sería utilizar diferentes plantillas para diferentes tipos de objetos. Una forma eficiente de hacer esto es colocar las plantillas en la misma carpeta que los objetos a los que queremos aplicarlas.
Busque un objeto llamado Template.prefab en la misma carpeta que nuestro Prefab. Si no lo encontramos, buscaremos en la carpeta padre recursivamente:
Tipo de bloque desconocido "codeBlock", especifique un serializador para él en la propiedad `serializers.types`
En este punto, tenemos la posibilidad de modificar la plantilla Prefab, y todos los cambios se reflejarán en los Prefabs de esa carpeta, aunque no sean Variantes de la plantilla. En este ejemplo, cambié el color predeterminado del jugador (el color utilizado cuando la unidad no está asociada a ningún jugador). Observe cómo actualiza todos los objetos:
Al equilibrar los juegos, todas las estadísticas que necesitarás ajustar se distribuyen en varios componentes y se almacenan en un Prefab o ScriptableObject para cada personaje. Esto hace que el proceso de ajuste de detalles sea bastante lento.
Una forma común de facilitar el equilibrio es mediante el uso de hojas de cálculo. Pueden ser muy útiles ya que reúnen todos los datos y puedes usar fórmulas para calcular automáticamente algunos de los datos adicionales. Pero introducir estos datos en Unity manualmente puede ser un proceso muy largo.
Ahí es donde entran en juego las hojas de cálculo. Se pueden exportar a formatos simples como CSV(.csv) o TSV(.tsv), que es exactamente para lo que sirven los ScriptedImporters . A continuación se muestra una captura de pantalla de las estadísticas de las unidades en el prototipo:

El código para esto es bastante simple: Crea un ScriptableObject con todas las estadísticas de una unidad y luego podrás leer el archivo. Para cada fila de la tabla, cree una instancia de ScriptableObject y rellénela con los datos de esa fila.
Por último, agregue todos los ScriptableObjects al activo importado utilizando el contexto. También necesitamos agregar un activo principal, que acabo de configurar como un TextAsset vacío (ya que realmente no usamos el activo principal para nada aquí).
Esto funciona tanto para edificios como para unidades, pero debes verificar cuál estás importando, ya que las unidades tendrán muchas más estadísticas.
Tipo de bloque desconocido "codeBlock", especifique un serializador para él en la propiedad `serializers.types`
Una vez completado esto, ahora hay algunos ScriptableObjects que contienen todos los datos de la hoja de cálculo.

Los ScriptableObjects generados están listos para usarse en el juego según sea necesario. También puedes utilizar el PrefabPostprocessor que se configuró anteriormente.
En el método OnPostprocessPrefab , tenemos la capacidad de cargar este activo y utilizar sus datos para llenar los parámetros de los componentes automáticamente. Más aún, si establece una dependencia de este activo de datos, los Prefabs se volverán a importar cada vez que modifique los datos, manteniendo todo actualizado automáticamente.
Al intentar crear niveles increíbles, es fundamental poder cambiar y probar cosas rápidamente, realizar pequeños ajustes y volver a intentarlo. Por eso es tan importante contar con tiempos de iteración rápidos y reducir los pasos necesarios para comenzar a realizar pruebas.
Una de las primeras cosas que tenemos en cuenta cuando se trata de tiempos de iteración en Unity es la recarga de dominio. La recarga de dominio es relevante en dos situaciones clave: después de compilar el código para cargar las nuevas bibliotecas vinculadas dinámicamente (DLL) y al entrar y salir del modo de reproducción. La recarga de dominio que viene con la compilación no se puede evitar, pero tienes la opción de deshabilitar las recargas relacionadas con el Modo de reproducción en Configuración del proyecto > Editor > Ingresar a la configuración del modo de reproducción.
Deshabilitar la recarga de dominio al ingresar al modo de juego puede causar algunos problemas si su código no está preparado para ello; el problema más común es que las variables estáticas no se restablecen después de jugar. Si su código puede funcionar con esta opción deshabilitada, hágalo. Para este prototipo, la recarga de dominio está deshabilitada, por lo que puedes ingresar al modo de juego casi instantáneamente.
Un problema aparte con los tiempos de iteración tiene que ver con el recálculo de los datos necesarios para jugar. A menudo, esto implica seleccionar algunos componentes y hacer clic en botones para activar los recálculos. Por ejemplo, en este prototipo, hay un TeamController para cada equipo dentro de la escena. Este controlador tiene una lista de todos los edificios enemigos para poder enviar unidades a atacarlos. Para completar estos datos automáticamente, utilice la interfaz IProcessSceneWithReport . Esta interfaz se llama para las escenas en dos ocasiones diferentes: durante las compilaciones y al cargar una escena en el modo de reproducción. Con ello viene la oportunidad de crear, destruir y modificar cualquier objeto que desees. Sin embargo, tenga en cuenta que estos cambios solo afectarán las compilaciones y el modo de juego.
Es en esta devolución de llamada donde se crean los controladores y se establece la lista de edificios. Gracias a esto no es necesario hacer nada manualmente. Los controladores con una lista actualizada de edificios estarán allí cuando comience el juego, y la lista se actualizará con los cambios que hayamos realizado.
Para el prototipo, se configuró un método de utilidad que permite obtener todas las instancias de un componente en una escena. Puedes usar esto para obtener todos los edificios:
Tipo de bloque desconocido "codeBlock", especifique un serializador para él en la propiedad `serializers.types`
El resto del proceso es bastante trivial: Consigue todos los edificios, consigue todos los equipos a los que pertenecen los edificios y crea un controlador para cada equipo con una lista de edificios enemigos.
Tipo de bloque desconocido "codeBlock", especifique un serializador para él en la propiedad `serializers.types`
Además de la escena que se está editando, también es necesario cargar otras escenas para poder jugar (es decir, una escena con los administradores, con la interfaz de usuario, etc.) Esto puede llevar un tiempo valioso. En el caso del prototipo, el Canvas con las barras de salud está en una escena diferente llamada InGameUI.
Una forma eficaz de trabajar con esto es agregar un componente a la escena con una lista de las escenas que deben cargarse junto con él. Si carga esas escenas sincrónicamente en el método Awake , la escena se cargará y todos sus métodos Awake se invocarán en ese punto. Entonces, cuando se llama al método Start , puede estar seguro de que todas las escenas están cargadas e inicializadas, lo que le da acceso a los datos que contienen, como los singletons del administrador.
Recuerda que es posible que tengas algunas escenas abiertas cuando ingreses al Modo de reproducción, por lo que es importante verificar si la escena ya está cargada antes de cargarla:
Tipo de bloque desconocido "codeBlock", especifique un serializador para él en la propiedad `serializers.types`
A lo largo de las partes uno y dos de este artículo, le he mostrado cómo aprovechar algunas de las características menos conocidas que Unity tiene para ofrecer. Todo lo descrito es sólo una fracción de lo que se puede hacer, pero espero que estos trucos te resulten útiles para tu próximo proyecto o, al menos, interesantes.
Los activos utilizados para crear el prototipo se pueden encontrar de forma gratuita en Asset Store:
- Esqueletos: Unidades de estrategia en tiempo real de dibujos animados: demostración de no muertos
- Caballeros: Unidades de estrategia en tiempo real de dibujos animados: demostración
- Torres: Impresionante torre de mago estilizada
Si deseas discutir este artículo de dos partes o compartir tus ideas después de leerlo, dirígete a nuestro foro de programación. Me despido por ahora, pero aún puedes conectarte conmigo en Twitter en @CaballolD. Asegúrese de estar atento a los futuros blogs técnicos de otros desarrolladores de Unity como parte del trabajo en curso. SerieTecnología de las Trincheras.