
Separe los datos del juego y la lógica con ScriptableObjects
Esta página explica cómo usar ScriptableObjects como contenedores de datos que separan los datos de la lógica en el código de su juego.
Este es el segundo de una serie de seis mini-guías creadas para ayudar a los desarrolladores de Unity con la demostración que acompaña al libro electrónico, Crear una arquitectura de juego modular en Unity con ScriptableObjects.
La demostración está inspirada en la mecánica clásica de juegos de arcade de bola y paleta, y muestra cómo los ScriptableObjects pueden ayudarle a crear componentes que sean probables, escalables y amigables para los diseñadores.
Juntos, el libro electrónico, el proyecto de demostración y estas mini-guías proporcionan las mejores prácticas para usar patrones de diseño de programación con la clase ScriptableObject en su proyecto de Unity. Estos consejos pueden ayudarle a simplificar su código, reducir el uso de memoria y promover la reutilización del código.
Esta serie incluye los siguientes artículos:
- Comience con la demostración de ScriptableObjects de Unity
- Use enums basados en ScriptableObject en su proyecto de Unity
- Use ScriptableObjects como objetos delegados
- Use ScriptableObjects como canales de eventos en el código del juego
- Cómo usar un conjunto en tiempo de ejecución basado en ScriptableObject
Nota importante antes de comenzar
Antes de sumergirte en el proyecto de demostración de ScriptableObject y esta serie de mini-guías, recuerda que, en su esencia, los patrones de diseño son solo ideas. No se aplicarán a cada situación. Estas técnicas pueden ayudarte a aprender nuevas formas de trabajar con Unity y ScriptableObjects.
Cada patrón tiene pros y contras. Elige solo aquellos que beneficien de manera significativa tu proyecto específico. ¿Tus diseñadores dependen en gran medida del Editor de Unity? Un patrón basado en ScriptableObject podría ser una buena opción para ayudarles a colaborar con tus desarrolladores.
En última instancia, la mejor arquitectura de código es la que se adapta a tu proyecto y equipo.

Contenedores de datos
Los desarrolladores de software a menudo se preocupan por la modularidad: descomponer una aplicación en unidades más pequeñas y autónomas. Cada módulo se vuelve responsable de un aspecto específico de la funcionalidad de la aplicación.
En Unity, los ScriptableObjects pueden ayudar con la separación de datos de la lógica.
Los ScriptableObjects son excelentes para almacenar datos, especialmente cuando son estáticos. Esto los hace ideales para estadísticas de juegos, valores de configuración para objetos o NPCs, diálogos de personajes y mucho más.
Aislar los datos de juego de la lógica de comportamiento puede hacer que cada parte independiente del proyecto sea más fácil de probar y mantener. Esta "separación de preocupaciones" puede reducir efectos secundarios no deseados e indeseados a medida que realizas cambios necesarios.

Flujo de trabajo general
Si deseas un repaso sobre el flujo de trabajo de ScriptableObject, este artículo de Unity Learn puede ayudar. De lo contrario, aquí tienes una explicación rápida:
Definir un ScriptableObject: Para crear uno, define una clase C# que herede de la clase base ScriptableObject con campos y propiedades para los datos que deseas almacenar. Los ScriptableObjects pueden almacenar los mismos tipos de datos disponibles para los MonoBehaviours, lo que los convierte en contenedores de datos versátiles. Agrega el Atributo CreateAssetMenu desde el Editor para facilitar la creación del activo en el proyecto.
Crear un activo: Una vez que hayas definido una clase ScriptableObject, puedes crear una instancia de ese ScriptableObject en el proyecto. Esto aparece como un activo guardado en el disco que puedes reutilizar en diferentes GameObjects y escenas.
Establecer valores: Después de crear el activo, pópulate con datos estableciendo los valores de sus campos y propiedades en el Inspector.
Usar el activo: Una vez que el activo contiene datos, haz referencia a él desde una variable o campo. Cualquier cambio realizado en el activo ScriptableObject se reflejará en todo el proyecto.
Puedes reutilizar los ScriptableObjects como contenedores de datos en diferentes partes de tu juego. Por ejemplo, puedes definir las propiedades de un arma o un personaje dentro de un ScriptableObject, y luego hacer referencia a ese activo desde cualquier parte del proyecto.
Nota: También puedes generar ScriptableObjects en tiempo de ejecución a través del método CreateInstance. Para el almacenamiento de datos, sin embargo, comúnmente crearás los activos ScriptableObject por adelantado utilizando el Atributo CreateAssetMenu.

ScriptableObjects versus MonoBehaviours
Para entender mejor por qué los ScriptableObjects son una opción más adecuada para el almacenamiento de datos que los MonoBehaviours, compara las versiones vacías de cada uno. Asegúrate de establecer tu Serialización de Activos en Modo: Forzar Texto en la Configuración del Proyecto para ver el marcado YAML como texto.
Crea un nuevo GameObject con un MonoBehaviour vacío. Luego, compáralo con un activo ScriptableObject vacío. Colocándolos uno al lado del otro, deberían verse como la comparación mostrada en la imagen de arriba.
Los ScriptableObjects son más ligeros en comparación con los MonoBehaviours y no llevan la sobrecarga asociada con estos últimos, como el componente Transform. Esto le da a los ScriptableObjects una huella de memoria más pequeña y los hace más optimizados para el almacenamiento de datos.

Demostración de patrones
Los ScriptableObjects se guardan como activos, por lo que persisten fuera del modo de Juego, lo que puede ser útil. Por ejemplo, los datos de ScriptableObject están disponibles desde cualquier lugar, incluso si cargas una nueva escena.
El ejemplo de demostración de Patrones presenta una pantalla de créditos básica que puedes probar tú mismo. Modifica el ScriptableObject Credits_Data y luego presiona Actualizar para ver el texto almacenado aparecer.
Si tuvieras un RPG con una cantidad extensa de diálogos o una escena de tutorial con un guion predefinido, esta es una forma común de almacenar muchos datos.
Mientras que los datos dentro del ScriptableObject se actualizan instantáneamente cuando se modifican, nuestro proyecto requiere un botón de Actualizar para refrescar la pantalla manualmente. La pantalla basada en el UI Toolkit se construye solo una vez y necesita ser notificada cuando los datos han sido alterados.
Crea un evento dentro del ScriptableObject si deseas sincronizar actualizaciones automáticamente. Por ejemplo, este script ExampleSO llamaría al evento OnValueChanged cada vez que ExampleValue cambia. Mira el ejemplo de código a continuación.
Luego, haga que su objeto de interfaz de usuario de escucha se suscriba a OnValueChanged y se actualice en consecuencia.

Almacenando datos del juego con el patrón flyweight
Los ScriptableObjects brillan cuando muchos objetos comparten los mismos datos. Por ejemplo, si estuvieras construyendo un juego de estrategia donde numerosas unidades tienen la misma velocidad de ataque y salud máxima, es ineficiente almacenar esos valores individualmente en cada GameObject.
En su lugar, puedes consolidar los datos compartidos en un lugar central y hacer que cada objeto haga referencia a esa ubicación compartida. En el diseño de software, esto es una optimización conocida como el patrón flyweight. Reestructurar tu código de esta manera evita copiar muchos valores y reduce tu huella de memoria.
En PaddleBallSO, el ScriptableObject GameDataSO actúa como almacenamiento de datos compartidos.
En lugar de mantener una copia separada de configuraciones comunes (velocidad, masa, rebote físico, etc.), los scripts de Paddle y Ball hacen referencia a la misma instancia de GameDataSO cuando es posible. Cada elemento del juego mantiene datos únicos como posiciones y eventos de entrada, pero por defecto utiliza datos compartidos cuando es posible.
Aunque los ahorros de memoria pueden no ser notables con solo dos o tres objetos, editar datos compartidos es más rápido y menos propenso a errores que editar cada uno manualmente.
Por ejemplo, si necesitas modificar la velocidad de la paleta, ajustarla en una sola ubicación actualiza ambas palas en cada escena. Si los almacenaras como campos únicos en los MonoBehaviours, un clic incorrecto podría fácilmente desincronizar dos valores.
Descargar datos en ScriptableObjects también puede ayudar con el control de versiones y prevenir conflictos de fusión cuando los compañeros de equipo trabajan en la misma escena o Prefab.

Datos del juego PaddleBallSO
El GameDataSO muestra cómo usar un ScriptableObject como contenedor de datos. En PaddleBallSO, esto incluye varias configuraciones para configurar la jugabilidad:
- Datos de la paleta: Atributos como la velocidad de la paleta, la resistencia y la masa determinan el movimiento y la física de las palas durante la jugabilidad.
- Datos de la pelota: Esto contiene la velocidad actual de la pelota, la velocidad máxima y el multiplicador de rebote, que controla el comportamiento de la pelota cuando interactúa con una simulación.
- Datos del partido: GameDataSO contiene información sobre los retrasos entre puntos durante un partido, ayudando a controlar el ritmo del juego.
- IDs de jugadores: PlayerIDSO ScriptableObjects funcionan como una identificación de equipo para cada jugador (por ejemplo, Jugador1 y Jugador2).
- Sprites de jugadores: Estos sprites opcionales permiten la personalización del avatar del jugador.
- Diseño del nivel: El objeto LevelLayoutSO define las posiciones iniciales para los jugadores y elementos del juego como goles y paredes.
Con estas configuraciones y datos todos en un lugar central, GameDataSO permite que cualquier objeto acceda a estos datos compartidos. Esto simplifica cómo gestionas esos objetos y promueve una mayor consistencia en todo tu proyecto. ¿Cambiar la física de la paleta? Haz un cambio aquí en lugar de ajustar varios scripts.

Ejemplo de serialización dual: Diseños de niveles
A veces, puedes tener tu pastel y comértelo también. Con la serialización dual, puedes almacenar datos en un ScriptableObject mientras lo mantienes simultáneamente en otro formato.
El script LevelLayoutSO demuestra este concepto. Además de contener las posiciones iniciales para las paletas y la pelota, almacena datos de transformación para las paredes y los goles en una estructura personalizada.
Estos valores se pueden escribir en disco a través del método ExportToJson. Los archivos JSON son texto legible por humanos, lo que permite una modificación sencilla fuera de Unity. Esto te permite trabajar con ScriptableObjects en el Editor y luego almacenar sus datos en otra ubicación, como un archivo JSON o XML.
Los formatos de archivo como JSON y XML pueden ser desafiantes de trabajar en el Editor, pero son fáciles de modificar fuera de Unity en un editor de texto. Esto abre la posibilidad de niveles personalizados o modificados por el usuario.
El script GameSetup puede usar un ScriptableObject LevelLayout o un archivo JSON externo para generar el nivel del juego.
Para cargar un nivel modificado personalizado, el script de configuración genera un ScriptableObject en tiempo de ejecución con CreateInstance. Luego, lee el texto del archivo JSON para poblar el ScriptableObject.
Tus datos personalizados reemplazan el contenido del ScriptableObject y te permiten usar este nivel modificado externamente como cualquier otro. El resto de la aplicación funciona normalmente, sin darse cuenta del cambio.

Otros usos de los contenedores de datos ScriptableObject
Aunque nuestro mini-juego de pelota con paleta no puede demostrar todos los casos de uso para contenedores de datos ScriptableObject, considera lo siguiente para tus propias aplicaciones:
- Configuración del juego: Piensa en constantes, reglas del juego o cualquier otro parámetro de configuración que no necesite cambiar durante el juego. Otros componentes pueden referirse a estos datos de configuración sin usar valores codificados.
- Atributos de personajes y enemigos: Usa ScriptableObjects para definir atributos como salud, poder de ataque, velocidad, etc. Esto permite a tus diseñadores equilibrar y ajustar elementos del juego sin un desarrollador.
- Sistemas de inventario y de objetos: Las definiciones y propiedades de los objetos como nombres, descripciones e íconos son perfectas para ScriptableObjects. También puedes usarlos como parte de un sistema de gestión de inventario para rastrear los objetos que el jugador recoge, usa o equipa.
- Diálogo y sistemas narrativos: Los ScriptableObjects pueden almacenar texto de diálogo, nombres de personajes, caminos de diálogo ramificados y otros datos relacionados con la narrativa. Pueden sentar las bases para sistemas de diálogo complejos.
- Datos de nivel y progresión: Puedes usar ScriptableObjects para definir diseños de niveles, puntos de aparición de enemigos, objetivos y otra información relacionada con el nivel.
- Clips de audio: Como se ve en el proyecto PaddleBallSO, los ScriptableObjects pueden almacenar uno o más clips de audio. Estos pueden definir efectos de audio o música en múltiples partes de tu juego.
- Clips de animación: Los ScriptableObjects se pueden usar para almacenar clips de animación, lo cual es útil para definir animaciones comunes que se comparten entre múltiples GameObjects o personajes.
A medida que profundices en los ScriptableObjects y los adaptes a tus propios proyectos, descubrirás aún más aplicaciones para ellos. Son especialmente útiles para gestionar datos y facilitan el mantenimiento de la consistencia en varios elementos del juego.

Más recursos de ScriptableObject
Lee más sobre patrones de diseño con ScriptableObjects en el e-book Crea una arquitectura de juego modular en Unity con ScriptableObjects. También puedes encontrar más información sobre patrones de diseño comunes en el desarrollo de Unity en Mejora tu código con patrones de programación de juegos.