O que você está procurando?
Engine & platform

Compreensão da linguagem de serialização do Unity, YAML

NICOLAS ALEJANDRO BORROMEO Nicolas Alejandro Borromeo
Jul 28, 2022|13 Min
Compreensão da linguagem de serialização do Unity, YAML
Esta página da Web foi automaticamente traduzida para sua conveniência. Não podemos garantir a precisão ou a confiabilidade do conteúdo traduzido. Se tiver dúvidas sobre a precisão do conteúdo traduzido, consulte a versão oficial em inglês da página da Web.

Você sabia que pode editar qualquer tipo de recurso sem o incômodo de lidar com linguagens de serialização como XML ou JSON no Unity Editor? Embora isso funcione na maioria das vezes, há alguns casos em que você precisa modificar seus arquivos diretamente. Pense em conflitos de mesclagem ou arquivos corrompidos como exemplos.

É por isso que, nesta postagem do blog, vamos desvendar melhor o sistema de serialização do Unity e compartilhar casos de uso do que pode ser alcançado modificando diretamente os arquivos de Asset.

Como sempre, faça backup dos seus arquivos e, de preferência, use o controle de versão para evitar a perda de dados. A modificação manual de arquivos de Asset é uma operação arriscada e não é suportada pelo Unity. Os arquivos de ativos não foram projetados para serem modificados manualmente e não geram mensagens de erro úteis para explicar o que aconteceu se e quando ocorrerem erros, o que dificulta a correção de bugs. Ao entender melhor como o Unity funciona e se preparar para resolver conflitos de mesclagem, você pode compensar as situações em que a API do banco de dados de ativos não é suficiente.

Estrutura YAML

O YAML, também conhecido como "YAML Ain't Markup Language", faz parte da família de linguagens de serialização de dados legíveis por humanos, como XML e JSON. Mas, por ser leve e relativamente simples em comparação com outras linguagens comuns, é considerada mais fácil de ler.

O Unity usa uma biblioteca de serialização de alto desempenho que implementa um subconjunto da especificação YAML. Por exemplo, linhas em branco, comentários e algumas outras sintaxes compatíveis com YAML não são compatíveis com arquivos Unity. Em certos casos extremos, o formato do Unity diverge da especificação YAML.

Vamos explorar isso examinando um trecho de código YAML em um Cube Prefab. Primeiro, crie um cubo padrão no Unity, converta-o em um Prefab e abra o arquivo Prefab em qualquer editor de texto. Como você pode ver na Figura 1, as duas primeiras linhas são cabeçalhos que não serão repetidos posteriormente. O primeiro define qual versão do YAML você está usando, enquanto o segundo cria uma macro chamada "!u!" para o prefixo URI "tag:unity3d.com,2011:" (discutido abaixo).

Código das linhas de cabeçalho no formato YAML
Código das linhas de cabeçalho no formato YAML

Após os cabeçalhos, você encontrará uma série de definições de objetos, como GameObjects em um Prefab ou cena, os componentes de cada GameObject e possivelmente outros objetos, como configurações de Lightmap para cenas.

YAML para um GameObject chamado Cube
YAML para um GameObject chamado Cube

Cada definição de objeto começa com um cabeçalho de duas linhas, como o do nosso exemplo da Figura 2: "--- !u!1 &7618609094792682308" segue o formato "--- !u!{CLASS ID} &{FILE ID}", que pode ser analisado em duas partes:

  • !u!{CLASS ID}:Informa ao Unity a qual classe o objeto pertence. A parte "!u!" será substituída pela macro definida anteriormente, deixando-nos com "tag:unity3d.com,2011:1" - o número 1 referindo-se ao ID do GameObject nesse caso. Cada ID de classe é definido no código-fonte do Unity, mas uma lista completa deles pode ser encontrada aqui.
  • &{FILE ID}Esta parte define o ID do próprio objeto, que é usado para fazer referência a objetos entre si. É chamado de File ID porque representa o ID do objeto em um arquivo específico. Continue lendo para obter mais informações sobre referências de arquivos cruzados mais adiante nesta postagem.

A segunda linha de cabeçalho do objeto é o nome do tipo de objeto (aqui, GameObject), que permite identificá-lo ao ler o arquivo.

Formato do cabeçalho
Formato do cabeçalho

Após o cabeçalho do objeto, você pode encontrar todas as propriedades serializadas. Em nosso exemplo de GameObject acima, a Figura 2 fornece detalhes como seu nome (m_Name: Cube) e camada (m_Layer: 0). No caso da serialização do MonoBehaviour, você notará os campos públicos e os privados com o atributo SerializeField. Esse formato é usado de forma semelhante para ScriptableObjects, Animations, Materials e assim por diante. Observe que os ScriptableObjects usam o MonoBehaviour como seu tipo de objeto, em vez de definir o seu próprio. Isso ocorre porque a mesma classe MonoBehaviour interna também os hospeda.

Refatoração rápida com YAML

Com o que abordamos até agora, você pode começar a aproveitar o poder de modificar o YAML para fins como refatoração de trilhas de animação.

Os arquivos de animação do Unity funcionam descrevendo um conjunto de trilhas ou curvas de animação, uma para cada propriedade que você deseja animar. Conforme mostrado na Figura 4, uma Curva de animação identifica o objeto que precisa animar por meio da propriedade do caminho, que contém os nomes dos GameObjects filhos até o objeto específico. Neste exemplo, estamos animando um GameObject chamado "JumpingCharacter" - um filho do GameObject "Shoulder", que é um filho do GameObject que tem o componente Animator reproduzindo essa animação. Para aplicar a mesma animação a diferentes objetos, o sistema de animação usa caminhos baseados em strings em vez de IDs de GameObject.

Código da propriedade de caminho de uma curva de animação
Código da propriedade de caminho de uma curva de animação

Renomear um objeto animado na hierarquia pode levar a um problema muito comum: A curva pode perder o controle. Embora isso geralmente seja resolvido renomeando cada trilha de animação na janela Animação, há casos em que várias animações com várias curvas são aplicadas ao mesmo objeto, o que torna o processo lento e propenso a erros. Em vez disso, a edição YAML permite corrigir vários caminhos de Curva de Animação de uma só vez usando uma operação clássica de "pesquisar e substituir" nos arquivos de animação com o editor de texto com o qual você está mais familiarizado.

YAML original e hierarquia à esquerda, versão renomeada do GameObject à direita
YAML original e hierarquia à esquerda, versão renomeada do GameObject à direita
Referências locais

Conforme mencionado anteriormente, cada objeto em um arquivo YAML tem um ID conhecido como "ID do arquivo". Essa ID é exclusiva para cada objeto dentro do arquivo e serve para resolver referências entre eles. Pense em um GameObject e seus componentes, os componentes e seu GameObject, ou até mesmo referências de script, como uma referência de componente "Weapon" a um GameObject "SpawnPoint" no mesmo Prefab.

O formato YAML para isso é "{fileID: FILE ID}" como o valor da propriedade. Na Figura 6, você pode ver como essa transformação pertence a um GameObject com o ID 4112328598445621100, já que sua propriedade "m_GameObject" faz referência a ele por meio do ID do arquivo. Você também pode observar exemplos de referências nulas, como "m_PrefabInstance" (já que sua ID de arquivo é zero). Continue lendo para saber mais sobre instâncias pré-fabricadas.

Código de transformação associado a um GameObject específico
Código de transformação associado a um GameObject específico

Vamos considerar o caso de reparar objetos dentro de um Prefab. Você pode alterar o ID do arquivo da propriedade "m_Father" de uma transformação com o ID do arquivo da nova transformação de destino e até mesmo corrigir o YAML da transformação pai antiga para remover esse objeto de sua matriz "m_Children" e adicioná-lo à nova propriedade pai "m_Children".

Transformar com um pai e um único filho
Transformar com um pai e um único filho

Para localizar uma transformação específica pelo nome, você deve determinar primeiramente o ID do arquivo do GameObject pesquisando aquele com o m_Name que você está procurando. Só então você poderá localizar a transformação cuja propriedade m_GameObject faz referência a esse ID de arquivo.

Meta-arquivos e referências entre arquivos

Ao fazer referência a objetos fora desse arquivo, como um script "Weapon" que faz referência a um Prefab "Bullet", as coisas ficam um pouco mais complexas. Lembre-se de que o ID do arquivo é local ao arquivo, o que significa que pode ser repetido em arquivos diferentes. Para identificar exclusivamente um objeto em outro arquivo, precisamos de um ID adicional ou "GUID" que identifique o arquivo inteiro em vez de objetos individuais dentro dele. Cada ativo tem essa propriedade GUID definida em seu meta-arquivo, que pode ser encontrado na mesma pasta que o arquivo original, com exatamente o mesmo nome e uma extensão ".meta" adicionada.

Imagem de uma lista de ativos do Unity e seus arquivos meta
Imagem de uma lista de ativos do Unity e seus arquivos meta

Para formatos de arquivo não nativos da Unity, como imagens PNG ou arquivos FBX, a Unity serializa configurações extras de importação para eles nos arquivos meta, como a resolução máxima e o formato de compactação de uma textura ou o fator de escala de um modelo 3D. Isso é feito para salvar as propriedades estendidas do arquivo separadamente e, convenientemente, versioná-las em praticamente qualquer software de controle de versão. Mas, além dessas configurações, o Unity também salvará configurações gerais de Asset no meta-arquivo, como o GUID (propriedade "GUID") ou o Asset Bundle (propriedade "assetBundleName"), mesmo para pastas ou arquivos de formato nativo do Unity, como Materials.

Código do arquivo Meta para uma textura
Código do arquivo Meta para uma textura

Com isso em mente, você pode identificar exclusivamente um objeto combinando o GUID no meta-arquivo e o ID do arquivo do objeto dentro do YAML, conforme mostrado na Figura 10. Mais especificamente, você pode ver que o YAML gerou a variável "bulletPrefab" de um script Weapon, que faz referência ao GameObject raiz com o ID de arquivo 4551470971191240028 do Prefab com o GUID afa5a3def08334b95acd2d70ee44a7c2.

Código de referência a outro objeto de arquivo
Código de referência a outro objeto de arquivo

Você também pode ver um terceiro atributo chamado "Type" (Tipo). O tipo é usado para determinar se o arquivo deve ser carregado da pasta Assets (Ativos) ou da pasta Library (Biblioteca). Observe que ele só é compatível com os seguintes valores, a partir de 2 (já que 0 e 1 estão obsoletos):

  • Tipo 2: Ativos que podem ser carregados diretamente da pasta Assets pelo Editor, como materiais e arquivos .asset
  • Tipo 3: Ativos que foram processados e gravados na pasta Library e carregados de lá pelo Editor, como Prefabs, texturas e modelos 3D

Outro fator a ser destacado com relação à serialização de scripts é que o tipo YAML é o mesmo para todos os scripts; apenas MonoBehaviour. O script real é referenciado na propriedade "m_Script", usando o GUID do meta-arquivo do script. Com isso, você pode observar como cada script é tratado, apenas como um ativo.

YAML do MonoBehaviour fazendo referência a um ativo de script
YAML do MonoBehaviour fazendo referência a um ativo de script

Os casos de uso para esse cenário incluem, mas não se limitam a:

  • Encontrar todos os usos de um ativo pesquisando o GUID do ativo em todos os outros ativos
  • Substituir todos os usos desse ativo por outro GUID de ativo em todo o projeto
  • Substituir um ativo por outro que tenha uma extensão diferente (ou seja, substituir um arquivo MP3 por um arquivo WAV) excluindo o ativo original, nomeando o novo exatamente da mesma forma com a nova extensão e renomeando o meta-arquivo do ativo original com a nova extensão
  • Correção de referências perdidas ao excluir e adicionar novamente o mesmo ativo, alterando o GUID da nova versão com o GUID da versão antiga
Instâncias de pré-fabricados, pré-fabricados aninhados e variantes

Ao usar instâncias de Prefab em uma cena, ou Prefabs aninhados dentro de outro Prefab, os GameObjects e componentes do Prefab não são serializados no Prefab que os usa, mas um objeto PrefabInstance é adicionado. Como você pode ver na Figura 12, o PrefabInstance tem duas propriedades principais: "m_SourcePrefab" e "m_Modifications".

YAML para um Prefab aninhado
YAML para um Prefab aninhado

Como você deve ter notado, "m_SourcePrefab" é uma referência ao Nested Prefab Asset. Agora, se você procurar o ID do arquivo no Nested Prefab Asset, não o encontrará. Nesse caso, "100100000" é o ID do arquivo de um objeto criado durante a importação do Prefab, chamado Prefab Asset Handle, que não existirá no YAML.

Além disso, "m_Modifications" inclui um conjunto de modificações ou "substituições" feitas no Prefab original. Na Figura 12, substituímos os eixos X, Y e Z da posição local original de uma transformação dentro do Prefab aninhado, que pode ser identificado por meio de seu ID de arquivo na propriedade de destino. Observe que a Figura 12 acima foi encurtada para facilitar a leitura. Uma PrefabInstance real normalmente terá mais entradas na seção m_Modifications.

Agora, você deve estar se perguntando: se não tivermos os objetos do Prefab aninhado em nosso Prefab externo, como faremos referência aos objetos nos Prefabs aninhados? Para esses cenários, o Unity cria um objeto de "espaço reservado" no Prefab que faz referência ao objeto adequado no Prefab aninhado. Esses objetos de espaço reservado são marcados com a tag "stripped", o que significa que eles são simplificados apenas com as propriedades necessárias para atuar como objetos de espaço reservado.

Placeholder Nested Prefab Transform a ser referenciado por seus filhos
Placeholder Nested Prefab Transform a ser referenciado por seus filhos

A Figura 13 mostra de forma semelhante como temos uma Transform marcada com a tag "stripped", que não tem as propriedades usuais de uma Transform (como "m_LocalPosition"). Em vez disso, ele tem as propriedades "m_CorrespondingSourcePrefab" e "m_PrefabInstance" preenchidas de forma a fazer referência ao Nested Prefab Asset e ao objeto PrefabInstance no arquivo ao qual pertence. Acima dele, você pode ver parte de outra transformação cujo "m_Father" faz referência a essa transformação de espaço reservado, tornando esse GameObject um filho do objeto Prefab aninhado. À medida que você começar a fazer referência a mais objetos nos Prefabs aninhados, mais desses objetos de espaço reservado serão adicionados ao YAML.

Convenientemente, não há diferença quando se trata de variantes de pré-fabricados. O Prefab básico de uma Variant é apenas uma PrefabInstance com uma Transform que não tem pai, o que significa que é o objeto raiz da Variant. Na Figura 14, você pode ver que a propriedade "m_TransformParent" da PrefabInstance faz referência a "fileID: 0.” Isso significa que ele não tem um pai, o que o torna o objeto raiz.

Código da instância do Prefab sem pai, tornando-o o Prefab base do arquivo
Código da instância do Prefab sem pai, tornando-o o Prefab base do arquivo

Embora você possa usar esse conhecimento para substituir um Prefab aninhado ou o Prefab básico de uma Variant por outro, esse tipo de modificação pode ser arriscado. Proceda com cautela e tenha um backup para o caso de precisar.

Comece substituindo todas as referências ao GUID do Prefab básico atual pelo GUID do novo, tanto no objeto PrefabInstance quanto nos objetos de espaço reservado. Não se esqueça de anotar os IDs de arquivo dos objetos de espaço reservado. Suas propriedades "m_CorrespondingSourceObject" não apenas fazem referência ao ativo, mas também aos objetos dentro dele por meio de seus IDs de arquivo. É muito provável que os IDs de arquivo dos objetos no Prefab atual sejam diferentes dos do novo Prefab e, se você não os corrigir, perderá substituições, referências, objetos e outros dados.

Como você pode ver, alterar uma base ou um Prefab aninhado não é tão simples quanto se imagina. Esse é um dos principais motivos pelos quais não há suporte nativo no Editor.

Referências obsoletas

Há vários cenários em que objetos e referências obsoletos podem ser deixados no YAML; um caso clássico seria a remoção de variáveis em scripts. Se você adicionar um script de arma ao Prefab do jogador, terá que definir a referência do Prefab da bala para um Prefab existente e, em seguida, remover a variável do Prefab da bala do script da arma. A menos que você altere e salve o Player Prefab novamente, re-serializando-o no processo, a referência do marcador será deixada no YAML. Outro exemplo se refere a objetos de espaço reservado de Prefabs aninhados que não são removidos quando o objeto é excluído do Prefab original, o que, novamente, pode ser corrigido alterando e salvando o Prefab. Por fim, a nova serialização de ativos pode ser forçada por meio de scripts com a API AssetDatabase.ForceReserializeAssets.

Mas por que o Unity não elimina automaticamente as referências obsoletas nos cenários listados acima? Isso se deve principalmente ao desempenho; para evitar a re-serialização de todos os ativos toda vez que você alterar um script ou Prefab básico. Outro motivo é evitar a perda de dados. Digamos que você tenha removido por engano uma propriedade de script (como Bullet Prefab) e queira recuperá-la. Você só precisa reverter a alteração em seu script. Desde que você tenha uma variável com o mesmo nome da variável removida, suas alterações não serão perdidas. A mesma coisa aconteceria se você excluísse o Bullet Prefab referenciado. Se você recuperar o Prefab exatamente como ele era, incluindo o meta-arquivo, a referência será preservada.

Normalmente, isso não é um problema durante o tempo de execução, já que quando o Unity constrói o Player ou Addressables, esses objetos e referências obsoletos são limpos. Mas, mesmo assim, há alguns casos em que referências obsoletas podem causar problemas, ou seja, o uso de Asset Bundles puros. O cálculo da dependência do Asset Bundle considera referências obsoletas, que podem criar dependências desnecessárias entre os pacotes, carregando mais do que o necessário em tempo de execução. Vale a pena pensar nisso ao usar os Asset Bundles. Crie ou use qualquer ferramenta existente para eliminar referências desnecessárias.

Conclusão

Embora você possa ignorar completamente o YAML na maior parte do tempo, entendê-lo é útil para compreender o sistema de serialização do Unity. Embora enfrentar grandes refatorações e ler ou modificar o YAML diretamente com ferramentas de processamento de ativos possa ser rápido e eficaz, é altamente recomendável procurar soluções baseadas na API do Unity Asset Database sempre que possível. Ele também é particularmente útil para resolver problemas de mesclagem no controle de versão. Recomendamos que você explore a ferramenta Smart Merge, que pode mesclar automaticamente Prefabs conflitantes, e leia mais sobre YAML em nossa documentação oficial.