Aventuras do editor de unidade: Matryoshka serializado

Breve introdução


Como regra, para chegar ao campo de uma propriedade serializada que nos interessa, a terapia manual nos aconselha a usar o método FindPropertyRelative (), no qual o nome da variável é lançado.

Por certas razões, essa abordagem nem sempre é conveniente. As razões podem ser muito diversas. Por exemplo, o nome de uma variável pode mudar, precisamos de acesso sangrado a uma propriedade não serializada, precisamos ter acesso a getter-setters ou mesmo métodos de um objeto serializado. Não faremos as perguntas “por que você precisa disso?” E “por que você não poderia ficar sem as formas tradicionais”. Suponha que precisamos - e é isso.

Então, vamos descobrir como obter o objeto com o qual estamos trabalhando, bem como todos os seus objetos-pai da propriedade serializada, e não sermos pegos na estrada de serialização cheia de armadilhas.

Atenção. Este artigo implica que você já sabe trabalhar com o UnityEditor, pelo menos uma vez escreveu PropertyDrawers personalizados e pelo menos em termos gerais, entende como uma propriedade serializada difere de um objeto serializado.


Caminho de serialização


Para começar, coloque todos os pontos sobre O.

No caso mais primitivo, temos uma certa classe de herdeiros da MonoBehaviour, e ela tem um certo campo pertencente a uma classe serializada, obviamente não é um herdeiro da vaca sagrada unitária de A.K.A. UnityEngine.Object.

public class ExamplePulsar : MonoBehaviour
{
...
        [Space][Header("Example Sandbox Inspector Field")]
        public SandboxField sandboxField;
...
}

No código acima, SandboxField é uma classe com o atributo Serializable.

Para obter acesso ao MonoBehaviour do proprietário não é um problema:

UnityEngine.Object serializationRoot = property.serializedObject.targetObject;

Se desejar, você pode fazer isso como agora, mas agora não precisamos disso. Estamos interessados ​​no próprio campo serializado para desenhá-lo com todo o blackjack, como na figura abaixo.


Podemos seguir o caminho de serialização da seguinte maneira:

string serializationPath = property.propertyPath;

No nosso caso, o caminho de serialização consistirá em um de nossos campos e retornará “sandboxField”, do qual não estamos com frio nem calor, pois para o primeiro nível de aninhamento precisamos saber apenas o nome da variável (que, aliás, foi devolvida a nós).

Observe que não há MonoBehaviour principal no caminho. Agora isso não importa, mas será importante quando começarmos a desmontar uma boneca russa que se parece com isso:
nestedClassVariable.exampleSandboxesList.Array.data [0] .evenMoreNested.Array.data [0]
Para não ser pego mais tarde, quando as propriedades estiverem aninhadas, faremos o seguinte com antecedência:

string[] path = property.propertyPath.Split('.');

Agora temos todos os nós do caminho de serialização. Mas, no caso mais primitivo, precisamos apenas do nó zero. Pegue:

string pathNode = path[0];

Acenda um pouco de reflexão e pegue o campo a partir daqui:

Type objectType = serializationRoot.GetType();
FieldInfo objectFieldInfo = objectType.GetField(pathNode);
object field = objectFieldInfo.GetValue(serializationRoot);

Deixamos nos bastidores a questão da velocidade desse empreendimento. Para um pequeno número desses campos e um pequeno aninhamento, os custos de reflexão serão significativamente menores do que para tudo o que acontece sob o capô do UnityEditor durante a renderização. Se você deseja provas - em um github, os desenvolvedores do Unity têm uma coisa tão interessante, o UnityCsReference, veja como você está, como a renderização do ObjectField é implementada, por exemplo.

Na verdade, isso não é tudo (ha ha) . Temos o campo, estamos felizes, podemos fazer o que quisermos com ele e até tentar escrever nosso próprio UnityEvent com todos os botões e ações importantes que afetam apenas o nosso campo, independentemente do objeto em que ele esteja.

Pelo menos, enquanto estiver na raiz desse objeto, tudo ficará bem, mas não será mais muito. No caminho da serialização, estamos aguardando matrizes e todos os tipos de listas, cujo principal desejo é nos colocar de tênis, alterando oportunamente o número de elementos. Mas para o inferno com isso, primeiro cavaríamos sob o próprio array.

Nosso objetivo é a resistência a essas bonecas

Se não tivéssemos matrizes no caminho de serialização, o problema seria resolvido trivialmente: percorreríamos os nós de serialização em um loop até chegarmos ao final da cadeia. Algo como o seguinte código:

object currentObject = serializationRoot;
for (int i = 0; i < directSearchDepth; i++)
{
       string pathNode = path[i];
       Type objectType = currentObject.GetType();
       FieldInfo objectFieldInfo = objectType.GetField(pathNode);
       object nextObject = objectFieldInfo.GetValue(currentObject);
       currentObject = nextObject;
}
return currentObject;

Aqui estamos aguardando duas notícias desagradáveis ​​ao mesmo tempo. Vou começar com o segundo.

A etapa de executar nextObject pode repentinamente retornar nulo em vez do objeto esperado. Normalmente, isso acontece quando criamos o objeto pai pela primeira vez no inspetor, e o caminho da serialização já existe, mas o campo correspondente não existe (isso nos trará ainda mais facilidades).

Nesse caso, seria bom adicionar imediatamente a saída do método com o retorno nulo:

object nextObject = objectFieldInfo.GetValue(currentObject);
if (nextObject == null) return null;
currentObject = nextObject;

"Espere um minuto! - você diz. "E o que fazer na OnGUI se zero nos foi devolvido?"

Resposta: nada. Nada literalmente. Basta fazer um retorno e, assim, pular esta etapa do desenho, aguardando a criação do campo. Nada terrível vai acontecer com isso.

public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
       SandboxField sandboxField = GetTarget<T>(property);
       if (sandboxField == null) return;
}

aqui GetTarget () é a função correspondente, tirando o objeto serializado da propriedade

A propósito, aconselhamo-lo a entrar no campo de seu interesse, não aqui, mas em GetPropertyHeight. Isso será necessário caso escrevamos campos expansíveis recolhíveis com tamanhos diferentes, dependendo do conteúdo. GetPropertyHeight () é chamado antes de OnGUI (), portanto, se pegarmos o campo e o escrevermos no campo de nosso PropertyDrawer, não precisaremos levá-lo novamente ao OnGUI.

Observe que uma instância do PropertyDrawer personalizado é criada sozinha para desenhar todas as propriedades serializadas visíveis no momento, e novas propriedades são lançadas nela de cima para baixo. Isso deve ser levado em consideração para não interferir no cálculo da altura da próxima propriedade; caso contrário, você poderá ter uma situação desagradável ao clicar na dobra e o campo que você espera será expandido.

Além disso, todo o ouropel responsável por exibir o campo no editor e que você deseja serializar, você deve serializar no lado da classe serializável, não no PropertyDrawer e por uma questão de precisão - coloque colchetes de compilação condicionais para que toda essa vergonha espanhola não tente ir para a compilação :

[Serializable]
public class SandboxField
{
#if UNITY_EDITOR
        public bool editorFoldout;
#endif
}

Outra armadilha que nos espera aqui: todos os campos criados através do editor queriam cuspir no construtor da classe e nos valores padrão especificados na classe. Se você faz, por exemplo, o seguinte (exemplo do meu projeto, onde era o valor dos nós da superfície da água):

[SerializeField]private int m_nodesPerUnit = 5;

Esse valor será ignorado na serialização à queima-roupa assim que você adicionar um novo item à lista. Pedir ajuda ao construtor não é menos inútil: tudo o que você escreveu lá será ignorado. O novo objeto é uma meta como um falcão, todos os seus valores são realmente os valores padrão, mas não os que você deseja ver lá, mas todos os tipos de nulos, falsos, 0, Color.clear e outras coisas obscenas.

Muleta em carne
. NonUnitySerializableClass, . , DefaultEditorObject(), .

- :

public abstract class NonUnitySerializableClass
{
        protected virtual void DefaultEditorObject()
        {
            // virtually do nothing
        }

        [SerializeField]private bool validated = false;
        public void EditorCreated(bool force = false)
        {
            if (validated && !force) return;
            DefaultEditorObject();
            validated = true;
        }

        public NonUnitySerializableClass()
        {
            EditorCreated(true);
        }
}

DefaultEditorObject(), , EditorCreated .

: .



Vamos voltar às nossas ovelhas, ou melhor, às matrizes. Outro problema que pode surgir em um estágio ainda mais precoce está nesta linha:

FieldInfo objectFieldInfo = objectType.GetField(pathNode);

O lado direito pode nos retornar zero se encontrar um array no caminho de serialização (e qualquer objeto "IList" será o array "Array"). Desagradável.

O que fazer? Tente não cair nessas situações escrevendo um manipulador:

for (int i = 0; i < pathNode.Length; i++)
{
        string pathNode = path[i];
        Type objectType = currentObject.GetType();
        FieldInfo objectFieldInfo = objectType.GetField(pathNode);

        if (objectFieldInfo == null)
        {
                if (pathNode == "Array")
                {
                        i++;
                        string nextNode = path[i];
                        string idxstr = nextNode.Substring(nextNode.IndexOf("[") + 1);
                        idxstr = idxstr.Replace("]", "");
                        int arrayNumber = Convert.ToInt32(idxstr);
                        IList collection = currentObject as IList;
                        if (collection.Count <= arrayNumber) return null;
                        currentObject = collection[arrayNumber];
                }
                else
                {
                        throw new NotImplementedException("   ");
                }
        }
        else //  ,     
        {
                object nextObject = objectFieldInfo.GetValue(currentObject);
                if (nextObject == null) return null;
                currentObject = nextObject;
        }
}
return currentObject;

Sim, podemos até entrar em uma situação desagradável aqui, quando o caminho de serialização já possui, por exemplo, o elemento data [0] ou data [1], e a matriz ainda não o implementou. Por exemplo, criamos uma lista vazia. Pedimos-lhe N elementos - e sem essa linha bonita:

if (collection.Count <= arrayNumber) return null;

... temos um monte de exceções em ronronos. E tudo o que era necessário era pular a etapa de renderização, tendo esperado até que os campos de interesse para nós fossem criados.

Ainda não me deparei com outros casos em que objectFieldInfo == null, mas ao mesmo tempo o nó de serialização não é designado como Array, portanto, lançar uma terrível exceção em uma situação excepcional e hipotética é quebrá-lo posteriormente.

Em geral, temos uma função mais ou menos funcional que nos permite extrair um campo por sua propriedade serializada. No futuro, essa função pode ser modificada forçando a extração de todos os objetos no caminho de serialização, bem como a pesquisa do "pai" mais próximo, incluindo ou excluindo matrizes ao longo do caminho.

Life hack para desenhar propriedades aninhadas
- , Rect position Rect indentedPosition = EditorGUI.IndentedRect(position). , EditorGUI, position , GUI – indentedPosition. EditorGUILayout OnGUI, ( , ).

, MonoScript ( -, ), static-, AssetDatabase, .

Obrigado pela atenção.

All Articles