
Tres maneras de diseñar tu juego con ScriptableObjects
Lo que encontrarás en esta página: Consejos para que el código de tu juego sea fácil de cambiar y depurar diseñándolo con ScriptableObjects.
Estos consejos corren por cuenta de Ryan Hipple, ingeniero principal de Schell Games, quien tiene experiencia avanzada en el uso de ScriptableObjects para diseñar juegos. Puedes ver la charla Unite de Ryan sobre ScriptableObjects aquí. También te recomendamos ver la sesión del ingeniero Richard Fine de Unity para presentar ScriptableObjects.
¿Qué son los ScriptableObjects?
ScriptableObject es una clase Unity serializable que te permite almacenar grandes cantidades de datos compartidos independientes de las instancias de script. El uso de ScriptableObjects facilita la gestión de cambios y la depuración. Puedes crear un nivel de comunicación flexible entre los diferentes sistemas de tu juego para que sea más fácil administrar los cambios y las adaptaciones durante toda la producción, así como reutilizar los componentes.
Tres pilares de la ingeniería de juegos
Utiliza el diseño modular:
- Evita crear sistemas que dependan directamente unos de otros. Por ejemplo, un sistema de inventario debería poder comunicarse con otros sistemas de tu juego, pero no quieres crear una referencia estricta entre ellos, porque dificulta volver a ensamblar los sistemas en diferentes configuraciones y relaciones.
- Crea escenas con borrón y cuenta nueva: evita que existan datos transitorios entre tus escenas. Cada vez que llegas a una escena, debe ser un corte y una carga limpios. Esto te permite tener escenas que tienen un comportamiento único que no estaba presente en otras escenas, sin tener que hacer un hackeo.
- Configura los Prefabs para que funcionen por su cuenta. En la medida de lo posible, cada prefab que arrastres a una escena debe tener su funcionalidad contenida dentro. Esto ayuda mucho con el control del código fuente en los equipos más grandes, en los que las escenas son una lista de prefabs y los prefabs contienen la funcionalidad individual. De esta manera, la mayoría de tus registros se realizan a nivel de prefab, lo que genera menos conflictos en la escena.
- Enfoca cada componente en resolver un solo problema. Esto facilita el proceso de unir varios componentes para construir algo nuevo.
Facilita el cambio y la edición de partes:
- Aprovecha al máximo tu juego como un juego basado en datos. Cuando diseñas tus sistemas de juego para que sean como máquinas que procesan datos como instrucciones, puedes hacer cambios en el juego de manera eficiente, incluso mientras se está ejecutando.
- Si tus sistemas están configurados para que sean lo más modulares y estén basados en componentes posible, te resultará más fácil editarlos, incluso para tus artistas y diseñadores. Si los diseñadores logran armar las cosas en el juego sin tener que pedir una función explícita, en gran parte gracias a la implementación de componentes pequeños que hacen cada uno una sola cosa, potencialmente pueden combinar dichos componentes de diferentes maneras para encontrar nuevas mecánicas y jugabilidad. Ryan dice que algunas de las características más geniales en las que su equipo ha trabajado en sus juegos provienen de este proceso, que él llama "diseño emergente"".
- Es fundamental que tu equipo pueda hacer cambios en el juego durante el tiempo de ejecución. Cuanto más puedas cambiar tu juego en el tiempo de ejecución, más equilibrio y valores podrás encontrar. Además, si puedes restaurar el estado de tu tiempo de ejecución como lo hacen los ScriptableObjects, estás en un lugar ideal.
Facilita la depuración:
Este es realmente un subpilar de los dos primeros. Cuanto más modular sea tu juego, más fácil será probar cualquier parte de él. Cuanto más editable sea tu juego (más características tenga su propia vista Inspector), más fácil será depurarlo. Asegúrate de que puedas ver el estado de depuración en el Inspector y nunca consideres que una función está completa hasta que tengas algún plan sobre cómo depurarla.
Arquitecto para variables
Una de las cosas más simples que puedes crear con ScriptableObjects es una variable basada en assets, autocontenida. A continuación, se muestra un ejemplo de FloatVariable, pero esto también se puede serializar en cualquier otro tipo.
Todos los miembros de tu equipo, independientemente de su nivel técnico, pueden definir una nueva variable de juego mediante la creación de un nuevo asset FloatVariable. Cualquier MonoBehaviour u ScriptableObject puede usar una floatVariable pública en lugar de una float pública para hacer referencia a este nuevo valor compartido.
Mejor aún, si un MonoBehaviour cambia el valor de una FloatVariable, otros MonoBehaviours pueden ver ese cambio. Esto crea una capa de mensajería entre los sistemas que no necesitan referencias entre sí.

Ejemplo: Puntos de salud del jugador
Un ejemplo de caso de uso para esto son los puntos de salud (HP) de un jugador. En un juego con un solo jugador local, el HP del jugador puede ser una FloatVariable llamada PlayerHP. Cuando el jugador recibe daño, esto se resta de PlayerHP y, cuando el jugador se recupera, se agrega a PlayerHP.
Ahora imagina un Prefab de barra de salud en la escena. La barra de estado monitorea la variable PlayerHP para actualizar su visualización. Sin ningún cambio de código, podría apuntar fácilmente a algo diferente, como una variable PlayerMP. La barra de salud no sabe nada sobre el jugador en la escena, solo se lee de la misma variable en la que escribe el jugador.
Una vez que tenemos esta configuración, es fácil agregar más cosas para ver PlayerHP. El sistema de música puede cambiar a medida que el PlayerHP se reduce, los enemigos pueden cambiar sus patrones de ataque cuando saben que el jugador está débil, los efectos de espacio en pantalla pueden enfatizar el peligro del próximo ataque, etc. La clave aquí es que el script del jugador no envía mensajes a estos sistemas y estos sistemas no necesitan saber sobre el GameObject del jugador. También puedes ir al Inspector cuando el juego se está ejecutando y cambiar el valor de PlayerHP para probar las cosas.
Al editar el valor de una FloatVariable, puede ser una buena idea copiar tus datos en un valor de tiempo de ejecución para no cambiar el valor almacenado en el disco para el ScriptableObject. Si haces esto, los MonoBehaviours deberían acceder a RuntimeValue para evitar editar el InitialValue que se guarda en el disco.
Arquitecto para eventos
Una de las características favoritas de Ryan para compilar sobre ScriptableObjects es un sistema de eventos. Las arquitecturas de eventos ayudan a modularizar tu código al enviar mensajes entre sistemas que no saben directamente uno del otro. Permiten que las cosas respondan a un cambio de estado sin tener que supervisarlo constantemente en un bucle de actualización.
Los siguientes ejemplos de código provienen de un sistema de eventos que consta de dos partes: un GameEvent ScriptableObject y un GameEventListener MonoBehaviour. Los diseñadores pueden crear cualquier cantidad de GameEvents en el proyecto para representar mensajes importantes que se pueden enviar. Un GameEventListener espera a que se genere un GameEvent específico y responde invocando un UnityEvent (que no es un evento verdadero, sino más bien una llamada a una función serializada).
Ejemplo de código: GameEvent: ScriptableObject
GameEvent ScriptableObject:
Ejemplo de código: GameEventListener
GameEventListener:

Sistema de eventos que maneja la muerte del jugador
Un ejemplo de esto es el manejo de la muerte del jugador en un juego. Este es un punto en el que gran parte de la ejecución puede cambiar, pero puede ser difícil determinar dónde programar toda la lógica. ¿Debería el script Player activar la interfaz de usuario de Game Over o cambiar una música? ¿Deberían los enemigos revisar cada frame si el jugador sigue vivo? Un sistema de eventos nos permite evitar dependencias problemáticas como estas.
Cuando el jugador muere, el script del jugador llama a Raise (elevar) en el evento OnPlayerDied (morir en el jugador). El script Player no necesita saber qué sistemas se preocupan por él, ya que solo es una transmisión. La interfaz de usuario de Game Over está escuchando el evento OnPlayerDied y comienza a animarse, un script de cámara puede escucharlo y comenzar a hacer un fundido a negro, y un sistema de música puede responder con un cambio en la música. Podemos hacer que cada enemigo también escuche a OnPlayerDied, lo que activará una animación de burla o un cambio de estado para regresar a un comportamiento de inactividad.
Este patrón hace que sea increíblemente fácil agregar nuevas respuestas a la muerte del jugador. Además, es fácil probar la respuesta a la muerte del jugador llamando a Raise en el evento desde un código de prueba o un botón en el Inspector.
El sistema de eventos que construyeron en Schell Games se ha convertido en algo mucho más complejo y tiene funciones que permiten pasar datos y tipos de generación automática. Este ejemplo fue, básicamente, el punto de partida para lo que usan hoy en día.
Arquitecto para otros sistemas
Los objetos programables no tienen que ser solo datos. Toma cualquier sistema que implementes en un MonoBehaviour y mira si puedes mover la implementación a un ScriptableObject. En lugar de tener un InventoryManager en un DontDestroyOnLoad MonoBehaviour, intenta colocarlo en un ScriptableObject.
Dado que no está vinculado a la escena, no tiene un Transform y no obtiene las funciones Update, pero mantendrá el estado entre las cargas de la escena sin ninguna inicialización especial. En lugar de un singleton, usa una referencia pública al objeto de tu sistema de inventario cuando necesites un script para acceder al inventario. Esto facilita el intercambio en un inventario de prueba o en un inventario tutorial que si estuvieras usando un singleton.
Aquí puedes imaginar un script del jugador que hace referencia al sistema de inventario. Cuando el jugador aparece, puede pedir al inventario todos los objetos que posea y aparecer cualquier equipo. La interfaz de usuario (UI) de Equip también puede hacer referencia al inventario y recorrer los elementos en bucle para determinar qué dibujar.