Editor de lógica visual para Unity3d. Parte 2

Introduccion


Hola queridos lectores, en el artículo de hoy me gustaría destacar el tema de la arquitectura del núcleo del editor de lógica visual para Unity3d . Esta es la segunda parte de la serie. Puedes leer el anterior aquí . Entonces, ¿de qué hablaremos? El editor visual se basa en el núcleo central, que le permite ejecutar, cargar y almacenar datos lógicos. A su vez, el núcleo utiliza, como se mencionó en el artículo anterior, ScriptableObject , como una clase base para trabajar con componentes lógicos. Consideremos con más detalle todos estos aspectos.

Artículos de la serie:

Editor de lógica visual para Unity3d. Parte 1

¿Por qué es ScriptableObject?


Antes de comenzar el desarrollo, pensé durante mucho tiempo sobre qué construir el sistema. En la primera forma, era MonoBehaviour , pero tuve que abandonar esta idea, ya que estos scripts deberían colgar en GameObject , como componentes. El siguiente paso fue la idea de usar su propia clase, que no es descendiente de UnityEngine.Object . Pero esta opción no se arraigó, aunque funcionaba bastante, pero se arrastró, escribiendo su serializador, inspector, recolector de basura, etc. Como resultado, la única forma razonable era usar ScriptableObject , cuyo ciclo de vida es similar a MonoBehaviour , si la creación se produce mientras la aplicación se ejecuta a través de ScriptableObject.CreateInstance . Además, este problema se resolvió automáticamente utilizando JsonUtility (aunque ahora ya no es un problema) y el inspector de Unity .

Arquitectura


A continuación se muestra un diagrama generalizado de en qué consiste el núcleo uViLEd .

imagen

Consideremos cada elemento con más detalle.

Controlador


El controlador es el elemento principal del núcleo, que es un script MonoBehavior (el único en todo el sistema). La clase de controlador es un singleton y es accesible para todos los componentes de la lógica. Lo que hace esta clase:

  1. Almacena enlaces de objetos de Unity
  2. Inicia la lógica al comienzo de la escena.
  3. Representa el acceso a los métodos para ejecutar la lógica desde fuentes externas.
  4. Proporciona el trabajo de métodos Mono en componentes.

Código base de clase de 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 escena tiene su propio controlador y su propio conjunto de lógica.
Nota : más detalles sobre la carga de datos lógicos y su lanzamiento se discutirán por separado.

Elementos centrales del núcleo uViLEd


Componente


En la primera parte, ya dije que el componente es un ScriptableObject . Todos los componentes son descendientes de la clase LogicComponent , que a su vez es simple.

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

Aquí, coroutineHost es un enlace a un controlador lógico, que se introdujo solo por conveniencia y se utiliza, como su nombre lo indica, para trabajar con corutinas. El uso de esta abstracción es necesario para separar los componentes de otro código presente en el proyecto Unity .

Variables


Las variables, como se mencionó en un artículo anterior, son componentes especializados para almacenar datos, el código para ellas se presenta a continuación.

Código de implementación variable
 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; } } } } } 


Aquí Variable es una clase abstracta básica para todas las variables, es necesaria para separarlas de los componentes ordinarios. La clase principal es genérica , que almacena los datos en sí y proporciona eventos para establecer el valor y cambiarlo.

Comunicaciones


Cuáles son las conexiones, dije en un artículo anterior. Brevemente, es una entidad virtual que permite que los componentes utilicen los métodos de los demás y también se refieren a variables lógicas. Para el programador, esta conexión es suave y no está visible en el código. Todas las comunicaciones se forman durante el proceso de inicialización (lea sobre esto a continuación). Considere las clases que le permiten formar relaciones.

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


Punto de salida
 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(); } } } } 


Referencia variable
 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 : aquí vale la pena explicar un punto, los controladores automáticos para establecer eventos y cambiar el valor de una variable se usan solo cuando se establecen en el método Constructor , ya que en ese momento las referencias a las variables aún no se habían establecido.

Trabajar con lógica


Almacenamiento


En el primer artículo sobre el editor de lógica visual, se mencionó que la lógica es un conjunto de variables, componentes y las relaciones entre ellos:

 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(); } } 

Esta clase es aparentemente serializable, pero JsonUtility de Unity no se usa para la serialización. En su lugar, se utiliza una opción binaria, cuyo resultado se guarda como un archivo con la extensión de bytes . ¿Por qué se hace esto? En general, la razón principal es la seguridad, es decir, para la opción de cargar la lógica desde una fuente externa, es posible cifrar datos y, en general, deserializar una matriz de bytes es más difícil que abrir json .

Echemos un vistazo más de cerca a las clases ComponentsStrorage y LinksStorage . El sistema usa un GUID para la identificación global de datos. A continuación se muestra el código de clase, que es la base para los contenedores de datos.

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

Ahora considere el código de la clase ComponentsStorage , que, como su nombre lo indica, almacena datos sobre los componentes de la 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>(); } } 

La clase es bastante simple. La siguiente información se almacena para cada componente:

  1. Identificador único (cadena GUID ) encontrado en Identificador
  2. Nombre del tipo
  3. El nombre del ensamblaje en el que se encuentra el tipo de componente.
  4. Cadena Json con datos de serialización (resultado de JsonUtility.ToJson )
  5. Indicador de actividad de componente (estado)

Ahora veamos la clase LinksStorage . Esta clase almacena información sobre las relaciones entre componentes, así como sobre referencias a variables.

 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>(); } } 

En principio, tampoco hay nada complicado en esta clase. Cada enlace contiene la siguiente información:

  1. Indicador que indica que este enlace es una referencia a una variable
  2. Indicador de actividad de enlace
  3. El identificador (cadena GUID ) del componente con el punto de salida
  4. El identificador (cadena GUID ) del componente de punto de entrada
  5. El nombre del punto de salida del componente fuente de comunicación.
  6. Nombre del punto de entrada del componente de comunicación de destino.
  7. Nombre de campo de clase para establecer una referencia variable
  8. Orden de llamada de comunicación

Ejecutar desde el repositorio


Antes de entrar en los detalles del código, primero quiero detenerme en la descripción de la secuencia de cómo el controlador inicia la lógica:

  1. La inicialización comienza en el método Despertar del controlador
  2. La lista de lógicas de escena carga y deserializa los datos lógicos de un archivo binario ( TextAsset )
  3. Para cada lógica ocurre:
    • Creación de componentes
    • Ordenar enlaces por CallOrder
    • Establecer enlaces y referencias variables
    • Ordenar métodos de componentes mono por orden de ejecución


Consideremos con más detalle cada aspecto de esta cadena.

Serialización binaria y deserialización
 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); } } } } 


Carga de datos lógicos de un activo de texto (archivo binario)
 namespace uViLEd.Core { public partial class LogicStorage { public static LogicStorage Load(TextAsset textAsset) => Serialization.Deserialize(textAsset) as LogicStorage; } } 


Lanzamiento lógico
 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); } } 


Creación 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); } 

Entonces, ¿qué sucede en esta función?

  1. La marca de actividad del componente está marcada
  2. Obtener el tipo de componente del ensamblaje
  3. Una instancia de componente se crea por tipo
  4. Deserialización de parámetros de componentes de json
  5. El enlace se establece en la rutina
  6. Método constructor llamado
  7. Se guarda una copia temporal en la instancia del componente
  8. Si el componente implementa la interfaz IDisposable, el enlace se almacena en la lista correspondiente
  9. Buscando métodos Mono en el componente


Crear enlaces
 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; } } } 

Al crear una conexión, uno de los momentos más intrínsecamente difíciles en todo el sistema, considere cada etapa:

  1. Comprueba la bandera de actividad de comunicación
  2. En la lista temporal de componentes creados se buscan, el componente de origen de la conexión y el componente de destino.
  3. Comprueba el tipo de conexión:
    • Si el tipo de conexión es una referencia a una variable, los valores necesarios se establecen mediante reflexión
    • Si la conexión es normal, entonces la reflexión también se usa para establecer los valores necesarios para los métodos de puntos de salida y entrada

  4. Para las comunicaciones normales, primero verifique si el componente hereda las interfaces IInputPointParse e IOutputPointParse .
  5. Dependiendo de los resultados del párrafo anterior, el campo LinkedInputPoints en el componente fuente y el método del controlador en el componente objetivo se obtienen a través de la reflexión.
  6. Los manejadores de métodos se obtienen al convertir la llamada al método sin pasar por MethodInfo.Invoke en una simple llamada de Action<T> . Sin embargo, obtener dicho enlace a través de la reflexión es demasiado complicado, por lo tanto, se han OUTPUT_POINT<T> métodos especiales en la OUTPUT_POINT<T> que lo permiten. Para una versión no genérica de esta clase, no se requiere dicha acción.

     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; } } 

    El primer método se usa cuando el punto de entrada no acepta ningún parámetro. El segundo método, respectivamente, para el punto de entrada con un parámetro.

  7. A través de la reflexión, se agrega un enlace al controlador de Acción al campo LinkedInputPoints (que, como se muestra arriba, es una lista)


Trabajando con 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 se almacena en el diccionario a través de un enlace a una clase especial a través de la cual se realiza la llamada. Esta clase convierte automáticamente el método en una referencia a Action . Por lo tanto, hay una aceleración significativa de las llamadas a los métodos Mono en comparación con el MethodInfo.Invoke habitual.

 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 : la sobrecarga del método Call con el parámetro bool se usa para métodos Mono como ApplicationPause , etc.

Lógica de disparo externo


Un lanzamiento externo de lógica es un lanzamiento durante el funcionamiento de la aplicación, dicha lógica no se establece en la escena y no hay ningún enlace a ella. Se puede descargar del paquete o de los recursos. En general, comenzar desde el exterior no es muy diferente de comenzar desde el comienzo de la escena, con la excepción de trabajar con métodos Mono .

Código de método para ejecución lógica externa (diferida)
 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 puede ver, todas las referencias a los métodos Mono se guardan en el diccionario local, después de lo cual se inician los métodos de Inicio y luego se agregan todos los demás métodos al diccionario general.

Ejecutando la lógica como una instancia


Durante el funcionamiento de la aplicación, puede ser necesario ejecutar cierta lógica durante un tiempo determinado o hasta que realice su tarea. Las opciones anteriores de activación lógica no pueden hacer esto, ya que la lógica se activa a lo largo de la vida de la escena.

Para ejecutar la lógica como una instancia, se guardan los enlaces a todas las instancias de componentes y similares que, una vez finalizado el trabajo, se pueden eliminar, lo que borra la memoria.

Código de clase de almacén de datos de instancia 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>(); } 

El lanzamiento lógico en sí es similar al lanzamiento 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; } 


Se puede ver en el código que después de crear la lógica, los datos al respecto se almacenan en una instancia de una clase especializada, y después de comenzar, se devuelve un identificador único para la instancia de la lógica.

Este identificador es necesario para llamar a la función detener y borrar memoria.
 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); } 


Cerrar la escena y borrar la memoria.


Al crear un ScriptableObject en una escena en ejecución a través de ScriptableObject.CreateInstance , la instancia se comporta igual que MonoBehaviour , es decir, cuando se descarga de la escena, se llamará a OnDestroy para cada uno y se eliminará de la memoria. Sin embargo, como se dijo en un artículo anterior, un componente puede heredar IDisposable , por lo que lo limpio en el 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 puede ver, si existen instancias de lógica al momento de descargar la escena, entonces se realiza la limpieza en ellas.

Problema y solución con objetos Unity


En la primera parte del artículo sobre el editor visual, mencioné el problema de mantener enlaces a los objetos de la escena, que está relacionado con la incapacidad de restaurar los datos serializados. Esto se debe al hecho de que el identificador único de los objetos de la escena es diferente cada vez que se inicia la escena. La solución a este problema es la única opción: transferir el almacenamiento de enlaces en la escena. Para este propósito, se agregaron un repositorio especializado y una clase de contenedor al controlador lógico a través del cual los componentes lógicos reciben referencias a objetos de Unity , incluidos recursos, prefabricados y otros activos.

Código de almacenamiento
 [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; } 

Aquí:

  1. Id : identificador único del activo u objeto de escena
  2. Obj : enlace a objetos de activo o escena


Como puede ver en el código, nada súper complicado.

Ahora considere una clase de contenedor para objetos 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 : el indicador _objNotFound es necesario para no buscar el repositorio cada vez que no haya ningún objeto en él.

Rendimiento


Ahora, creo que vale la pena detenerse en una cuestión como la productividad. Si observa detenidamente todo el código anterior, puede comprender que en una aplicación que ya se está ejecutando, el sistema no afecta la velocidad, es decir. todo funcionará como siempre la aplicación de Unity . Comparé la actualización del MonoBehaviour habitual y a través de uViLEd y no pude lograr al menos ninguna diferencia que pudiera indicarse aquí, todos los números son paritarios hasta 1000 llamadas por cuadro. El único cuello de botella es la velocidad de carga de la escena, pero incluso aquí no pude obtener cifras significativas, aunque entre las plataformas (revisé Android e iOS) la diferencia es grande. En la lógica de 70 componentes y aproximadamente 150 conexiones (incluidas referencias a variables), los números fueron los siguientes:

  1. Android (MediaTek 8-core muy mediocre)
    - Al inicio de la aplicación - ~ 750ms
    - Ejecución de la escena en la aplicación en ejecución ~ 250ms
  2. iOS (iPhone 5s)
    - Al inicio de la aplicación - ~ 100ms
    - Ejecución de la escena en la aplicación en ejecución ~ 50ms

Conclusión


La arquitectura del núcleo del sistema uViLEd es bastante simple y directa, no es recomendable usarla indirectamente desde el editor visual, aunque no excluyo que algunas soluciones resulten útiles para alguien. Sin embargo, este artículo es importante para una comprensión general de cómo funciona el sistema, cuya última parte importante es el editor mismo. Y la última parte final de la serie, que será la más agitada, estará dedicada a esto.

PD: la redacción del artículo fue muy larga, por lo que pido disculpas. Inicialmente, planeé lanzar todas las partes restantes a la vez, pero el lanzamiento de nuevas versiones de Unity 3d, así como los choques vitales (agradables y no tan), cambiaron los planes.

Editor de lógica visual para Unity3d. Parte 1

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


All Articles