ScriptableObjects

Los Objetos Scriptables son un tipo especial de objeto de datos en Unity. Tienen varios beneficios importantes, pero es posible que no funcionen de manera ideal para cada situación.

Introducción a los Objetos Scriptables

Puede pensar en un objeto programable como un objeto destinado solo a contener datos. Si ha estado utilizando clases o estructuras tradicionales de C# para objetos simples solo de datos, podría utilizarlas en su lugar. Por supuesto, usted se estará preguntando “por qué” le gustaría usar un Objeto Scriptable. Aquí hay algunos pros y contras:

Pros

  1. Pueden sobrevivir a una carga (como cuando construyes tus scripts o cuando ingresas y sales del modo de reproducción).
  2. Se guardan por referencia, mientras que las clases y estructuras normales se serializan como copias completas. Esto puede ayudarlo a evitar la duplicación de datos.
  3. Pueden manejar el polimorfismo, mientras que las clases normales terminan siendo tratadas como la clase base.
  4. Se pueden guardar como un activo del proyecto.
  5. No necesitan estar adjuntos a GameObjects.

Contras

  1. Debe heredar de ScriptableObject, lo que puede romper muchas de sus opciones arquitectónicas o de diseño.
  2. No puede crearlos usando constructores normales, sino que debe usar “CreateInstance” en su lugar.
  3. Los beneficios de serialización no son igualmente aplicables al tiempo de ejecución.

He creado varias mini demos para aclarar estos puntos. Las dos primeras demostraciones muestran cómo podría encontrarse con problemas si no estuviera usando ScriptableObjects. Las dos demos siguientes muestran cómo ScriptableObjects supera esos mismos problemas.


Demo 1

Pérdida de referencias de objeto en la serialización

Comencemos con algunos ejemplos de serialización. Comience por crear un nuevo script llamado “Demo1” y otro llamado “Demo1Data”, también agregue un script de editor llamado “Demo1Inspector”:


using UnityEngine;
public class Demo1 : MonoBehaviour 
{
  public Demo1Data dataA;
  public Demo1Data dataB;
}

Este script tendrá dos copias de la misma instancia de “Demo1Data”. Usaremos un script editor para crear y asignar sus valores.


using UnityEngine;
[System.Serializable]
public class Demo1Data 
{
  public int value;
}

Este script muestra una clase C# estándar muy simple. Se puede serializar, gracias a la etiqueta “[System.Serializable]”, pero Unity no lo manejará perfectamente, lo que se demostrará pronto.


using UnityEngine;
using UnityEditor;
[CustomEditor(typeof(Demo1))]
public class Demo1Inspector : Editor 
{
  public override void OnInspectorGUI()
  {
    DrawDefaultInspector ();
    Demo1 myTarget = (Demo1)target;
    if (GUILayout.Button ("Create Shared Data")) 
    {
      myTarget.dataA = new Demo1Data ();
      myTarget.dataB = myTarget.dataA;
    }
  }
}

Este script proporcionará un botón en el inspector de nuestro componente que creará una nueva instancia de “Demo1Data” y la asignará a ambos campos en el script “Demo1”.

IMPORTANTE
El script del editor debe agregarse a una carpeta llamada “Editor” o no funcionará correctamente

Crea una nueva escena. Agregue “Demo1” como componente a cualquier objeto de juego, como crear un nuevo objeto de juego vacío o incluso adjuntarlo a la cámara. Luego mira en el inspector. Unity creará automáticamente nuevas instancias de “Demo1Data” para ambos campos simplemente mirando el objeto en el inspector. Puede asignar cualquier valor que desee a cada uno de los campos “Value”. Si ingresa y sale del modo de reproducción, los valores incluso persistirán, hasta ahora todo bien.

Salga del modo de reproducción (si aún no lo hizo), luego use el botón “Crear Data” en el inspector. El valor para ambos campos debería volver a ‘0’ porque ambos campos ahora se refieren a la misma instancia nueva. Si modifica el campo de valor de “dataB”, debería ver el campo de valor de la actualización “dataA”. Sigue luciendo bien … al menos hasta que ingrese y salga del modo de reproducción. Pruébalo, luego modifica el valor de “dataB” una vez más. Uh oh, ¡los dos ya no están haciendo referencia al mismo objeto! Unity ha creado una copia completa del objeto original para ambos campos.

Demo 2

Pérdida de tipo de objeto en la serialización

Esta demostración mostrará cómo Unity no puede serializar correctamente el tipo de un objeto. Puede encontrar este problema con una lista polimórfica de objetos. Crea lo siguiente:


using UnityEngine;
public class Demo2 : MonoBehaviour 
{
  public Demo2Data[] dataArray;
}

Este script tendrá una matriz de objetos. Cada objeto compartirá una clase base - “Demo2Data”, pero en realidad se instanciará como una subclase.


using UnityEngine;
[System.Serializable]
public class Demo2Data 
{
  public string name;
  public override string ToString ()
  {
    return string.Format ("[{0}]", name);
  }
}
[System.Serializable]
public class Demo2NumberData : Demo2Data
{
  public int number;
  public override string ToString ()
  {
    return string.Format ("[{0}, {1}]", name, number);
  }
}
[System.Serializable]
public class Demo2BoolData : Demo2Data
{
  public bool toggle;
  public override string ToString ()
  {
    return string.Format ("[{0}, {1}]", name, toggle);
  }
}

Aquí hay tres clases, una clase base llamada “Demo2Data” y dos subclases de la misma. Tenga en cuenta que nunca instanciaremos una copia de la clase base directamente.


using UnityEngine;
using UnityEditor;

[CustomEditor(typeof(Demo2))]
public class Demo2Inspector : Editor 
{
  public override void OnInspectorGUI()
  {
    DrawDefaultInspector ();
    Demo2 myTarget = (Demo2)target;
    if (GUILayout.Button ("Crear Datos")) 
    {
      var dataA = new Demo2NumberData ();
      dataA.name = "Demo2NumberData";
      dataA.number = UnityEngine.Random.Range (1, 100);
      var dataB = new Demo2BoolData ();
      dataB.name = "Demo2BoolData";
      dataB.toggle = UnityEngine.Random.value > 0.5;
      myTarget.dataArray = new Demo2Data[] { dataA, dataB };
    }
    if (GUILayout.Button ("Imprimir Valores"))
    {
      foreach (var data in myTarget.dataArray) 
      {
        Debug.Log (data.ToString());
      }
    }
  }
}

Este script proporcionará algunos botones en el inspector de nuestro componente. El primero está etiquetado como “Crear Datos” e instanciará cada una de nuestras subclases de datos y las asignará a la matriz de datos de nuestro script. El segundo botón está etiquetado como “Imprimir Valores” y hará que cada objeto en la matriz imprima sus valores en la consola. IMPORTANTE: el script del editor debe agregarse a una carpeta “Editor” o no funcionará correctamente.

Crea una nueva escena. Agregue la “Demo2” como un componente a cualquier objeto del juego, como crear un nuevo objeto de juego vacío o incluso adjuntarlo a la cámara. Luego mira en el inspector. Unity creará automáticamente una matriz vacía de datos simplemente mirando el objeto en el inspector. Llenemos nuestro objeto con datos haciendo clic en el botón “Crear Datos”. Debería ver que la matriz contiene ahora dos objetos.

Aunque la clase de datos base y sus subclases tienen la etiqueta “[System.Serializable]”, no verá los campos agregados para los campos “número” o “alternar” de las instancias reales. Esto se debe a que Unity los trata como a la clase base, que solo conoce el “nombre” del objeto. Sin embargo, los datos todavía están allí (al menos por el momento). Haga clic en el botón “Imprimir Valores” y debería ver la descripción completa. En una de mis propias ejecuciones vi salidas como las siguientes:

[Demo2NumberData, 84]

[Demo2BoolData, False]

Se ve bien hasta ahora, ¿verdad? Bueno, veamos si puede sobrevivir a una carga. Adelante, ingrese y salga del modo de reproducción. Ahora presione el botón “Imprimir Valores” una vez más. Debería ver un resultado como este:

[Demo2NumberData]

[Demo2BoolData]

Al igual que Unity no sabía cómo mostrar los objetos correctamente, ¡tampoco sabía cómo serializarlos correctamente! ¡Ambos objetos ahora son instancias de la clase base y se pierden sus datos de subclase!

Demo 3

Las referencias a objetos Scriptable sobreviven a la serialización

Esta vez recrearemos la Demo 1, excepto que usaremos un Objeto Scriptable para nuestros datos serializados en lugar de una clase C# estándar. Crea lo siguiente:


using UnityEngine;
public class Demo3 : MonoBehaviour 
{
  public Demo3Data dataA;
  public Demo3Data dataB;
}

using UnityEngine;
[System.Serializable]
public class Demo3Data : ScriptableObject 
{
  public int value;
}

using UnityEngine;
using UnityEditor;
[CustomEditor(typeof(Demo3))]
public class Demo3Inspector : Editor 
{
  public override void OnInspectorGUI()
  {
    DrawDefaultInspector ();
    Demo3 myTarget = (Demo3)target;
    if (GUILayout.Button ("Crear Datos")) 
    {
      myTarget.dataA = ScriptableObject.CreateInstance<Demo3Data> ();
      myTarget.dataB = myTarget.dataA;
    }
  }
}

Crea una nueva escena y adjunta el script Demo3 a un objeto. A diferencia de Demo1, el script Demo3 no creará automáticamente nuevas instancias del objeto Scriptable con solo mirar el script en el inspector. Para comenzar a jugar con datos, haga clic en el botón “Crear datos compartidos”. Ahora, ambos campos muestran el objeto de datos en si mismo. Podríamos personalizar aún más el script del editor para que se vea similar a Demo1 si así lo desea, pero por ahora no es necesario. Para editar el valor del objeto compartido, haga doble clic en el objeto de datos en cualquier campo. La ventana del inspector se actualizará mostrando solo el objeto que está editando.

Ahora, para la gran prueba, ¿puede esta versión sobrevivir a una recarga de ensamblaje? Continúa e ingresa y luego sal del modo de reproducción. Intente editar el valor de cualquier objeto de datos. Luego regrese y abra el objeto a través del otro campo. ¡Debería ver que la referencia se serializó correctamente, porque tendrá el mismo valor! Unity pudo retener la referencia compartida en lugar de tener que serializar una copia completa del objeto para cada campo.

Objetos Scriptables en tiempo de ejecución

He mostrado los objetos de script utilizados tanto en el modo de edición como durante el modo de reproducción. Sin embargo, vale la pena señalar que algunos de los mayores beneficios de los objetos programables, en particular su fácil serialización, no es algo que pueda utilizar en tiempo de ejecución.

Aún puede guardar datos, como mediante el uso de JsonUtility para convertir sus objetos programables en JSON. El resultado podría guardarse de varias maneras, por ejemplo, escribiendo el valor en PlayerPrefs o escribiendo un archivo en el disco. Desafortunadamente, es probable que termine con los mismos desafíos de serialización demostrados en mis dos primeras demos. No tendrá una manera fácil de conservar referencias de objeto, ni tendrá una manera fácil de volver a crear matrices de objetos polimórficos.

Los Objetos Scriptables son excelentes pequeños contenedores de datos. Se pueden usar en tiempo de ejecución o editar e incluso se pueden guardar como recursos del proyecto. Ofrecen varios beneficios que los objetos estándar y las clases se pierden, como la correcta serialización, pero no son perfectos. Creo que su interfaz podría ser más intuitiva, y personalmente no me gusta la restricción de tener que heredar de ScriptableObject o de no poder usar un constructor estándar. En general, vale la pena dedicarles un poco de tiempo porque pueden proporcionar algunos flujos de trabajo convenientes y pueden ayudarlo a proyectar rápidamente su contenido.

GitHub
/img/ref.png
.