En este post quiero mostrar cómo guardar determinados datos, ya sean partidas o configuraciones o ambas. Hoy en día los JSONs representan una forma bastante general para leer este tipo de cosas y me parece una idea copada enseñar a usarlos. Como siempre trato de hacer, enseño lo básico y ustedes usen los recursos de la manera que más útil les parezca.
JSON es básicamente un formato de texto que nos permite una especie de intercambio de datos, que bien podría darse entre multiples lenguajes de programación. Es bastante usado y cada lenguaje tiene sus herramientas para leer datos a partir de un archivo en este formato, sin tener que parsear a mano cada una de sus partes. Este tipo de formato soporta múltiples tipos de datos que son los que más se suelen usar/necesitar para poder almacenar información de tal manera que sirva para la gran parte de los casos. Suele usarse no sólo para guardar/cargar configuraciones sino también como resultado de alguna request a algún servidor o servicio, que podría devolver el resultado de alguna compra del usuario con todos los datos de dicha compra.
Los tipos que podemos almacenar en un JSON son:
TIPO DE DATO | FORMA DE USO |
Object | {} |
Array | [] |
String | “” |
Int | 10 |
Float | 10.5 |
Booleano | true |
Pudiendo escribir cada uno de estos tipos de datos dentro de este formato, podría generar un archivo que guarde gran cantidad de información y después leerla al momento de, por ejemplo, cargar mi juego y/o aplicación. Y así como puedo leer datos, también podría escribirlos. Vamos a ver un poco el formato con un ejemplo:
{ "settingsData": { "soundSettings": { "volume": 100 }, "videosettings": { "quality": "high" } }, "achievementsData": [ "You are the best!", "Complete the game!", "Complete without die!", "This is another achievment." ], "isGameComplete": true }
De más está decir que es indistinto para el formato si las llaves están arriba o abajo, o incluso si todo ese texto está en una sola línea. Ponerlo en múltiples líneas es sólo para poder leerlo mejor, pero si no necesito que una persona lo lea, podría incluso generarlo todo junto, en una línea, para que la carga consuma menos cantidad de datos. Yo soy de los que prefiere tener las llaves abajo, pero la página que estoy usando los pone arriba y por el momento elijo no quejarme. Por cierto, esa página es https://jsoneditoronline.org. Nos sirve para generar un archivo en este formato notificándonos si hay algún error, y ayudándonos en su creación.
Volviendo al tema en cuestión, pueden interpretar lo que está escrito en ese texto? Básicamente tengo dentro de ese archivo determinadas variables que pueden ser leídas y tratadas como las variables en cualquier lenguaje de programación. De hecho, fijense que tengo objetos adentro de otros objetos. O sea, el objeto “settingsData” tiene dentro un objeto “soundSettings“. Podemos hacer que esto se convierta a un objeto en un lenguaje de programación y accederlo de la manera en la que lo haríamos en dicho lenguaje. Por ejemplo, podríamos convertirlo a C#, guardarlo en una variable y accederlo con “myVar.settingsData.soundSettings.volume“, y si imprimimos ese valor nos mostraría “100“.
Si es la primera vez que ves este formato suena como muy utópico, verdad? Como si facilitara el manejo de datos demasiado. Pues esa es la idea. Aunque también estaría bueno no caer en el otro extremo: Usarlo para todo. Tampoco es que se podría (o debería), programar un juego entero en este formato. Como todo: Usenlo para lo que sea más conveniente.
Ahora… Cómo obtenemos acceso a estos datos? Vamos a verlo desde varios lugares. O sea, hay que cargar un archivo que tenga estos datos, leer el texto en este archivo y luego, de alguna manera, mágica o no, convertir el formato de ese texto (string), al formato correspondiente. Vamos a empezar por ver el proceso de construir un JSON.
CREACIÓN DE UN JSON
Hay muchas formas de crear un JSON. Si conocemos la estructura, podríamos directamente escribir el texto o string correspondiente, pero lógicamente no sería la manera más cómoda. Vamos a empezar a crear una clase. Imaginemos que tenemos los datos del JSON mostrado de ejemplo, pero guardados en una clase. Vamos a empezar por mostrarlo de una manera simple.
public class SettingsData { public bool isGameComplete; }
Si pasamos esta clase a JSON, quedaría básicamente así:
{ "settings": { "isGameComplete": true } }
Ahora la pregunta del millón: ¿Cómo pasamos de uno a otro? Para empezar tenemos que serializar la clase. Básicamente es un proceso que se hace para que al lenguaje le sea más “fácil” reconstruir el objeto en algún futuro. No es algo difícil indicárselo al lenguaje. Vamos a hacerle un pequeño cambio a la clase y con eso ya estaría:
[System.Serializable] public class SettingsData { public bool isGameComplete; }
Cabe destacar que si usan “using System” arriba de todo podrían escribir sólo “Serializable” y con eso bastaría. Ahora… Esto es requisito para que lo podamos convertir pero… Cómo lo convertimos? Bien, Unity tiene una clase que podemos usar llamada JsonUtility, que cuenta con funciones para crear JSONs desde una clase serializable y también para crear clases desde un JSON.
using UnityEngine; public class TestJSON : MonoBehaviour { private void Start() { SettingsData settings = new SettingsData(); settings.isGameComplete = true; Debug.Log(JsonUtility.ToJson(settings)); } } [System.Serializable] public class SettingsData { public bool isGameComplete; }
En el ejemplo hice toda la parte de la implementación para que se pueda testear. Pero la parte más útil es la parte que explicamos recién, que sería la función “ToJson()“, que básicamente convierte el objeto que le pase por parámetro a un JSON. Tengan en cuenta que esto puede pasar SÓLO si el objeto es serializable. Caso contrario no va a devolver algo que sea útil. Incluso podría devolver sólo una parte del objeto en formato JSON.
Hasta acá no parece nada de otro mundo, verdad? El tema es que… En el ejemplo que puse arriba de todo, el JSON tiene otros objetos dentro. Vamos a ver ese caso. ¿Cómo debería ser la clase entonces?
using UnityEngine; public class TestJSON : MonoBehaviour { private void Start() { SettingsData settings = new SettingsData(); settings.isGameComplete = true; settings.soundData = new SoundData(); settings.soundData.volume = 100; settings.videoData = new VideoData(); settings.videoData.quality = "high"; settings.achievements = new[]{ "You are the best!", "Complete the game!", "Complete without die!", "This is another achievment." }; Debug.Log(JsonUtility.ToJson(settings)); } } [System.Serializable] public class SettingsData { public SoundData soundData; public VideoData videoData; public string[] achievements; //El array ya es serializable!! public bool isGameComplete; } [System.Serializable] public class SoundData { public float volume; } [System.Serializable] public class VideoData { public string quality; }
En líneas generales, la idea sería construir una clase que guarde todos los datos que queremos “JSONizar“, con todas las subclases necesarias. Tengan en cuenta que todas las clases que creamos como “SettingsData” y “VideoData” no necesariamente tienen que estar dentro del mismo archivo. Eso elijanlo ustedes.
Lo que tienen que tener también en cuenta es que los datos simples como enteros, flotantes, textos y demás ya pueden serializarse de manera nativa, así que no hay nada que hacer. Pero para objetos propios o más complejos, hay que hacer una implementación con el “System.Serializable” que pusimos arriba, y mayoritariamente van a tener que crear una clase para ello. Yo les remendaría que guarden los datos con los tipos más básicos posibles y en tal caso luego lo reconstruyan con alguna lógica aparte.
No les recomiendo usar esto para cosas como agarrar el estado entero de un personaje, convertirlo en JSON y guardarlo como un savegame. O sea, usar JSONs para hacer un savegame no está mal, de hecho se podría usar tranquilamente, pero a lo que voy es que no lo usen para guardar estados de objetos tan complejos como pueden ser los personajes de un juego, ya que probablemente necesiten gran cantidad de datos para guardarse que probablemente no haga diferencia guardarlos o no.
GUARDAR EL JSON GENERADO EN UN ARCHIVO
Si entendieron y pudieron aplicar correctamente los pasos anteriores, notaran que ya tienen una cadena de texto, que básicamente representa todo el objeto que acabamos de convertir. Pasemos ahora al proceso de guardarlo en algún archivo.
Existen muchas maneras de guardarlo, dependiendo de cuál se ajuste más a tus necesidades y restricciones (por ejemplo, si se quiere guardar en un servidor, o un celular, o una PC). Vamos a ver algunos métodos, algunos más sencillos que otros, para este proceso. Tengan en cuenta que el hecho de que sea más fácil de usar no significa que sea ideal para todos los casos. Por lo general, lo idea sea lo que mejor se adapte a nuestras necesidades, y no afecte demasiado al rendimiento del juego.
PLAYER PREFS
Este método es el más sencillo, ya que es una herramienta que provee Unity. Podríamos decir que esa es una ventaja, ya que al ser parte de Unity se encarga de hacer el chequeo correspondiente para guardarlo de maneras diferentes dependiendo de para qué plataforma se vaya a exportar el proyecto. Lo “malo” (entre comillas porque tampoco creo que sea taaaaan malo), es que no podemos modificar la forma en la cual escribe, así que en el caso de querer hacerlo de una manera más particular, no podremos. Pero como dije, tampoco es una gran desventaja. De hecho, tendremos la seguridad de que va a funcionar sin importar en qué plataforma lo usemos.
Para guardar datos, al igual que para leerlos, es bastante sencillo. PlayerPrefs tiene funciones bastante útiles como:
PlayerPrefs.SetString(string key, string value); PlayerPrefs.GetString(string key, string defaultValue);
Con estos dos métodos podremos escribir y leer datos en el dispositivo del usuario, ya sea una PC, table, celular, etc. Cabe aclarar que esto no escribe los datos de manera instantánea. Unity los pasa al disco/almacenamiento una vez que la aplicación se cierra. Por supuesto también hay una manera de hacerlo instantáneamente, aunque no está de más aclarar que no es muy recomendable escribir todo el tiempo en el disco o en el medio de almacenamiento que se use. Esa manera es:
PlayerPrefs.Save();
Con esto podríamos guardar el JSON generado anteriormente, ya que lo tenemos convertido en un string
, haciendo algo como:
PlayerPrefs.SetString("data", myJsonGenerated);
Y listo! Con esto ya guardaríamos los datos generados. Ahora en cualquier momento podríamos leerlos. Recuerden que tienen que llamar a la función Save()
si quieren que los datos se guarden instantáneamente. Caso contrario deberán esperar a que la aplicación se cierre.
ESCRIBIR Y LEER UN ARCHIVO CON LA CLASE FILE
Otro de los métodos que tenemos es uno más “tradicional“. Se usaba cuando no teníamos un framework como Unity que te facilite las cosas. La ventaja que yo creo que tiene es que nos da toda la libertad para escribir el archivo como nosotros queramos. La desventaja es que no vamos a poder guardarlo en cualquier lado en todas las plataformas, así que vamos a tener que preguntar en qué plataforma estamos y elegir una forma de guardado diferente.
Algo a tener en cuenta es que esta clase File
de la cual hablo es la que tiene C# en System.IO
. Hago la aclaración porque Unity tiene una clase llamada File también en UnityEngine.Windows
, pero no es la misma. De hecho, en la página de ayuda de Unity podemos ver que aclara que sólo sirve para la plataforma de Windows.
Con esta clase podemos usar los siguientes métodos:
//Escribe un texto dentro de un archivo ubicado en la ruta //especificada en el primer parámetro. Si el archivo no existe //lo crea y le pone el texto. Caso contrario lo sobreescribe. File.WriteAllText(string path, string content);
//Si tenemos el texto a escribir dentro de una lista, array //o incluso un diccionario, podríamos pasarlo como array //y hacer que cada índice sea una línea dentro del archivo. File.WriteAllLines(string path, string[] content);
El primer parámetro de ambos métodos representan la ruta. Desde acá sí deberíamos especificar la ruta completa, ya que el método la necesita. Podemos hacerlo de varias maneras:
- @”C:\Windows” (noten que el arroba es simplemente para no tener que escapear cada una de las contrabarras).
- Directory.GetCurrentDirectory() + @”\UnDirectorioCualquiera\NombreArchivo.extension”.
La clase Directory
la podríamos usar como ayuda para ir construyendo directorios y/o eliminándolos. Ambas clases contienen un método llamado Exist()
al cual le podemos pasar una ruta y nos dice si dicho archivo o directorio existen.
Volviendo a la clase File
, de más está decir que también tiene los métodos para leer desde un archivo:
//Leemos todo el contenido de un archivo DE TEXTO. string txt = File.ReadAllText(string path);
//Lo mismo que la anterior, sólo que lee cada línea y la pone //en un índice de un array. Esto es útil para cosas como leer //configuraciones que están separadas línea a línea. En tal caso, //no habría necesidad de separar las líneas porque este método //ya cumpliría esa función. string[] lines = File.ReadAllLines(string path);
Y con esto tenemos el segundo método para escribir y leer archivos externos. Probablemente este sea el que más dificultades les pueda traer en cuanto a portabilidad, ya que es un toque complicado hacer que funcione en todas las plataformas al mismo tiempo.
NOTA IMPORTANTE:
Unity tiene una property que podemos usar para guardar datos en el directorio asignado para cada sistema operativo. Dicha variable es Application.persistentDataPath
. Es un directorio que va a ser diferente dependiendo de si la app es para Android, iOS, etc.
GUARDAR Y LEER DATOS CON SCRIPTABLEOBJECTS:
Con los ScriptableObjects volvemos un poco a la parte de Unity. Es algo bastante nuevo de Unity y que de a poco se va utilizando cada vez más. Básicamente nos permite crear un archivo, que pertenece siempre dentro del directorio de Unity y cuya serialización es hecha puramente por Unity.
IMPORTANTE:
Si bien esta debería ser una nota importante a tener en cuenta, Unity SÍ nos permite modificarlos en runtime, aunque el problema seguiría siendo el mismo: al cerrar el juego, los valores que quedan son los que vinieron de fábrica. Es importante tenerlo en cuenta también porque este es otro más de los comportamientos que difiere del editor. O sea, mientras los usemos en el editor, cualquier cambio que le hagamos VA A PERSISTIR entre una ejecución y otra (con el botón PLAY). Pero al buildear pasará a tener los valores predeterminados.
¿Y por qué nos mostrás esto si al final no podemos guardar datos con esto?
Bueno… Eso no es del todo correcto. Al tener el ScriptableObject como una clase nos da la facilidad de acceder a los datos como queramos con las ventajas de cualquier objeto serializable, teniendo el auto-completar del editor. Ahora bien… El proceso de guardado sería exactamente igual, o sea: utilizamos JsonUtility.ToJson(miObjeto) para generar el archivo y luego lo guardamos con cualquier método. Hasta acá todo medianamente normalito.
Para cargarlo es un poquitín diferente, y tiene su lógica. Digamos que a la variable que almacena la referencia al ScriptableObject no podemos sobreescribirla y ya con un JsonUtility.FromJson(). Esto es porque dicha función CREA una instancia nueva del objeto que querémos deserializar, lo cual no se puede hacer con los ScriptableObjects. Pero, para este caso y para casos en los cuales ya tenemos creada una instancia y/o referencia y sólo nos interesa rellenar los valores de las variables por los valores que nosotros tenemos guardados en algún JSON, podríamos usar el siguiente método: JsonUtility.FromJsonOverwrite(jsonString, targetObject). Este método recibe el JSON correspondiente (en formato string), y un objeto como segundo parámetro, que sería el objeto al cual le vamos a pisar sus variables con las que estén en el JSON. O sea que nos quedaría de la siguiente manera:
//jsonString => Variable que contiene nuestro JSON en formato string. //targetObject => Variable a la cual se le van a setear los datos. JsonUtility.FromJsonOverwrite(jsonString, targetObject);
Y con esto estaríamos combinando dos sistemas de guardado, mezclando un poco de comodidad con versatilidad.
CONCLUSIÓN:
Obviamente hay muchas más formas para guardar datos, y no necesariamente tienen que quedarse con una. Prueben cuál es el método de guardado que más les guste y que a la vez les convenza más a la hora de hacer sus juegos. Si encuentro alguna forma que me vuele la cabeza o que vea que es bastante buena, voy a hacer un post (luego de aprenderla).