Última actualización: enero de 2020. Tiempo de lectura: 10 minutos

Tres maneras geniales de diseñar tu juego con ScriptableObjects

What you will get from this page: Tips for how to keep your game code easy to change and debug by architecting it with Scriptable Objects.

These tips come from Ryan Hipple, principal engineer at Schell Games, who has advanced experience using Scriptable Objects to architect games. You can watch Ryan’s Unite talk on Scriptable Objects here; we also recommend you see Unity engineer Richard Fine’s session for a great introduction to Scriptable Objects. Thank you Ryan!

 

 

¿Qué son los ScriptableObjects (objetos programables)?

ScriptableObject is a serializable Unity class that allows you to store large quantities of shared data independent from script instances. Using ScriptableObjects makes it easier to manage changes and debugging. You can build in a level of flexible communication between the different systems in your game, so that it’s more manageable to change and adapt them throughout production, as well as reuse components.

Tres pilares del diseño de videojuegos

Usa 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 eso dificulta el reensamblado de los sistemas con configuraciones y relaciones diferentes. 
  • Crea escenas que sean cortes limpios: evita el pasaje de datos de una escena a otra. Cada vez que llegas a una escena, debería ser un corte limpio y una carga nueva. Así, tus escenas tendrán un comportamiento único que no está presente en las demás escenas, sin tener que hacer ningún tipo de rodeos. 
  • Configura los Prefabs para que funcionen de manera independiente. Tanto como sea posible, cada prefab que traigas a la escena debería autocontener su propia funcionalidad. Eso es de gran ayuda con el control del código fuente en los equipos más grandes, ya que las escenas están formadas por una lista de prefabs y cada prefab contiene su propia funcionalidad. Así, la mayor parte del código que ingreses (check-in) será en forma de prefabs, lo cual causará menos conflictos en la escena. 
  • Centra cada componente en la resolución de un solo problema. Eso permite que sea más fácil reunir múltiples componentes para construir algo nuevo.

 

Facilita la modificación y edición de las partes:

  • Haz que tu juego se base en los datos tanto como sea posible. Cuando diseñas sistemas de juegos para que sean como máquinas que procesan los datos en forma de instrucciones, puedes hacer cambios en el juego de forma eficiente, incluso mientras se está ejecutando. 
  • Si tus sistemas están configurados para ser tan modulares y basados en componentes como sea posible, será más fácil editarlos, hasta para tus artistas y diseñadores. Así, los diseñadores pueden compaginar las diferentes partes del juego sin tener que solicitar una característica explícita, en gran parte gracias a la implementación de pequeños componentes que hacen cada uno una sola cosa. De esa manera, pueden potencialmente hacer combinaciones diferentes de esos componentes para encontrar nuevos aspectos y mecánica del juego. Ryan menciona que algunas de las características más geniales de sus juegos nacieron 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 el juego durante el tiempo de ejecución, más oportunidades tendrás de encontrar valores y equilibrio. Además, si puedes devolver el estado del tiempo de ejecución al principio, tal como hacen los ScriptableObjects, eso es excelente.

 

Facilita la depuración:

Este punto realmente es un subpilar del número dos. Cuanto más modular sea tu juego, más fácil será probar cada una de sus partes. Cuanto más editable sea tu juego (es decir, cuantas más sean las características que tienen su propia vista en el Inspector), más fácil resultará depurarlo. Asegúrate de que puedas ver el estado de depuración en el Inspector y nunca consideres que una característica está completa hasta tener algún plan sobre cómo depurarla. 

Diseña tomando en cuenta las variables

Una de las cosas más simples que puedes construir con los ScriptableObjects es una variable basada en assets, autocontenida. A continuación, se muestra un ejemplo de FloatVariable, pero esto puede utilizarse con cualquier otro tipo serializable.

Todos los integrantes de tu equipo, independientemente de su nivel técnico, pueden definir una nueva variable de juego mediante la creación de un nuevo asset de FloatVariable. Cualquier MonoBehaviour o ScriptableObject puede usar una FloatVariable pública en lugar de una float pública para hace referencia a este nuevo valor compartido.

Mejor aún, si un MonoBehaviour cambió el valor de una FloatVariable, otros MonoBehaviours pueden ver ese cambio. Eso genera una capa de mensajes entre los sistemas que no necesitan establecer referencias entre sí. 

FloatVariable.cs (C#)
[CreateAssetMenu]
public class FloatVariable : ScriptableObject
{
	public float Value;
}

Ejemplo: puntos de salud del jugador

Un caso de uso de esta práctica es, por ejemplo, cómo se procesan los puntos de salud (HP) del jugador. En un juego con un solo jugador local, el HP del jugador puede ser una FloatVariable llamada PlayerHP. Cuando el jugador sufre algún daño, esto se resta de PlayerHP y, cuando el jugador se recupera, se añade a PlayerHP.

Ahora, imagina que en la escena hay un Prefab que muestra el estado de salud. Esta barra de estado supervisa la variable PlayerHP y actualiza lo que muestra en función de su contenido. Sin necesidad de cambiar nada el código, puede apuntar fácilmente a otra cosa diferente, como una variable PlayerMP. La barra de estado de salud no sabe nada sobre el jugador que está en la escena: solo lee la información de la misma variable en la que escribe el jugador.

Si nuestro código funciona de esta manera, es fácil añadir más cosas a los puntos de salud del jugador (PlayerHP). El sistema de música puede cambiar cuando el valor de PlayerHP está bajo, 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 está en ejecución y cambiar el valor de PlayerHP para hacer pruebas. 

Cuando edites el valor de una FloatVariable, se recomienda copiar los datos en un valor de tiempo de ejecución para no modificar el valor almacenado en el disco correspondiente a ScriptableObject. Si haces esto, los MonoBehaviours deberían acceder a RuntimeValue a fin de evitar que se edite el InitialValue que está guardado en el disco.

RuntimeValue.cs (C#)
[CreateAssetMenu]
public class FloatVariable : ScriptableObject, ISerializationCallbackReceiver
{
	public float InitialValue;

	[NonSerialized]
	public float RuntimeValue;

public void OnAfterDeserialize()
{
		RuntimeValue = InitialValue;
}

public void OnBeforeSerialize() { }
}

Diseña tomando en cuenta los eventos

Una de las características que Ryan prefiere construir sobre ScriptableObjects es un sistema de eventos. Las arquitecturas de eventos ayudan a modularizar tu código, dado que se envían mensajes entre los diferentes sistemas que no saben directamente de la existencia uno del otro. Esto permite que las cosas respondan a un cambio en el estado sin necesidad de una supervisión constante en un bucle de actualización.

Los siguientes ejemplos de código vienen de un sistema de eventos que consta de dos partes: un GameEvent ScriptableObject (objeto programable de evento de juego) y un GameEventListener MonoBehaviour (monocomportamiento del listener de eventos del juego). Los diseñadores pueden crear la cantidad de GameEvents que deseen en el proyecto para representar importantes mensajes que pueden enviarse. Un GameEventListener espera que ocurra un GameEvent específico y responde invocando un UnityEvent (evento de Unity, que no es un verdadero evento, sino más bien una llamada a una función serializada).

Ejemplo de código: GameEvent ScriptableObject

GameEvent ScriptableObject: 

GameEvent ScriptableObject.cs (C#)
[CreateAssetMenu]
public class GameEvent : ScriptableObject
{
	private List<GameEventListener> listeners = 
		new List<GameEventListener>();

public void Raise()
{
	for(int i = listeners.Count -1; i >= 0; i--)
listeners[i].OnEventRaised();
}

public void RegisterListener(GameEventListener listener)
{ listeners.Add(listener); }

public void UnregisterListener(GameEventListener listener)
{ listeners.Remove(listener); }
}

Ejemplo de código: GameEventListener

GameEventListener:

GameEventListener.cs (C#)
public class GameEventListener : MonoBehaviour
{
public GameEvent Event;
public UnityEvent Response;

private void OnEnable()
{ Event.RegisterListener(this); }

private void OnDisable()
{ Event.UnregisterListener(this); }

public void OnEventRaised()
{ Response.Invoke(); }
}

Sistema de eventos que maneja la muerte del jugador

Un ejemplo de esto es la forma en que se maneja la muerte del jugador en el juego. Es un punto en el que gran parte de la ejecución puede cambiar, pero puede resultar difícil determinar dónde programar toda la lógica. ¿Debería el script del jugador activar la interfaz de usuario (UI) de final de juego o un cambio en la música? ¿Deberían los enemigos revisar todos los frames para ver si el jugador sigue vivo? Un sistema de eventos nos permite evitar dependencias problemáticas como esta.

Cuando el jugador muere, el script del jugador activa el evento OnPlayerDied. El script del jugador no necesita saber qué sistemas se encargan de eso, dado que solo es un emisor de la información. La interfaz de usuario (UI) de final del juego supervisa el evento OnPlayerDied y comienza a aparecer, un script de la cámara puede supervisarlo y comenzar a hacer el fundido a negro, el sistema de música puede responder con un cambio en el sonido. Podemos hacer que cada enemigo también esté pendiente del evento OnPlayerDied y se active una animación en la que el enemigo se burla o un cambio de estado que lo hace regresar a un comportamiento en reposo.

Con este patrón, resulta muy fácil agregar nuevas respuestas a la muerte del jugador. Además, es fácil probar la respuesta a la muerte del jugador si se activa el evento (Raise) mediante un código de prueba o un botón en el Inspector.

El sistema de eventos que construyeron en Schell Games creció hasta convertirse en algo mucho más complejo y tiene características que le permiten pasar datos y tipo autogenerados. Este ejemplo fue, básicamente, el punto de inicio para lo que usan hoy en día.

Diseña tomando en cuenta otros sistemas

Los ScriptableObjects no tienen que ser solo datos. Toma cualquier sistema que implementes en un MonoBehaviour y fíjate si puedes mover la implementación a un ScriptableObject en cambio. En lugar de tener un InventoryManager (administrador de inventario) en un DontDestroyOnLoad MonoBehaviour (monocomportamiento de no destruir al cargar), intenta ponerlo en un ScriptableObject.

Dado que no está vinculado a la escena, no tiene un Transform y no obtiene las funciones de actualización, pero sí mantiene el estado entre la carga de una escena y otra sin necesidad de una inicialización especial. En lugar de un singleton, utiliza una referencia pública a tu objeto del sistema de inventario cuando necesites un script para acceder al inventario. De esta forma, será más fácil cambiar por un inventario de prueba o un inventario de tutorial que si estuvieras usando un singleton.

Aquí puedes imaginar un script del jugador que hace referencia al sistema de inventario. Cuando se crea el jugador, puede solicitar al inventario que le dé todos los objetos que le pertenecen y crear el equipo necesario. La interfaz de usuario (UI) del equipo también puede hacer referencia al inventario y recorrer todos los elementos en forma de bucle para determinar qué se debe seleccionar. 

¿Te gustó este contenido?

¡Sí!
No tanto.

Usamos cookies para brindarte la mejor experiencia en nuestro sitio web. Visita nuestra página de política de cookies si deseas más información.

Listo