07 diciembre 2025

Serialización de diccionarios en Unity

En desarrollo, la serialización consiste cambiar el formato de un objeto de memoria para que pueda ser almacenado en disco. 

Uno de los formatos más populares de serialización es JSON, un estándar de texto con el que guardas datos en ficheros siguiendo una estructura de llaves muy similar a la de los diccionarios de Python. 

Cuando serializas un objeto, la instancia de una clase, a JSON lo que haces es seleccionar los datos claves del objeto y los conviertes en campos y valores de un fichero JSON. ¿Cuáles son los datos claves del objeto? es el mínimo subconjunto de datos que te permita reconstruir el objeto en el estado que necesitas. Ese proceso de reconstrucción es lo que se denomina deserialización y es el inverso al otro: lees el fichero y con su contenido creas una instancia de la clase con un estado lo más parecido posible al original.

Unity está constantemente serializando y deserializando. Cuando usamos el inspector para darle valor a un campo público de un MonoBehavior, Unity serializa ese valor a su propio formato para que lo tengas en el inspector la próxima vez que arranques el editor. Por defecto, Unity hace ese proceso con todos los campos públicos de los MonoBehaviour y los ScriptableObject, pero también puedes forzarlo en los campos privados si los defines precedidos del atributo [SerializeField]. Ese tributo tiene también el efecto de permitirnos editar en el inspector campos privados. Sin él, el inspector sólo muestra los públicos. Cuidado, eso no significa que tengas que usar ese atributo en todos los campos privados de tu MonoBehavior. Su uso sólo tiene sentido en aquellos campos que contienen valores de base de tu clase, es decir, aquellos que configurarías en el inspector antes de la ejecución del juego. Preceder un campo calculado de con [SerializeField] no tendría sentido, salvo que quisieras conservar ese valor calculado para una ejecución posterior.

Precisamente, ese era el caso que me movió a escribir este artículo. Estoy escribiendo una clase que me permita analizar un escenario y generar un grafo representando todas sus zonas transitables. El proceso de creación del grafo no viene a cuento, pero mi era que el grafo se generase en tiempo de desarrollo, se guardase y se cargase en tiempo de ejecución. Vamos, que quería que mi grafo se guardase en un ScriptableObject. Uno de los componentes de mi grafo se basa en un diccionario, cuyas claves son las coordenadas enteras de una malla rectangular, y sus valores los nodos con los enlaces a los nodos vecinos. Y el problema viene porque Unity no sabe serializar diccionarios. Puede serializar listas, pero no diccionarios. Ese es el motivo por el que puedes editar listas en el inspector, pero no diccionarios.

Los diccionarios son una de las estructuras de datos más comunes en desarrollo, así que no he sido el primero en encontrarse este problema. Es tan habitual, que otros engines, como Godot, llevan a gala poder serializar diccionarios.

¿Cómo se puede sortear ese problema? Bueno, puedes generar una clase que represente hacia fuera el comportamiento de un diccionario, pero que por dentro se base en dos listas, una de claves y otra de valores. A la hora de serializar, se guardarían esas dos listas que sí que son procesables por Unity. A la hora de deserializar, se leerían esas dos listas y se usarían para crear en memoria un diccionario interno desde el que la clase ofrecería su funcionalidad al resto de los componentes del juego. Esta solución es perfectamente legítima, pero a estas alturas ha sido tan usada que ya ha sido incluida en una herramienta de serialización de múltiples tipos de clases (no sólo diccionarios) como es Odin Serializer. Así que sería como reinventar la rueda. Si con todo y con eso, quieres hacértelo tú mismo, la misma página de Odin te explica cómo, aunque advierte de que el demonio está en los detalles y que puede haber situaciones raras al editar prefabs que pueden obligar sofisticar bastante la implementación inicial. Ese camino ya lo han recorrido los chicos de Odin Serializer, así que voy a explicar cómo utilizarlo.

Logo de Odin Serializer
Logo de Odin Serializer

Odin Serializer es una herramienta open source y gratuita. Te la puedes descargar de su página web, en formato de paquete de unity con todos los ficheros de código fuente para serializar todo lo que permite Odin. Odin Serializer es sólo el banderín de enganche gratuito de un conjunto de herramientas que, esas sí, son de pago. A partir del serializador gratuito abarcan una herramienta para crear inspectores personalizados y otra para encontrar errores en tus proyectos. Ambas son potentísimas. La primera te permite ahorrarte el paso por el UI Toolkit a la hora de implementar inspectores cómodos y eficientes. La segunda detecta los errores más típicos en el desarrollo con Unity y ofrece un conjunto de atributos personalizados para cargar de semántica tus campos y detectar casos en los que sus valores no se ajustes a su semántica (o directamente impedirte introducir esos valores). Aunque en el caso concreto de este artículo me ha bastado con usar el Serializer gratuito, te recomiendo que le eches un ojo a sus otras herramientas y precios. Son de pago único y de una cuantía tirada para un desarrollador indie.

La página de descarga sólo te pide el namespace base de tu juego, para personalizar los namespaces de todos los ficheros de código incluidos. El paquete incluye una carpeta OdinSerializer de la que cuelgan el resto de carpetas de la herramienta. Tienes que colocar esa carpeta dentro de la de Scripts de tu juego.

Una vez importado en tu carpeta de Scripts, OdinSerializer ofrece un conjunto de clases especializadas que heredan de las habituales en Unity:

  • SerializedBehaviour
  • SerializedComponent
  • SerializedMonoBehaviour
  • SerializedNetworkBehaviour
  • SerializedScriptableObject
  • SerializedStateMachineBehaviour
  • SerializedUnityObject

Te basta con sustituir la clase de Unity de la que herede tu componente por la equivalente para que Odin se encargue de serializar aquellos tipos de los que Unity no se ocupe. Todos los diccionarios, por ejemplo, serán serializados por Odin sin que haga falta nada más, de manera que si editas su contenido desde el editor, se conservará entre ejecuciones de este.

Sin embargo, hay que tener en cuenta que aunque serialices el contenido del diccionario con Odin, el inspector de Unity será incapaz de leerlo. Así que no te extrañe si tu diccionario público sigue sin aparecer en el inspector. Para eso haría falta instalarse Odin Inspector, que es uno de los componentes de pago. Sin ese componente, cualquier modificación desde el editor del contenido de un diccionario serializado debería venir de scripts personalizados para ejecutarse desde el editor. Mi caso era precisamente ese, ya que he configurado un botón en el inspector del componente de mi grafo para generar la malla al pulsarlo y guardar los valores creados en el diccionario interno del componente. Si modifico el escenario, no tengo más que volver a pulsar el botón para regenerar la malla del grafo.

Con Odin Serializer deberías ser capaz de resolver la mayor parte de tus necesidades para serializar diccionarios, pero no siempre es tan sencillo. Aunque ya había usado Odin Serializer en proyectos más simples, no he conseguido hacerlo funcionar en el proyecto que motivó este artículo. Generaba mi grafo correctamente al pulsar el botón, pero dicho grafo no estaba allí en el siguiente arranque del editor o al recargar el nivel. Por lo que quiera que sea, el diccionario que contenía el grafo no se salvaba al guardar el nivel. Le he dado muchas vueltas y no sé si se debe a que tengo varios niveles de diccionarios anidados unos dentro de otros, o porque los scripts que contienen esos diccionarios están colocados en prefabs también anidados. También puede ser porque mi componente usa un editor personalizado, lo que añade un nivel más de indirección a la serialización de sus datos. Sea por la razón que sea, al final no me ha quedado más remedio que implementar mi propia serialización. Precisamente esa serialización manual acerca de la que prevenía, si podías resolver tu problema con Odin. En esta ocasión no me ha quedado más remedio que reinventar la rueda, aunque he tenido la suerte de poder contar con el método explicado en la misma página de Odin Serializer. Te voy a contar cómo, por si te sirve para salir del paso en alguna ocasión.

Como decía antes, la clave está en crear una clase que herede de Dictionary para conservar su funcionalidad. Para dotar a esa nueva clase de la capacidad de serializarse dentro de Unity, hay que hacer que implemente el interfaz ISerializationCallbackReceiver. Cuando Unity recibe la orden de serializar una clase, porque el campo de esa clase sea público o esté marcado con el atributo [SerializeField], espera que dicha clase implemente el interfaz ISerializationCallbackReceiver. Ese interfaz consta de dos métodos, OnBeforeSerialize() en el que deberemos implementar cómo queremos guardar la información de nuestro objeto al serializar, y OnAfterDeserialize() en el que implementaremos cómo recuperar la información guardada para reconstruir con ella el estado del objeto.

La clase que he creado se basa en dos listas, una para las claves del diccionario y otra para sus valores. Será en esas listas donde guarde los datos a conservar, gracias a que Unity sí que serializa listas de manera nativa.

Los campos de mi diccionario personalizable
Los campos de mi diccionario personalizable

Ahora la implementación del interfaz. Primero para el proceso de serialización.

Proceso de serialización de mi diccionario personalizable
Proceso de serialización de mi diccionario personalizable

Cuando Unity llame a OnBeforeSerialize() será porque es hora de guardar la información del estado del objeto. Para ello, lo que hago es vaciar el contenido anterior de las listas (líneas 42 y 43) para volver a rellenarlas con el listado actualizado de las claves y los valores del diccionario. Cuando se cierre la instancia de la clase se perderá el contenido del diccionario, pero las listas habrán quedado serializadas junto con el resto de información de la escena.

Ahora el proceso inverso. Se abre de nuevo la escena y Unity llama a los métodos OnAfterDeserialize() de todos sus objetos para que estos restauren su estado anterior a partir de la información deserializada.

Proceso de deserialización de mi diccionario personalizable
Proceso de deserialización de mi diccionario personalizable

Como se puede ver en el listado, dado que las listas sí que serializaron, estarán con su contenido intacto al llamar a OnAfterDeserialize() lo que permitirá utilizarlo para restaurar las entradas del diccionario. En la línea 34 a 36 se puede ver que recorro ambas listas para regenerar las entradas del diccionario interno de la clase.

Ahora viene algo importante. Unity no puede serializar tipos genéricos y la clase recién creada (UnitySerializedDictionary) lo es. La solución es concretar la clase genérica para cada uno de los usos a los que la apliquemos. Una vez concretada, la clase resultante sí podrá ser serializada por Unity. Esa es la razón por la que tengo un fichero aparte, estático, con las diferentes versiones concretadas del diccionario genérico.

Clases concretas a partir de la genérica
Clases concretas a partir de la genérica

Para evitar cualquier tentación de usar directamente la clase genérica, sin concretarla, lo mejor es marcarla como abstracta.

Serán esas versiones concretas las que podamos usar en nuestro código, con la tranquilidad de que Unity conservará su contenido entre ejecuciones.

Uso del diccionario concretado
Uso del diccionario concretado

MonoBehaviour que utiliza nuestro diccionario concretado y serializable
MonoBehaviour que utiliza nuestro diccionario concretado y serializable

Espero que te haya resultado útil y que te sirva para no cortarte a la hora de usar diccionarios en tus componentes.