Editor de lógica visual para Unity3d. Parte 2

1. Introdução


Olá queridos leitores, no artigo de hoje, gostaria de destacar o tema da arquitetura do kernel do editor de lógica visual do Unity3d . Esta é a segunda parte da série. Você pode ler o anterior aqui . Então, sobre o que vamos falar? O editor visual é baseado no núcleo principal, que permite executar, carregar e armazenar dados lógicos. Por sua vez, o kernel usa, como mencionado no artigo anterior, ScriptableObject , como uma classe base para trabalhar com componentes lógicos. Vamos considerar com mais detalhes todos esses aspectos.

Artigos da série:

Editor de lógica visual para Unity3d. Parte 1

Por que ScriptableObject?


Antes de iniciar o desenvolvimento, pensei por um longo tempo sobre o que construir o sistema. Na primeira forma - era o MonoBehaviour , mas tive que abandonar essa ideia, pois esses scripts deveriam ficar no GameObject , como componentes. O próximo passo foi a idéia de usar sua própria classe, que não é descendente do UnityEngine.Object . Mas essa opção não criou raízes, embora estivesse funcionando muito bem, mas arrastou-se, escrevendo seu serializador, inspetor, coletor de lixo etc. Como resultado, a única maneira razoável era usar o ScriptableObject , cujo ciclo de vida é semelhante ao MonoBehaviour , se a criação ocorre enquanto o aplicativo está sendo executado por meio de ScriptableObject.CreateInstance . Além disso, esse problema foi resolvido automaticamente usando o JsonUtility (embora agora isso não seja mais um problema) e o inspetor do Unity .

Arquitetura


Abaixo está um diagrama generalizado do que consiste o núcleo uViLEd .

imagem

Vamos considerar cada elemento em mais detalhes.

Controlador


O controlador é o elemento principal do kernel, que é um script MonoBehavior (o único em todo o sistema). A classe do controlador é um singleton e é acessível a todos os componentes da lógica. O que esta classe faz:

  1. Armazena links de objetos da unidade
  2. Inicia a lógica no início da cena
  3. Representa o acesso a métodos para executar lógica de fontes externas
  4. Fornece o trabalho de métodos Mono em componentes

Código base da classe do controlador
namespace uViLEd.Core { public partial class LogicController : MonoBehaviour { private static LogicController _instance; public static LogicController Instance { get { _instance = _instance ?? FindObjectOfType<LogicController>(); return _instance; } } [Serializable] public class SceneLogicData { public string Name; public TextAsset BinaryData => _data; public string Id => _id; [SerializeField] private string _id; [SerializeField] private TextAsset _data; public SceneLogicData(string id, string name, TextAsset binaryData) { _id = id; _data = binaryData; Name = name; } } [HideInInspector] public List<SceneLogicData> SceneLogicList = new List<SceneLogicData>(); void Awake() { _instance = this; foreach (var sceneLogic in SceneLogicList) { RunLogicInternal(LogicStorage.Load(sceneLogic.BinaryData)); } } } } 


Nota : cada cena tem seu próprio controlador e seu próprio conjunto de lógica.
Nota : mais detalhes sobre o carregamento de dados lógicos e seu lançamento serão discutidos separadamente.

Elementos principais do kernel do uViLEd


Componente


Na primeira parte, eu já disse que o componente é um ScriptableObject . Todos os componentes são descendentes da classe LogicComponent , que por sua vez é simples.

 namespace uViLEd.Core { public abstract class LogicComponent : ScriptableObject { protected MonoBehaviour coroutineHost => _logicHost; private MonoBehaviour _logicHost; public virtual void Constructor() { } } } 

Aqui, coroutineHost é um link para um controlador lógico, que foi introduzido apenas por conveniência e é usado, como o nome indica, para trabalhar com corotinas. O uso dessa abstração é necessário para separar os componentes de outro código presente no projeto do Unity .

Variáveis


Variáveis, conforme mencionado em um artigo anterior, são componentes especializados para armazenar dados, o código para elas é apresentado abaixo.

Código de implementação variável
 namespace uViLEd.Core { public abstract class Variable : LogicComponent { } public abstract class Variable<T> : Variable { public delegate void Changed(); public delegate void Set(T newValue); public event Changed OnChanged; public event Set OnSet; public T Value { get { return _value; }set { var changed = false; if (_value == null && value != null || (_value != null && !_value.Equals(value))) { changed = true; } _value = value; if (changed) { OnChanged?.Invoke(); } OnSet?.Invoke(_value); } } [SerializeField] private T _value; public virtual void OnDestroy() { if(OnSet != null) { foreach (var eventHandler in OnSet.GetInvocationList()) { OnSet -= (Set)eventHandler; } } if(OnChanged != null) { foreach (var eventHandler in OnChanged.GetInvocationList()) { OnChanged -= (Changed)eventHandler; } } } } } 


Aqui Variable é uma classe abstrata básica para todas as variáveis, é necessário separá-las dos componentes comuns. A classe principal é genérica , que armazena os dados em si e fornece eventos para definir o valor e alterá-lo.

Comunicações


Quais são as conexões, eu disse em um artigo anterior. Resumidamente, é uma entidade virtual que permite que os componentes usem os métodos uns dos outros e também se refira a variáveis ​​lógicas. Para o programador, essa conexão é suave e não é visível no código. Todas as comunicações são formadas durante o processo de inicialização (leia sobre isso abaixo). Considere as classes que permitem formar relacionamentos.

Ponto de entrada
 namespace uViLEd.Core { public class INPUT_POINT<T> { public Action<T> Handler; } public class INPUT_POINT { public Action Handler; } } 


Ponto de saída
 namespace uViLEd.Core { public class OUTPUT_POINT<T> { private List<Action<T>> _linkedInputPoints = new List<Action<T>>(); public void Execute(T param) { foreach(var handler in _linkedInputPoints) { handler(param); } } } public class OUTPUT_POINT { private List<Action> _linkedInputPoints = new List<Action>(); public void Execute() { foreach (var handler in _linkedInputPoints) { handler(); } } } } 


Referência variável
 namespace uViLEd.Core { public class VARIABLE_LINK<T> { public T Value { get => _variable.Value; set => _variable.Value = value; } private Variable<T> _variableProperty { get => _variable; set { _variable = value; VariableWasSet = true; InitializeEventHandlers(); } } public bool VariableWasSet { get; private set; } = false; private Variable<T> _variable; private Variable<T>.Set _automaticSetHandler; private Variable<T>.Changed _automaticChangedHandler; public void AddSetEventHandler(Variable<T>.Set handler) { if (VariableWasSet) { _variable.OnSet += handler; }else { _automaticSetHandler = handler; } } public void RemoveSetEventHandler(Variable<T>.Set handler) { if (VariableWasSet) { _variable.OnSet -= handler; } } public void AddChangedEventHandler(Variable<T>.Changed handler) { if (VariableWasSet) { _variable.OnChanged += handler; }else { _automaticChangedHandler = handler; } } public void RemoveChangedEventHandler(Variable<T>.Changed handler) { if (VariableWasSet) { _variable.OnChanged -= handler; } } private void InitializeEventHandlers() { if (_automaticSetHandler != null) { _variable.OnSet += _automaticSetHandler; } if (_automaticChangedHandler != null) { _variable.OnChanged += _automaticChangedHandler; } } } } 


Nota : aqui vale a pena explicar um ponto: manipuladores automáticos para definir eventos e alterar o valor de uma variável são usados ​​somente quando definidos no método Constructor , pois, naquele momento, as referências às variáveis ​​ainda não estavam definidas.

Trabalhar com lógica


Armazenamento


No primeiro artigo sobre o editor de lógica visual, foi mencionado que a lógica é um conjunto de variáveis, componentes e os relacionamentos entre eles:

 namespace uViLEd.Core { [Serializable] public partial class LogicStorage { public string Id = string.Empty; public string Name = string.Empty; public string SceneName = string.Empty; public ComponentsStorage Components = new ComponentsStorage(); public LinksStorage Links = new LinksStorage(); } } 

Essa classe é aparentemente serializável, mas o JsonUtility do Unity não é usado para serialização. Em vez disso, é usada uma opção binária, cujo resultado é salvo como um arquivo com a extensão de bytes . Por que isso é feito? Em geral, o principal motivo é a segurança, ou seja, para a opção de carregar lógica de uma fonte externa, é possível criptografar dados e, em geral, desserializar uma matriz de bytes é mais difícil que o json aberto.

Vamos dar uma olhada nas classes ComponentsStrorage e LinksStorage . O sistema usa um GUID para identificação global de dados. Abaixo está o código da classe, que é a base para os contêineres de dados.

 namespace uViLEd.Core { [Serializable] public abstract class Identifier { public string Id { get; } public Identifier() { if (!string.IsNullOrEmpty(Id)) return; Id = System.Guid.NewGuid().ToString(); } } } 

Agora considere o código da classe ComponentsStorage , que, como o nome indica, armazena dados sobre os componentes da lógica:

 namespace uViLEd.Core { public partial class LogicStorage { [Serializable] public class ComponentsStorage { [Serializable] public class ComponentData : Identifier { public string Type = string.Empty; public string Assembly = string.Empty; public string JsonData = string.Empty; public bool IsActive = true; } public List<ComponentData> Items = new List<ComponentData>(); } } 

A aula é bem simples. As seguintes informações são armazenadas para cada componente:

  1. Identificador exclusivo (sequência GUID ) encontrado em Identificador
  2. Digite o nome
  3. O nome da montagem na qual o tipo de componente está localizado
  4. Cadeia Json com dados de serialização (resultado de JsonUtility.ToJson )
  5. Sinalizador de atividade (estado) do componente

Agora vamos dar uma olhada na classe LinksStorage . Esta classe armazena informações sobre os relacionamentos entre componentes, bem como sobre referências a variáveis.

 namespace uViLEd.Core { public partial class LogicStorage { [Serializable] public class LinksStorage { [Serializable] public class LinkData : Identifier { public bool IsVariable; public bool IsActive = true; public string SourceComponent = string.Empty; public string TargetComponent = string.Empty; public string OutputPoint = string.Empty; public string InputPoint = string.Empty; public string VariableName = string.Empty; public int CallOrder = -1; } public List<LinkData> Items = new List<LinkData>(); } } 

Em princípio, também não há nada complicado nesta classe. Cada link contém as seguintes informações:

  1. Sinalizador indicando que este link é uma referência a uma variável
  2. Sinalizador de atividade de link
  3. O identificador (sequência GUID ) do componente com o ponto de saída
  4. O identificador (string GUID ) do componente do ponto de entrada
  5. O nome do ponto de saída do componente de origem de comunicação
  6. Nome do ponto de entrada do componente de comunicação de destino
  7. Nome do campo da classe para definir uma referência de variável
  8. Ordem de chamada de comunicação

Executar a partir do repositório


Antes de entrar nos detalhes do código, primeiro quero me concentrar na descrição da sequência de como o controlador inicia a lógica:

  1. A inicialização é iniciada no método Awake do controlador
  2. A lista de lógicas de cena carrega e desserializa os dados lógicos de um arquivo binário ( TextAsset )
  3. Para cada lógica ocorre:
    • Criação de componentes
    • Classificando links por CallOrder
    • Configurando links e referências de variáveis
    • Classificando métodos de componente mono por ExecuteOrder


Vamos considerar mais detalhadamente cada aspecto dessa cadeia.

Serialização e desserialização binárias
 namespace uViLEd { public class Serialization { public static void Serialize(object data, string path, string fileName) { var binaryFormatter = new BinaryFormatter(); if (!Directory.Exists(path)) { Directory.CreateDirectory(path); } using (var fs = new FileStream(Path.Combine(path, fileName), FileMode.OpenOrCreate)) { binaryFormatter.Serialize(fs, data); } } public static object Deserialize(TextAsset textAsset) { var binaryFormatter = new BinaryFormatter(); using (var memoryStream = new MemoryStream(textAsset.bytes)) { return binaryFormatter.Deserialize(memoryStream); } } } } 


Carregando dados lógicos de um ativo de texto (arquivo binário)
 namespace uViLEd.Core { public partial class LogicStorage { public static LogicStorage Load(TextAsset textAsset) => Serialization.Deserialize(textAsset) as LogicStorage; } } 


Lançamento da lógica
 private void RunLogicInternal(LogicStorage logicStorage) { var instances = new Dictionary<string, LogicComponent>(); foreach (var componentData in logicStorage.Components.Items) { CreateComponent(componentData, instances); } logicStorage.Links.Items.Sort(SortingLinks); foreach (var linkData in logicStorage.Links.Items) { CreateLink(linkData, instances); } foreach (var monoMethods in _monoBehaviourMethods.Values) { monoMethods.Sort(SortingMonoMethods); } } 


Criação de componentes
 private void CreateComponent(LogicStorage.ComponentsStorage.ComponentData componentData, IDictionary<string, LogicComponent> instances, IList<IDisposable> disposableInstance, IDictionary<string, List<MonoMethodData>> monoMethods) { if (!componentData.IsActive) return; var componentType = AssemblyHelper.GetAssemblyType(componentData.Assembly, componentData.Type); var componentInstance = ScriptableObject.CreateInstance(componentType) as LogicComponent; JsonUtility.FromJsonOverwrite(componentData.JsonData, componentInstance); componentInstance.name = componentData.InstanceName; componentType.GetFieldRecursive(_LOGIC_HOST_STR).SetValue(componentInstance, this as MonoBehaviour); componentInstance.Constructor(); instances.Add(componentData.Id, componentInstance); if(componentInstance is IDisposable) { disposableInstance.Add((IDisposable)componentInstance); } SearchMonoBehaviourMethod(componentInstance, monoMethods); } 

Então, o que acontece nesta função:

  1. O sinalizador de atividade do componente está marcado
  2. Obtendo o tipo de componente da montagem
  3. Uma instância do componente é criada por tipo
  4. Desserialização de parâmetro de componente de json
  5. O link está definido na coroutineHost
  6. Método construtor chamado
  7. Uma cópia temporária é salva na instância do componente
  8. Se o componente implementa a interface IDisposable, o link para ele é armazenado na lista correspondente
  9. Procurando métodos Mono no componente


Criando Links
 private void CreateLink(LogicStorage.LinksStorage.LinkData linkData, Dictionary<string, LogicComponent> instances) { if (!linkData.IsActive) return; var sourceComponent = instances.ContainsKey(linkData.SourceComponent) ? instances[linkData.SourceComponent] : null; if (sourceComponent == null) return; var targetComponent = instances.ContainsKey(linkData.TargetComponent) ? instances[linkData.TargetComponent] : null; if (targetComponent == null) return; if (linkData.IsVariable) { var variableLinkFieldInfo = sourceComponent.GetType().GetField(linkData.variableName); if (variableLinkFieldInfo != null) { var variableLinkFieldValue = variableLinkFieldInfo.GetValue(sourceComponent); var variableLinkVariablePropertyInfo = variableLinkFieldInfo.FieldType.GetProperty(_VARIABLE_PROPERTY_STR, BindingFlags.NonPublic | BindingFlags.Instance); variableLinkVariablePropertyInfo.SetValue(variableLinkFieldValue, targetComponent, null); } } else { object handlerValue; MethodInfo methodListAdd; object linkedInputPointsFieldValue; Type outputPointType; object outputPoint; var outputPointParse = sourceComponent as IOutputPointParse; var inputPointParse = targetComponent as IInputPointParse; if (outputPointParse != null) { var outputPoints = outputPointParse.GetOutputPoints(); if (outputPoints.ContainsKey(linkData.OutputPoint)) { outputPoint = outputPoints[linkData.OutputPoint]; if (outputPoint is FieldInfo) { outputPoint = sourceComponent.GetType().GetField(linkData.OutputPoint).GetValue(sourceComponent); } outputPointType = outputPoint.GetType(); var linkedInputPointsFieldInfo = outputPointType.GetField(_LINKED_INPUT_POINTS_STR, BindingFlags.NonPublic | BindingFlags.Instance); linkedInputPointsFieldValue = linkedInputPointsFieldInfo.GetValue(outputPoint); methodListAdd = linkedInputPointsFieldInfo.FieldType.GetMethod(_ADD_STR); } } else { var outputPointFieldInfo = sourceComponent.GetType().GetField(linkData.OutputPoint); outputPoint = outputPointFieldInfo.GetValue(sourceComponent); if (outputPoint != null) { outputPointType = outputPoint.GetType(); var linkedInputPointsFieldInfo = outputPointFieldInfo.FieldType.GetField(_LINKED_INPUT_POINTS_STR, BindingFlags.NonPublic | BindingFlags.Instance); linkedInputPointsFieldValue = linkedInputPointsFieldInfo.GetValue(outputPoint); methodListAdd = linkedInputPointsFieldInfo.FieldType.GetMethod(_ADD_STR); } } if (inputPointParse != null) { var inputPoints = inputPointParse.GetInputPoints(); if (inputPoints.ContainsKey(linkData.InputPoint)) { var inputPoint = inputPoints[linkData.InputPoint]; if (inputPoint is FieldInfo) { inputPoint = targetComponent.GetType().GetField(linkData.InputPoint).GetValue(targetComponent); } var inputPointType = inputPoint.GetType(); var inputPointHandlerFieldInfo = inputPointType.GetField(_HANDLER_STR); handlerValue = inputPointHandlerFieldInfo.GetValue(inputPoint); } } else { var inputPointFieldInfo = targetComponent.GetType().GetField(linkData.InputPoint); var inputPointFieldValue = inputPointFieldInfo.GetValue(targetComponent); if (inputPointFieldValue != null) { var inputPointHandlerFieldInfo = inputPointFieldInfo.FieldType.GetField(_HANDLER_STR); handlerValue = inputPointHandlerFieldInfo.GetValue(inputPointFieldValue); } } var handlerParsedAction = GetParsedHandler(handlerValue, outputPoint); methodListAdd.Invoke(linkedInputPointsFieldValue, new object[] { handlerParsedAction }); } } private object GetParsedHandler(object handlerValue, object outputPoint) { var inputPointType = handlerValue.GetType(); var outputPointType = outputPoint.GetType(); if (inputPointType.IsGenericType) { var paramType = inputPointType.GetGenericArguments()[0]; if (paramType == typeof(object) && outputPointType.IsGenericType) { var parsingActionMethod = outputPointType.GetMethod(_PARSING_ACTION_OBJECT_STR, BindingFlags.NonPublic | BindingFlags.Instance); return parsingActionMethod.Invoke(outputPoint, new object[] { handlerValue }); } else { return handlerValue; } } else { if (outputPointType.IsGenericType) { var parsingActionMethod = outputPointType.GetMethod(_PARSING_ACTION_EMPTY_STR, BindingFlags.NonPublic | BindingFlags.Instance); return parsingActionMethod.Invoke(outputPoint, new object[] { handlerValue }); } else { return handlerValue; } } } 

Criando uma conexão, um dos momentos mais intrinsecamente difíceis de todo o sistema, consideraremos cada estágio:

  1. Verifica o sinalizador de atividade de comunicação
  2. Na lista temporária de componentes criados são pesquisados, o componente de origem da conexão e o componente de destino
  3. Verifica o tipo de conexão:
    • Se o tipo de conexão for uma referência a uma variável, os valores necessários serão definidos usando reflexão
    • Se a conexão for normal, a reflexão também será usada para definir os valores necessários para os métodos de pontos de saída e entrada

  4. Para comunicações normais, verifique primeiro se o componente herda as interfaces IInputPointParse e IOutputPointParse .
  5. Dependendo dos resultados do parágrafo anterior, o campo LinkedInputPoints no componente de origem e o método manipulador no componente de destino são obtidos por meio de reflexão.
  6. Os manipuladores de método são obtidos convertendo a chamada de método ignorando MethodInfo.Invoke em uma chamada simples Action<T> . No entanto, obter esse link por meio de reflexão é muito complicado, portanto, métodos especiais foram OUTPUT_POINT<T> na classe OUTPUT_POINT<T> que permite isso. Para uma versão não genérica dessa classe, nenhuma ação é necessária.

     private Action<T> ParsingActionEmpty(Action action) { Action<T> parsedAction = (value) => action(); return parsedAction; } private Action<T> ParsingActionObject(Action<object> action) { Action<T> parsedAction = (value) => action(value); return parsedAction; } } 

    O primeiro método é usado quando o ponto de entrada não aceita nenhum parâmetro. O segundo método, respectivamente, para o ponto de entrada com um parâmetro

  7. Por meio da reflexão, um link para o manipulador de ação é adicionado ao campo LinkedInputPoints (que, como mostrado acima, é uma lista)


Trabalhando com métodos mono
 private void SearchMonoBehaviourMethod(LogicComponent component, IDictionary<string, List<MonoMethodData>> monoBehaviourMethods) { var type = component.GetType(); var methods = type.GetMethods(BindingFlags.Public | BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Instance); foreach (var method in methods) { if (_monoMethods.Keys.Contains(method.Name)) { var priorityAttributes = method.GetCustomAttributes(typeof(ExecuteOrderAttribute), true); var priority = (priorityAttributes.Length > 0) ? ((ExecuteOrderAttribute)priorityAttributes[0]).Order : int.MaxValue; monoBehaviourMethods[method.Name].Add(new MonoMethodData(method, component, priority, _monoMethods[method.Name])); } } } 

Cada método é armazenado no dicionário por meio de um link para uma classe especial através da qual a chamada é feita. Essa classe converte automaticamente o método em uma referência a Action . Portanto, há uma aceleração significativa de chamadas para métodos Mono em comparação com o MethodInfo.Invoke usual.

 private class MonoMethodData { public int Order { get; private set; } private Action _monoMethodWrapper; private Action<bool> _monoMethodParamWrapper; public MonoMethodData(MethodInfo method, object target, int order, bool withParam) { if (!withParam) { _monoMethodWrapper = (Action)Delegate.CreateDelegate(typeof(Action), target, method.Name); } else { _monoMethodParamWrapper = (Action<bool>)Delegate.CreateDelegate(typeof(Action<bool>), target, method.Name); } Order = order; } public void Call() =>_monoMethodWrapper(); public void Call(bool param) => _monoMethodParamWrapper(param); } 

Nota : sobrecarregar o método Call com o parâmetro bool é usado para métodos Mono , como ApplicationPause , etc.

Lógica de disparo externo


Um lançamento externo da lógica é um lançamento durante a operação do aplicativo, essa lógica não está definida na cena e não há link para ela. Ele pode ser baixado do pacote ou de recursos. Em geral, começar de fora não é muito diferente de começar no início da cena, com exceção de trabalhar com métodos Mono .

Código de método para execução de lógica externa (adiada)
 public void RunLogicExternal(LogicStorage logicStorage) { var instances = new Dictionary<string, LogicComponent>(); var runMonoMethods = new Dictionary<string, List<MonoMethodData>>(); foreach (var monoMethodName in _monoMethods.Keys) { runMonoMethods.Add(monoMethodName, new List<MonoMethodData>()); } foreach (var componentData in logicStorage.Components.Items) { CreateComponent(componentData, instances, _disposableInstances, runMonoMethods); } logicStorage.Links.Items.Sort(SortingLinks); foreach (var linkData in logicStorage.Links.Items) { CreateLink(linkData, instances); } foreach (var monoMethods in runMonoMethods.Values) { monoMethods.Sort(SortingMonoMethods); } if (runMonoMethods.ContainsKey(_START_STR)) { CallMonoBehaviourMethod(_START_STR, runMonoMethods, true); } foreach (var monoMethodName in runMonoMethods.Keys) { _monoBehaviourMethods[monoMethodName].AddRange(runMonoMethods[monoMethodName]); } foreach (var monoMethods in _monoBehaviourMethods.Values) { monoMethods.Sort(SortingMonoMethods); } } 


Como você pode ver, todas as referências aos métodos Mono são salvas no dicionário local, após o qual os métodos Start são iniciados e todos os outros métodos são adicionados ao dicionário geral.

Executando lógica como uma instância


Durante a operação do aplicativo, pode ser necessário executar determinada lógica por um certo tempo ou até que ele execute sua tarefa. As opções anteriores de acionamento lógico não podem fazer isso, pois a lógica é acionada durante toda a vida da cena.

Para executar a lógica como uma instância, são salvos links para todas as instâncias de componentes e similares, que, após a conclusão do trabalho, podem ser excluídos, limpando assim a memória.

Código da classe do armazém de dados da instância lógica:

 private class InstanceLogicData { public readonly IList<LogicComponent> ComponentInstances = new List<LogicComponent>(); public readonly IDictionary<string, List<MonoMethodData>> MonoBehaviourMethods = new Dictionary<string, List<MonoMethodData>>(); public readonly IList<IDisposable> DisposableInstances = new List<IDisposable>(); } 

O lançamento lógico em si é semelhante ao lançamento externo descrito anteriormente.
 public string RunLogicInstance(LogicStorage logicStorage, object data) { var id = Guid.NewGuid().ToString(); var logicInstanceData = new InstanceLogicData(); var instances = new Dictionary<string, LogicComponent>(); _logicInstances.Add(id, logicInstanceData); foreach (var componentData in logicStorage.Components.Items) { CreateComponent(componentData, instances, logicInstanceData.DisposableInstances, logicInstanceData.MonoBehaviourMethods); } logicStorage.Links.Items.Sort(SortingLinks); foreach (var linkData in logicStorage.Links.Items) { CreateLink(linkData, instances); } foreach (var monoMethods in logicInstanceData.MonoBehaviourMethods.Values) { monoMethods.Sort(SortingMonoMethods); } return id; } 


Pode-se ver no código que, após a criação da lógica, os dados sobre ela são armazenados em uma instância de uma classe especializada e, após o início, um identificador exclusivo para a instância da lógica é retornado.

Este identificador é necessário para chamar a função de parada e limpeza da memória.
 public void StopLogicInstance(string instanceId) { if (!_logicInstances.ContainsKey(instanceId)) return; var logicInstance = _logicInstances[instanceId]; foreach (var disposableInstance in logicInstance.DisposableInstances) { disposableInstance.Dispose(); } foreach (var componentInstance in logicInstance.ComponentInstances) { Destroy(componentInstance); } logicInstance.ComponentInstances.Clear(); logicInstance.DisposableInstances.Clear(); logicInstance.MonoBehaviourMethods.Clear(); _logicInstances.Remove(instanceId); } 


Desligando a cena e limpando a memória


Ao criar um ScriptableObject em uma cena em execução via ScriptableObject.CreateInstance , a instância se comporta da mesma forma que MonoBehaviour , ou seja, ao descarregar da cena, o OnDestroy será chamado para cada um e será excluído da memória. No entanto, como foi dito em um artigo anterior, um componente pode herdar IDisposable , então eu o limpo no método OnDisable :

 void OnDisable() { foreach (var disposable in _disposableInstances) { disposable.Dispose(); } _disposableInstances.Clear(); _monoBehaviourMethods.Clear(); foreach (var logicInstance in _logicInstances.Values) { foreach (var disposableInstance in logicInstance.DisposableInstances) { disposableInstance.Dispose(); } logicInstance.DisposableInstances.Clear(); logicInstance.ComponentInstances.Clear(); logicInstance.MonoBehaviourMethods.Clear(); } _logicInstances.Clear(); _instance = null; } 

Nota : como você pode ver, se houver instâncias lógicas no momento do descarregamento da cena, a limpeza ocorre nelas.

Problema e solução com objetos do Unity


Na primeira parte do artigo sobre o editor visual, mencionei o problema de manter links para objetos de cena, o que está relacionado à incapacidade de restaurar dados serializados. Isso se deve ao fato de o identificador exclusivo dos objetos da cena ser diferente cada vez que a cena é iniciada. A solução para esse problema é a única opção - isso é transferir o armazenamento de links na cena. Para esse propósito, um repositório especializado e uma classe de wrapper foram adicionados ao controlador lógico através do qual os componentes lógicos recebem referências aos objetos do Unity , incluindo recursos, pré-fabricados e outros ativos.

Código para armazenamento
 [Serializable] private class ObjectLinkData { public string Id => _id; public UnityEngine.Object Obj => _obj; [SerializeField] private string _id = string.Empty; [SerializeField] private UnityEngine.Object _obj; public ObjectLinkData(string id, UnityEngine.Object obj) { _id = id; _obj = obj; } } [SerializeField] [HideInInspector] private List<ObjectLinkData> _objectLinks = new List<ObjectLinkData>(); public UnityEngine.Object GetObject(string id) { var linkData = _objectLinks.Find(link => { return string.Compare(link.Id, id, StringComparison.Ordinal) == 0; }); return linkData?.Obj; } 

Aqui:

  1. Id - identificador exclusivo do ativo ou objeto de cena
  2. Obj - link para objetos de ativos ou cenas


Como você pode ver no código, nada super complicado.

Agora considere uma classe de wrapper para objetos do Unity
 [Serializable] public class VLObject { public string Id => _id; public UnityEngine.Object Obj { get { if (_obj == null && !_objNotFound) { _obj = Core.LogicController.Instance.GetObject(Id); if (_obj == null) { _objNotFound = true; } } return _obj; } } private UnityEngine.Object _obj; [SerializeField] private string _id; private bool _objNotFound; public VLObject() { } public VLObject(UnityEngine.Object obj) { _obj = obj; } public T Get<T>() where T : UnityEngine.Object { return Obj as T; } } 

Nota : o sinalizador _objNotFound é necessário para não procurar no repositório todas as vezes, se não houver nenhum objeto nele.

Desempenho


Agora, acho que vale a pena insistir em uma questão como produtividade. Se você observar cuidadosamente todo o código acima, poderá entender que em um aplicativo já em execução, o sistema não afeta a velocidade, ou seja, tudo funcionará como o habitual aplicativo Unity . Comparei a atualização do MonoBehaviour usual e do uViLEd e não consegui pelo menos nenhuma diferença que pudesse ser indicada aqui; todos os números têm paridade de até 1000 chamadas por quadro. O único gargalo é a velocidade de carregamento da cena, mas mesmo aqui não consegui obter números significativos, embora entre as plataformas (verifiquei Android e iOS) a diferença é grande. Na lógica de 70 componentes e cerca de 150 conexões (incluindo referências a variáveis), os números eram os seguintes:

  1. Android (MediaTek 8 núcleos muito medíocre)
    - No início do aplicativo - ~ 750ms
    - Executando a cena no aplicativo em execução ~ 250ms
  2. iOS (iPhone 5s)
    - No início do aplicativo - ~ 100ms
    - Executando a cena no aplicativo em execução ~ 50ms

Conclusão


A arquitetura do kernel do sistema uViLEd é bastante simples e direta, não é aconselhável usá-lo indiretamente a partir do editor visual, embora eu não exclua que algumas soluções serão úteis para alguém. No entanto, este artigo é importante para uma compreensão geral de como o sistema funciona, cuja última parte importante é o próprio editor. E a última parte final da série, que será a mais movimentada, será dedicada a isso.

PS: a redação do artigo foi muito longa, pela qual peço desculpas. Inicialmente, planejei liberar todas as partes restantes de uma só vez, mas o lançamento de novas versões do Unity 3d, bem como choques vitais (agradáveis ​​e não tão), mudaram os planos.

Editor de lógica visual para Unity3d. Parte 1

Source: https://habr.com/ru/post/pt466187/


All Articles