sábado, 28 de mayo de 2016

Serialización

La serialización, según wikipedia, "consiste en un proceso de codificación de un objeto en un medio de almacenamiento (como puede ser un archivo, o un buffer de memoria) con el fin de transmitirlo a través de una conexión en red como una serie de bytes o en un formato humanamente más legible como XML o JSON, entre otros."

Esto en palabras más sencillas es la manera de guardar los datos de nuestro juego en formatos universales que pueden ser guardados en el disco duro, ser usados por otros programas etc.


Empezaremos por analizar las distintas formas que Unity tiene para serializar y luego haremos unos ejemplos prácticos guardando el estado de objetos en la escena de Unity.

Existen muchas formas de serializar datos en Unity, explicaré las más comunes y las que he usado con más frecuencia, entre las opciones tenemos, playerpref, scriptableObjects, xml, json, binario, todos estos métodos no son intercambiables, más bien cada uno cumple su propia función y muchas veces se usan varios métodos al tiempo, explicaré uno por uno.

Formas de serializar exclusivas de Unity


PlayerPrefs

Abreviatura de "Player Preferences" o preferencias de usuario, como su nombre lo indica está diseñado para guardar datos como el nivel de volumen, el lenguaje, y otras cosas que queremos que se mantengan entre secciones pero ningún dato importante para un juego como por ejemplo el dinero, debido a que los usuarios podrán modificar fácilmente estos datos.

Su uso es muy sencillo, pueden guardar int, floats y strings, en vez de reinventar la rueda aquí está el link a la documentación de Unity donde está muy claro.


ScriptableObject


Los scriptableObjects son assets que podemos guardar en nuestro proyecto, son mayormente usados para guardar datos, por ejemplo, una base de datos de items, armas, personajes, etc.

Para crear un scriptableObject se requieren 2 pasos, una clase que herede de ScriptableObject y agregarlo al menu para crearlo fácilmente o también hacer un pequeño script de editor que creará nuestro asset en el proyecto,
Paso 1, Clase que herede de scriptableObject :

using UnityEngine;
using System.Collections;

public class TestScriptableObject : ScriptableObject {


}

Podemos borrar los metodos Start y Update, los ScriptableObjects no hacen uso de ellos

Paso 2, Hacer que aparezca en el menú para crearlo fácilmente sin uso de script de editor

using UnityEngine;
using System.Collections;

[CreateAssetMenu(fileName = "Test", menuName = "Custom/ScriptableObject")]
public class TestScriptableObject : ScriptableObject {


}

Con el atributo [CreateAssetMenu()] podemos hacer que en el menú nos aparezca la opción de crear nuestro scriptableObject, "filename" es el nombre por defecto que le queremos dar a nuestro scriptableObject y "menuName" son los submenú en donde aparecerá, será más fácil de entender con una imagen :


Al seleccionar la opción nos creará nuestro scriptableObject y le pondrá el nombre por defecto que agregamos en código pero lo podremos cambiar :




La otra opción para crearlo es creando un sencillo script de editor, recuerden que los scripts de editor deben estar dentro de una carpeta llamada "Editor" si no hacemos esto, recibiremos un error cuando queramos compilar nuestro proyecto, aquí el script :

using UnityEngine;
using System.Collections;
using UnityEditor;

public class CreateScriptableObject
{
    //Con este atributo hacemos posible el llamar a este metodo desde el menú
    [MenuItem("Assets/Create/Custom/ScriptableObject")]
    public static void CreateMyAsset()
    {
        //Creamos el scriptableObject llamando al metodo estatico ScriptableObject.CreateInstance y guardandolo en una variable
        TestScriptableObject so = ScriptableObject.CreateInstance<TestScriptableObject>();

        //Con este metodo creamos el asset en el proyecto
        AssetDatabase.CreateAsset(so, "Assets/Test.asset");

        //Importante guardar los assets para que se escriban los cambios al disco duro
        AssetDatabase.SaveAssets();

        //Enfocamos la ventana del proyecto
        EditorUtility.FocusProjectWindow();

        //Seleccionamos el objeto que acabamos de crear
        Selection.activeObject = so;
    }
}

Este funcionará muy parecido al primer método.
Ahora que ya sabes como crear un scriptableObject este puede ser usado para guardar muchos datos, personalmente lo uso como una base de datos, ya que no es tan práctico para guardar datos en tiempo real, sin embargo puede ser usado para ello.
Para guardar datos sólo basta con crear variables en el script y estas se reflejarán en el inspector donde pueden ser alimentadas, también pueden ser arrastrados como variables a otros scripts y ser alimentado desde ellos.

Ejemplo base de datos :


Obviamente una base de datos como la de la imagen está fuera del enfoque de este tutorial pero quería mostrarles un ejemplo. Con scripts de editor y scriptableObjects puedes hacer maravillas.

Formas de serializar independientes de Unity

Los métodos anteriores que vimos son dependientes de Unity, ahora veremos las formas de serializar más estandarizadas a través de diferentes software, antes de analizar xml, json y binario quiero explicar las bases de la serialización, al serializar una clase guardaremos los campos públicos de esta, además en la mayoría de los casos la clase debe tener el atributo [System.Serializable] antes de poder ser serializada.

A que me refiero con los campos públicos? 




Al serializar guardaremos los valores de los campos resaltados en la imagen y si la clase hereda de otra, las dos deben tener el atributo [System.Serializable] es por esto y por cuestión de optimización que debemos evitar serializar clases que hereden de Monobehaviour o alguna otra clase de Unity, ya que tienen muchos datos que no necesitaremos, además de que algunas de las clases puedan no contener el atributo para serializar correctamente.


XML y Binario

Según wikipedia : "XML, siglas en inglés de eXtensible Markup Language ("lenguaje de marcas Extensible"), es un lenguaje de marcas desarrollado por el World Wide Web Consortium (W3C) utilizado para almacenar datos en forma legible."

Sencillamente se trata de un lenguaje o tipo de archivo en el que podemos serializar nuestros datos y a diferencia del binario estos pueden ser parcialmente o completamente leídos por humanos.

Ejemplo de como se ve un XML : 


Manos a la obra, veamos como serializar en xml, hay más de una manera de hacerlo, .Net nos trae intregadas varias maneras de hacerlo, veamos mi manera habitual:

Ejemplo sencillo de serialización en xml :

using UnityEngine;
using System.Collections;

//Este namespace como su nombre lo indica debemos agregarlo para acceder a la serializacion en xml
using System.Xml.Serialization;
//Este namespace es el que se encarga del manejo de archivos
using System.IO;

public class TestSerializationXML : MonoBehaviour {

 void Start () {
        //Creamos una variable del tipo de la clase que vamos a serializar
        ClassToSerialize test = new ClassToSerialize();

        //Le damos valores al primer campo publico
        test.PublicField1 = 150;

        //Le damos valor al segundo campo publico
        test.PublicField2 = "Hola Mundo";

        //Creamos una instancia de XmlSerializer usando el tipo de nuestra clase a serializar
        XmlSerializer serializer = new XmlSerializer(typeof(ClassToSerialize));

        //Aquí elegimos la ubicación y el nombre del archivo, en este caso uso la ruta de los assets + MyXML más el formato .xml
        FileStream stream = new FileStream(Application.dataPath + "/MyXML.xml", FileMode.Create);

        //Es importante elegir el tipo de Encoding a UTF8 para no tener problemas con ñ o tildes 
        StreamWriter streamWriter = new StreamWriter(stream, System.Text.Encoding.UTF8);

        //Aquí agregamos el objeto que creamos para serializarlo
        serializer.Serialize(streamWriter, test);

        //Cerramos el archivo para no tener errores cuando otro proceso intente acceder a el
        stream.Close();
        streamWriter.Close();
    }

}

//Clase para serializar
[System.Serializable]
public class ClassToSerialize
{
    public int PublicField1;
    public string PublicField2;

    public void Method1()
    {
        //hacer algo
    }
}

Al agregar ese script a un objeto en escena y dar clic en play obtendremos en nuestros assets un archivo llamado MyXML.xml con el siguiente contenido :
(Si no logras ver el archivo en el proyecto presiona Control + R, o minimiza y unity y vuelve a abrirlo para actualizar el proyecto)



Como se puede apreciar, los datos que llenamos se ven reflejados en el archivo, sin embargo el script que usamos parece un poco complicado, vamos a simplificar la serialización usando un método que nos facilite la vida:

public static void SaveXML<T>(string path, string fileName, object data) where T : class
    {
        //Verificamos que la ruta de guardado exista antes de intentar guardar el archivo, sino existe esta es creada
        Directory.CreateDirectory(path).Create();

        //Creamos la instancia de la clase XmlSerializer con el tipo deseado
        XmlSerializer serializer = new XmlSerializer(typeof(T));

        //Agregamos la extensión .xml al nombre del archivo si este no lo contiene
        if (!fileName.Contains(".xml"))
            fileName += ".xml";

        //Esta clase se encarga de la manipulación de archivos, creamos una instancia en modo crear, 
        //de esta manera si no existe el archivo se crea, usando la ruta y el nombre del archivo especificada
        FileStream stream = new FileStream(path + fileName, FileMode.Create);

        //Esta clase es la que se encargará de escribir el contenido en el archivo
        StreamWriter streamWriter = new StreamWriter(stream, System.Text.Encoding.UTF8);
        serializer.Serialize(streamWriter, data);

        //Cerramos el archivo para que esté disponible para otros procesos
        stream.Close();
        streamWriter.Close();
    }


De está manera serializar en XML será tan sencillo como llamar el método y dar los valores necesarios :
SaveXML<Tipo>("ruta", "nombre del archivo", datos);

Veamos como queda con el ejemplo usado antes :

public class TestSerializationXML : MonoBehaviour {

 void Start () {
        //Creamos una variable del tipo de la clase que vamos a serializar
        ClassToSerialize test = new ClassToSerialize();

        //Le damos valores al primer campo publico
        test.PublicField1 = 150;

        //Le damos valor al segundo campo publico
        test.PublicField2 = "Hola Mundo";

        SaveXML<ClassToSerialize>(Application.dataPath+"/", "MyXML.xml", test);
}

    public static void SaveXML<T>(string path, string fileName, object data) where T : class
    {
        Directory.CreateDirectory(path).Create();
        XmlSerializer serializer = new XmlSerializer(typeof(T));
        if (!fileName.Contains(".xml"))
            fileName += ".xml";
        FileStream stream = new FileStream(path + fileName, FileMode.Create);
        StreamWriter streamWriter = new StreamWriter(stream, System.Text.Encoding.UTF8);
        serializer.Serialize(streamWriter, data);
        stream.Close();
        streamWriter.Close();
    }

}

Ahora que ya sabemos como guardar un XML, veamos como es el proceso inverso de cargar desde un archivo XML, está vez usaremos inmediatamente un método para ello :

public static T LoadXml<T>(string path, string fileName) where T : class
    {
        //Creamos la instancia de la clase XmlSerializer con el tipo deseado
        XmlSerializer serializer = new XmlSerializer(typeof(T));

        //Verificamos si el archivo existe antes de intentar leerlo
        if(File.Exists(path+fileName))
        {
            //Esta clase se encarga de la manipulación de archivos, creamos una instancia en modo abrir
            FileStream stream = new FileStream(path + fileName, FileMode.Open);

            //Creamos una instancia de StreamReader la clase que se encarga de leer el archivo
            StreamReader sr = new StreamReader(stream, System.Text.Encoding.UTF8);

            //Creamos una variable de tipo T (el tipo T representa el tipo que usamos al llamar el metodo)
            T t = serializer.Deserialize(sr) as T;

            //Cerramos el archivo para que esté disponible para otros procesos
            stream.Close();
            sr.Close();

            //Regresamos el objeto deserializado
            return t;
        }
        //Si no existe el archivo regresamos null indicando que no encontramos nada
        return null;
    }

Veamos su uso cargando el archivo que creamos anteriormente :

void Start ()
    {
        ClassToSerialize test = LoadXml<ClassToSerialize>(Application.dataPath+"/", "MyXML.xml");
        if(test!=null)
        {
            Debug.Log(test.PublicField1);
            Debug.Log(test.PublicField2);
        }
    }

El resultado en la consola :


Para serializar en binario sólo basta con cambiar el método que usamos y la extensión del nombre del archivo a .bin

Quedarían de la siguiente forma :

public static void Save(string fileName, object data)
    {
        BinaryFormatter bf = new BinaryFormatter();
        Stream stream = new FileStream(Application.persistentDataPath + fileName, FileMode.Create);
        bf.Serialize(stream, data);
        stream.Close();
    }

    public static T Load(string fileName) where T : class
    {
        BinaryFormatter bf = new BinaryFormatter();
        Stream stream = new FileStream(Application.persistentDataPath + fileName, FileMode.Open);
        T t = (T)bf.Deserialize(stream);
        stream.Close();
        return t;
    }

La serialización en binario es muy poderosa, puede guardar cualquier tipo de archivo, guardar grafos, arboles, referencias, arrays multidimensionales, el único problema es que el archivo resultante no es legible por humanos, por lo tanto es un poco más complicado a la hora de debugear o editar externamente.

JSON

Según wikipedia : "JSON, acrónimo de JavaScript Object Notation, es un formato de texto ligero para el intercambio de datos. JSON es un subconjunto de la notación literal de objetos de JavaScript aunque hoy, debido a su amplia adopción como alternativa a XML, se considera un formato de lenguaje independiente."

Básicamente es como el XML pero más ligero y sencillo, muy utilizado actualmente.

Desde la versión 5.3 Unity nos ha facilitado la vida incluyendo su sencillo serializador json, de otra manera hay que buscar un framework (hay muchas opciones y gratuitas).


Al usar el serializador de Unity este nos devolverá un string, el cual tendremos que guardar en un archivo, primero hagamos un ejemplo imprimiendo en la consola:

using UnityEngine;
public class TestSerializationXML : MonoBehaviour {

 void Start () {
        //Creamos una variable del tipo de la clase que vamos a serializar
        ClassToSerialize test = new ClassToSerialize();

        //Le damos valores al primer campo publico
        test.PublicField1 = 150;

        //Le damos valor al segundo campo publico
        test.PublicField2 = "Hola Mundo";

        Debug.Log(JsonUtility.ToJson(test));
    }
}

//Clase para serializar
[System.Serializable]
public class ClassToSerialize
{
    public int PublicField1;
    public string PublicField2;

    public void Method1()
    {
        //hacer algo
    }
}


El resultado :



Como podemos ver es muy fácil de usar, Tan sólo JsonUtility.ToJson( Objeto a serializar ) y nos devolverá un string con el objeto serializado.

Ahora veamos como guardar ese string a un archivo, usaremos el siguiente metodo :

public static void TextWriter(string path, string fileName, string text)
    {
Directory.CreateDirectory(path).Create();
        using (StreamWriter writetext = new StreamWriter(path + fileName))
        {
            writetext.WriteLine(text);
        }
    }

Veamos como queda todo el script :

using UnityEngine;
using System.Collections;
using System.IO;

public class TestSerializationXML : MonoBehaviour {

 void Start () {
        //Creamos una variable del tipo de la clase que vamos a serializar
        ClassToSerialize test = new ClassToSerialize();

        //Le damos valores al primer campo publico
        test.PublicField1 = 150;

        //Le damos valor al segundo campo publico
        test.PublicField2 = "Hola Mundo";

        string serializedText = JsonUtility.ToJson(test);

        Debug.Log(serializedText);

        TextWriter(Application.dataPath + "/", "JSONTest.json", serializedText);
    }

    public static void TextWriter(string path, string fileName, string text)
    {
        Directory.CreateDirectory(path).Create();
        using (StreamWriter writetext = new StreamWriter(path + fileName))
        {
            writetext.WriteLine(text);
        }
    }
}

//Clase para serializar
[System.Serializable]
public class ClassToSerialize
{
    public int PublicField1;
    public string PublicField2;

    public void Method1()
    {
        //hacer algo
    }
}

Al agregar el anterior script a un objeto en escena y darle clic a play se creará un nuevo archivo en los assets llamado JSONTest.json
(Si no logras ver el archivo en el proyecto presiona Control + R, o minimiza y unity y vuelve a abrirlo para actualizar el proyecto)



Ahora procedemos a cargar el archivo, usaremos el siguiente método :

public static string TextReader(string path, string fileName)
    {
        //Inicializamos una variable de tipo string como string vacia
        string result = string.Empty;

        //Verificamos que el archivo exista
        if (File.Exists(path+ fileName))
        {
            //Creamos la instancia de streamReader con la ruta y el nombre del archivo
            using (StreamReader readtext = new StreamReader(path+ fileName))
            {
                //Guardamos en la variable todo el texto del archivo
                result = readtext.ReadToEnd();
            }
        }
        else
        {
            //Si no se encontró el archivo imprimimos un mensaje
            Debug.Log("No se encontró archivo en la ruta : " + path);
        }
        //Regresamos el resultado
        return result;
    }

Ahora lo usaremos para cargar el archivo e imprimir los resultados:
using UnityEngine;
using System.Collections;
using System.IO;

public class TestSerializationXML : MonoBehaviour
{

    void Start()
    {
        string text = TextReader(Application.dataPath + "/", "JSONTest.json");
        if (!string.IsNullOrEmpty(text))
        {
            ClassToSerialize test = JsonUtility.FromJson<ClassToSerialize>(text);
            Debug.Log(test.PublicField1);
            Debug.Log(test.PublicField2);
        }
    }

    public static string TextReader(string path, string fileName)
    {
        string result = string.Empty;
        if (File.Exists(path + fileName))
        {
            using (StreamReader readtext = new StreamReader(path + fileName))
            {
                result = readtext.ReadToEnd();
            }
        }
        else
        {
            Debug.Log("No se encontró archivo en la ruta : " + path);
        }
        return result;
    }
}

//Clase para serializar
[System.Serializable]
public class ClassToSerialize
{
    public int PublicField1;
    public string PublicField2;

    public void Method1()
    {
        //hacer algo
    }
}

Resultado en la consola :


De esta manera aprendimos a serializar nuestros archivos de diferentes maneras, pero puede que no esté del todo claro como podemos usar la serialización en nuestros juegos, en el siguiente post, debido a que este se hizo más grande de lo que esperaba, haré un ejemplo práctico con cada uno de los métodos de serializacion, Si no entiendes algo, por favor envíame un correo a darkyashamaru@gmail.com o déjalo en los comentarios y trataré de explicarlo de mejor manera.

6 comentarios:

  1. bastante interesante el tema, si acaso solo conocía playerPrefs, desconocía los otros metodos

    ResponderEliminar
    Respuestas
    1. Que bueno que te haya interesado, cuando empecé también sólo conocía PlayerPrefs, cualquier duda al respecto, no dudes en preguntar.

      Eliminar
  2. Muchas gracias Ricardo, excelente explicación, por fin entiendo como y para que se usa la serialización

    ResponderEliminar
  3. Muy bien explicado, la he usado en mi proyecto y funciona (XML), pero solo dentro del editor de Unity, al hacer la build no crea el archivo, sabes que puede ser?

    ResponderEliminar
  4. Creo que no se actualiza el hilo aún así pregunto yo también, usar estos métodos está bien para prevenir cheats? para android ? ios?

    ResponderEliminar
  5. Muchas gracias por la información

    ResponderEliminar