Comprender el lenguaje de serialización de Unity, YAML

¿Sabías que puedes editar cualquier tipo de activo sin la molestia de lidiar con lenguajes de serialización como XML o JSON en el Editor de Unity? Si bien esto funciona la mayor parte del tiempo, hay algunos casos en los que debes modificar tus archivos directamente. Pensemos en conflictos de fusión o archivos dañados como ejemplos.
Por eso, en esta publicación de blog, analizaremos más a fondo el sistema de serialización de Unity y compartiremos casos de uso de lo que se puede lograr modificando archivos de activos directamente.
Como siempre, haga una copia de seguridad de sus archivos y, idealmente, utilice el control de versiones para evitar la pérdida de datos. Modificar manualmente los archivos de activos es una operación riesgosa y Unity no la admite. Los archivos de activos no están diseñados para modificarse manualmente y no generarán mensajes de error útiles para explicar qué sucedió si y cuando ocurren errores, lo que dificulta la reparación de errores. Al comprender mejor cómo funciona Unity y prepararse para resolver conflictos de fusión, puede compensar situaciones en las que la API de base de datos de activos no es suficiente.
YAML, también conocido como “YAML Ain't Markup Language”, es parte de la familia de lenguajes de serialización de datos legibles por humanos como XML y JSON. Pero debido a que es liviano y relativamente sencillo en comparación con otros lenguajes comunes, se considera más fácil de leer.
Unity utiliza una biblioteca de serialización de alto rendimiento que implementa un subconjunto de la especificación YAML. Por ejemplo, líneas en blanco, comentarios y otras sintaxis compatibles con YAML no son compatibles con archivos Unity. En ciertos casos extremos, el formato Unity difiere de la especificación YAML.
Exploremos esto observando un fragmento de código YAML en un prefabricado de cubo. Primero, cree un cubo predeterminado en Unity, conviértalo en un Prefab y abra el archivo Prefab en cualquier editor de texto. Como puede ver en la Figura 1, las dos primeras líneas son encabezados que no se repetirán más adelante. El primero define qué versión de YAML estás usando, mientras que el segundo crea una macro llamada “!u!” para el prefijo URI “tag:unity3d.com,2011:” (que se analiza a continuación).

Después de los encabezados, encontrará una serie de definiciones de objetos, como GameObjects en un Prefab o escena, los componentes de cada GameObject y posiblemente otros objetos como configuraciones de Lightmap para escenas.

Cada definición de objeto comienza con un encabezado de dos líneas, como el de nuestro ejemplo para la Figura 2: “--- !u!1 &7618609094792682308” sigue el formato “--- !u!{CLASS ID} &{FILE ID}” que se puede analizar en dos partes:
- !u!{CLASS ID}:Esto le dice a Unity a qué clase pertenece el objeto. La parte “!u!” será reemplazada con la macro definida previamente, dejándonos con “tag:unity3d.com,2011:1” – el número 1 hace referencia al ID de GameObject en este caso. Cada ID de clase se define en el código fuente de Unity, pero puede encontrar una lista completa de ellas aquí.
- &{FILE ID}: esta parte define el ID del objeto en sí, que se utiliza para hacer referencia a objetos entre sí. Se llama ID de archivo porque representa el ID del objeto en un archivo específico. Continúe leyendo para obtener más información sobre las referencias entre archivos más adelante en esta publicación.
La línea de encabezado del segundo objeto es el nombre del tipo de objeto (aquí, GameObject), que le permite identificarlo leyendo el archivo.

Después del encabezado del objeto, puede encontrar todas las propiedades serializadas. En nuestro ejemplo de GameObject anterior, la Figura 2 proporciona detalles como su nombre (m_Name: Cubo) y capa (m_Layer: 0). En el caso de la serialización MonoBehaviour, notarás los campos públicos y los privados con el atributo SerializeField. Este formato se utiliza de manera similar para objetos programables, animaciones, materiales, etc. Tenga en cuenta que ScriptableObjects utiliza MonoBehaviour como su tipo de objeto, en lugar de definir el suyo propio. Esto se debe a que la misma clase interna MonoBehaviour también los aloja.
Con lo que hemos cubierto hasta ahora, puedes comenzar a aprovechar el poder de modificar YAML para fines tales como refactorizar pistas de animación.
Los archivos de animación de Unity funcionan describiendo un conjunto de pistas o curvas de animación; una para cada propiedad que desee animar. Como se muestra en la Figura 4, una Curva de Animación identifica el objeto que necesita animar a través de la propiedad de la ruta, que contiene los nombres de los GameObjects secundarios hasta el específico. En este ejemplo, estamos animando un GameObject llamado “JumpingCharacter”, un elemento secundario del GameObject “Shoulder”, que es un elemento secundario del GameObject que tiene el componente Animator reproduciendo esta animación. Para aplicar la misma animación a diferentes objetos, el sistema de animación utiliza rutas basadas en cadenas en lugar de ID de GameObject.

Cambiar el nombre de un objeto animado en la jerarquía puede provocar un problema muy común: La curva podría perder su rumbo. Si bien esto generalmente se resuelve cambiando el nombre de cada pista de animación en la ventana Animación, hay casos en los que se aplican varias animaciones con varias curvas al mismo objeto, lo que lo convierte en un proceso lento y propenso a errores. En cambio, la edición YAML le permite corregir varias rutas de curvas de animación de una sola vez utilizando una operación clásica de “buscar y reemplazar” en los archivos de animación con el editor de texto con el que esté más familiarizado.

Como se mencionó anteriormente, cada objeto en un archivo YAML tiene un ID conocido como “ID de archivo”. Este ID es único para cada objeto dentro del archivo y sirve para resolver referencias entre ellos. Piense en un GameObject y sus componentes, los componentes y su GameObject, o incluso referencias de script, como una referencia de componente “Arma” a un GameObject “SpawnPoint” en el mismo Prefab.
El formato YAML para esto es “{fileID: ID DE ARCHIVO}” como el valor de la propiedad. En la Figura 6, se puede ver cómo este Transform pertenece a un GameObject con el ID 4112328598445621100, dado que su propiedad “m_GameObject” lo referencia a través del File ID. También puede observar ejemplos de referencias nulas como “m_PrefabInstance” (dado que su ID de archivo es cero). Continúe leyendo para obtener más información sobre las instancias Prefab.

Consideremos el caso de reparentalización de objetos dentro de un Prefab. Puede cambiar el ID de archivo de la propiedad “m_Father” de un Transform con el ID de archivo del nuevo Transform de destino, e incluso corregir el antiguo YAML del Transform padre para eliminar este objeto de su matriz “m_Children” y agregarlo a la nueva propiedad padre “m_Children”.

Para encontrar una Transform específica por nombre, debes determinar principalmente su ID de archivo GameObject buscando aquel con el m_Name que estás buscando. Sólo entonces podrás localizar la Transformación cuya propiedad m_GameObject hace referencia a ese ID de archivo.
Al hacer referencia a objetos fuera de este archivo, como un script “Arma” que hace referencia a un Prefab “Bala”, las cosas se vuelven un poco más complejas. Recuerde que el ID del archivo es local, lo que significa que puede repetirse en diferentes archivos. Para identificar de forma única un objeto en otro archivo, necesitamos un ID adicional o “GUID” que identifique el archivo completo en lugar de los objetos individuales dentro de él. Cada activo tiene esta propiedad GUID definida en su archivo meta, que se puede encontrar en la misma carpeta que el archivo original, con exactamente el mismo nombre más una extensión “.meta” agregada.

Para formatos de archivos que no son nativos de Unity, como imágenes PNG o archivos FBX, Unity serializa configuraciones de importación adicionales para ellos en los archivos meta, como la resolución máxima y el formato de compresión de una textura, o el factor de escala de un modelo 3D. Esto se hace para guardar las propiedades extendidas de los archivos por separado y versionarlas cómodamente en casi cualquier software de control de versiones. Pero además de estas configuraciones, Unity también guardará configuraciones generales de activos en el archivo meta, como el GUID (propiedad "GUID") o el paquete de activos (propiedad "assetBundleName"), incluso para carpetas o archivos de formato nativo de Unity como Materiales.

Con esto en mente, puedes identificar de forma única un objeto combinando el GUID en el archivo meta y el ID de archivo del objeto dentro del YAML, como se muestra en la Figura 10. Más específicamente, puedes ver que YAML generó la variable “bulletPrefab” de un script de Arma, que hace referencia al GameObject raíz con el ID de archivo 4551470971191240028 del Prefab con el GUID afa5a3def08334b95acd2d70ee44a7c2.

También puedes ver un tercer atributo llamado “Tipo”. El tipo se utiliza para determinar si el archivo debe cargarse desde la carpeta Activos o desde la carpeta Biblioteca. Tenga en cuenta que solo admite los siguientes valores, comenzando en 2 (dado que 0 y 1 están obsoletos):
- Tipo 2: Activos que el Editor puede cargar directamente desde la carpeta Activos, como Materiales y archivos .asset
- Tipo 3: Activos que han sido procesados y escritos en la carpeta Biblioteca, y cargados desde allí por el Editor, como Prefabs, texturas y modelos 3D
Otro factor a destacar con respecto a la serialización de scripts es que el tipo YAML es el mismo para todos los scripts; solo MonoBehaviour. El script real se referencia en la propiedad “m_Script”, utilizando el GUID del archivo meta del script. Con esto podrás observar cómo se trata cada script, simplemente como un activo.

Los casos de uso para este escenario incluyen, entre otros:
- Encontrar todos los usos de un activo buscando el GUID del activo en todos los demás activos
- Reemplazar todos los usos de ese activo con otro GUID de activo en todo el proyecto
- Reemplazar un activo por otro que tenga una extensión diferente (es decir, reemplazar un archivo MP3 por un archivo WAV) eliminando el activo original, nombrando el nuevo exactamente igual con la nueva extensión y renombrando el archivo meta del activo original con la nueva extensión
- Corrección de referencias perdidas al eliminar y volver a agregar el mismo activo cambiando el GUID de la nueva versión con el GUID de la versión anterior
Al utilizar instancias Prefab en una escena, o Prefabs anidados dentro de otro Prefab, los GameObjects y componentes Prefab no se serializan en el Prefab que los usa, sino que se agrega un objeto PrefabInstance. Como puede ver en la Figura 12, PrefabInstance tiene dos propiedades clave: “m_SourcePrefab” y “m_Modifications”.

Como habrás notado, “m_SourcePrefab” es una referencia al activo prefabricado anidado. Ahora, si busca su ID de archivo en el activo prefabricado anidado, no lo encontrará. En este caso, “100100000” es el ID de archivo de un objeto creado durante la importación del Prefab, llamado Prefab Asset Handle, que no existirá en el YAML.
Además, “m_Modifications” comprende un conjunto de modificaciones o “anulaciones” realizadas al Prefab original. En la Figura 12, anulamos los ejes X, Y y Z de la posición local original de una Transformación dentro del Prefab Anidado, que puede identificarse a través de su ID de archivo en la propiedad de destino. Tenga en cuenta que la Figura 12 anterior se ha acortado para facilitar su lectura. Una PrefabInstance real normalmente tendrá más entradas en la sección m_Modifications.
Ahora, usted podría estar preguntándose, si no tenemos los objetos Prefab anidados en nuestro Prefab externo, ¿cómo hacemos referencia a los objetos en los Prefabs anidados? Para tales escenarios, Unity crea un objeto “marcador de posición” en el Prefab que hace referencia al objeto apropiado en el Prefab anidado. Estos objetos de marcador de posición están marcados con la etiqueta “eliminado”, lo que significa que están simplificados con solo las propiedades necesarias para actuar como objetos de marcador de posición.

La Figura 13 muestra de manera similar cómo tenemos una Transformación marcada con la etiqueta “stripped”, que no tiene las propiedades habituales de una Transformación (como “m_LocalPosition”). En cambio, tiene las propiedades “m_CorrespondingSourcePrefab” y “m_PrefabInstance” completas de una manera que hace referencia al activo prefabricado anidado y al objeto PrefabInstance en el archivo al que pertenece. Por encima de ella, puedes ver parte de otra transformación cuyo “m_Father” hace referencia a esta transformación de marcador de posición, lo que convierte a ese GameObject en un elemento secundario del objeto Prefab anidado. A medida que comience a hacer referencia a más objetos en los prefabricados anidados, se agregarán más de estos objetos de marcador de posición al YAML.
Convenientemente, no hay diferencia cuando se trata de variantes prefabricadas. El Prefab base de una Variante es simplemente un PrefabInstance con un Transform que no tiene padre, lo que significa que es el objeto raíz de la Variante. En la Figura 14, puede ver que la propiedad “m_TransformParent” de PrefabInstance hace referencia a “fileID: 0.” Esto significa que no tiene un padre, lo que lo convierte en el objeto raíz.

Si bien puedes usar este conocimiento para reemplazar un Prefab anidado o el Prefab base de una Variante por otro, este tipo de modificación puede ser riesgosa. Proceda con precaución y tenga una copia de seguridad por si acaso.
Comience reemplazando todas las referencias al GUID del Prefab base actual con el GUID del nuevo, tanto en el objeto PrefabInstance como en los objetos de marcador de posición. Asegúrese de tomar nota de los ID de archivo de los objetos de marcador de posición. Sus propiedades “m_CorrespondingSourceObject” no solo hacen referencia al activo, sino también a los objetos dentro de él a través de sus ID de archivo. Es muy probable que los ID de archivo de los objetos en el Prefab actual difieran de aquellos en el nuevo Prefab, y si no los soluciona, perderá anulaciones, referencias, objetos y otros datos.
Como puedes ver, cambiar una base o un prefabricado anidado no es tan sencillo como uno podría pensar. Esa es una de las principales razones por las que no se admite de forma nativa en el editor.
Hay varios escenarios en los que se podrían dejar objetos y referencias obsoletos en YAML; un caso clásico sería eliminar variables en scripts. Si agrega un script de Arma al Prefab de Jugador, deberá establecer la referencia del Prefab de Bala a un Prefab existente y luego eliminar la variable del Prefab de Bala del script de Arma. A menos que cambie y guarde nuevamente el Prefab del reproductor, volviéndolo a serializar en el proceso, la referencia de viñeta quedará en YAML. Otro ejemplo se centra en los objetos de marcador de posición de Prefabs anidados que no se eliminan cuando el objeto se elimina del Prefab original, lo que nuevamente se podría solucionar modificando y guardando el Prefab. Finalmente, la reserialización de activos podría forzarse mediante scripts con la API AssetDatabase.ForceReserializeAssets .
Pero ¿por qué Unity no elimina automáticamente las referencias obsoletas en los escenarios enumerados anteriormente? Esto se debe principalmente al rendimiento, para evitar volver a serializar todos los activos cada vez que se cambia un script o un Prefab base. Otra razón es evitar la pérdida de datos. Digamos que elimina por error una propiedad de script (como Bullet Prefab) y desea recuperarla. Solo necesitas revertir el cambio en tu script. Siempre que tenga una variable con el mismo nombre que la eliminada, sus cambios no se perderán. Lo mismo ocurriría si eliminas el prefabricado Bullet al que se hace referencia. Si recupera el Prefab exactamente como estaba, incluido el archivo meta, se conservará la referencia.
Normalmente esto no es un problema durante el tiempo de ejecución, dado que cuando Unity crea el reproductor o los direccionables, estos objetos y referencias obsoletos se borran. Pero aún así, hay algunos casos en los que las referencias obsoletas pueden causar problemas, a saber, el uso de paquetes de activos puros. El cálculo de dependencia del paquete de activos considera referencias obsoletas, que podrían crear dependencias innecesarias entre paquetes y cargar más de lo necesario en tiempo de ejecución. Vale la pena pensar en esto al utilizar paquetes de activos. Cree o utilice cualquier herramienta existente para eliminar referencias innecesarias.
Aunque puedes ignorar por completo YAML la mayor parte del tiempo, comprenderlo es útil para entender el sistema de serialización de Unity. Si bien enfrentar grandes refactorizaciones y leer o modificar el YAML directamente con herramientas de procesamiento de activos puede ser rápido y efectivo, se recomienda encarecidamente buscar soluciones basadas en la API de base de datos de activos de Unity siempre que sea posible. También es especialmente útil para resolver problemas de fusión en el control de versiones. Le recomendamos que explore la herramienta Smart Merge , que puede fusionar automáticamente Prefabs en conflicto, y lea más sobre YAML en nuestra documentaciónoficial.