Unity3d的可视逻辑编辑器。 第二部分

引言


尊敬的读者您好,在今天的文章中,我想重点介绍Unity3d的可视逻辑编辑器内核的体系结构主题。 这是本系列的第二部分。 您可以在这里阅读上一篇。 那么我们要谈什么呢? 可视编辑器基于核心内核,该内核使您可以运行,加载和存储逻辑数据。 反过来,如上一篇文章中所述,内核使用ScriptableObject作为使用逻辑组件的基类。 让我们更详细地考虑所有这些方面。

该系列文章:

Unity3d的可视逻辑编辑器。 第一部分

为什么是ScriptableObject?


在开始开发之前,我考虑了很长时间来构建系统。 在第一种形式中-它是MonoBehaviour ,但是我不得不放弃这个想法,因为这些脚本应该作为组件挂在GameObject上 。 下一步是使用您自己的类的想法,该类不是UnityEngine.Object的后代。 但是此选项虽然起作用,但并未扎根,但它一直拖延着编写其序列化程序,检查器,垃圾收集器等。结果,唯一合理的方法是使用ScriptableObject ,如果创建,其生命周期类似于MonoBehaviour应用程序通过ScriptableObject.CreateInstance运行时发生。 此外,使用JsonUtility (尽管现在不再是问题)和Unity检查器可以自动解决此问题。

建筑学


下面是uViLEd核心组成的一般示意图。

图片

让我们更详细地考虑每个元素。

控制者


控制器是内核的主要元素,它是MonoBehavior脚本(整个系统中唯一的脚本)。 控制器类是单例,并且逻辑的所有组件均可访问。 此类的作用:

  1. 存储Unity对象链接
  2. 在场景开始时开始逻辑
  3. 表示对从外部源运行逻辑的方法的访问
  4. 提供组件中Mono方法的工作

控制器类基本代码
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)); } } } } 


注意 :每个场景都有自己的控制器和逻辑集。
注意 :有关加载逻辑数据及其启动的更多详细信息,将另行讨论。

uViLEd内核的核心元素


组成部分


在第一部分中,我已经说过该组件是ScriptableObject 。 所有组件都是LogicComponent类的后代,而LogicComponent又很简单。

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

在这里, coroutineHost是到逻辑控制器的链接,仅为方便起见而引入,顾名思义,它用于与协程一起工作。 为了将组件与Unity项目中存在的其他代码分开,必须使用这种抽象。

变数


如上一篇文章中所述,变量是用于存储数据的专用组件,其代码如下所示。

可变执行代码
 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; } } } } } 


这里, 变量是所有变量的基本抽象类,必须将它们与普通组件分开。 主类是通用类,它存储数据本身并提供用于设置值和更改值的事件。

通讯技术


我在上一篇文章中告诉过什么关系。 简而言之,它是一个虚拟实体,它允许组件使用彼此的方法,并且还引用逻辑变量。 对于程序员而言,此连接是软连接,并且在代码中不可见。 所有通信都是在初始化过程中形成的(请在下面阅读)。 考虑允许您形成关系的类。

入口点
 namespace uViLEd.Core { public class INPUT_POINT<T> { public Action<T> Handler; } public class INPUT_POINT { public Action Handler; } } 


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


可变参考
 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; } } } } 


注意 :这里有一点需要说明,用于设置事件和更改变量值的自动处理程序仅在Constructor方法中设置后才使用,因为那时尚未设置对变量的引用。

使用逻辑


贮藏


在关于可视逻辑编辑器的第一篇文章中,提到了逻辑是一组变量,组件以及它们之间的关系:

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

该类显然是可序列化的,但是Unity的 JsonUtility不用于序列化。 而是使用二进制选项,其结果保存为带有字节扩展名的文件。 为什么要这样做? 通常,主要原因是安全性,也就是说,可以选择从外部源加载逻辑,从而可以加密数据,并且通常,反序列化字节数组比打开json困难。

让我们仔细看一下 ComponentsStrorageLinksStorage类 。 系统使用GUID进行全局数据识别。 下面是类代码,它是数据容器的基础。

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

现在考虑ComponentsStorage类的代码,顾名思义,该代码存储有关逻辑组件的数据:

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

该类非常简单。 为每个组件存储以下信息:

  1. 标识符中找到的唯一标识符( GUID字符串)
  2. 类型名称
  3. 零部件类型所在的装配体的名称
  4. 带有序列化数据的Json字符串( JsonUtility.ToJson的结果)
  5. 组件活动(状态)标志

现在让我们看一下LinksStorage类。 此类存储有关组件之间的关系以及有关变量的引用的信息。

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

原则上,此类也不复杂。 每个链接包含以下信息:

  1. 指示此链接是对变量的引用的标志
  2. 链接活动标志
  3. 具有输出点的组件的标识符( GUID字符串)
  4. 入口点组件的标识符( GUID字符串)
  5. 通信源组件的输出点名称
  6. 目标通信组件的输入点名称
  7. 用于设置变量引用的类字段名称
  8. 通讯通话单

从存储库运行


在深入研究代码之前,我首先要详细介绍控制器如何启动逻辑的顺序:

  1. 初始化从控制器的Awake方法开始
  2. 场景逻辑列表从二进制文件( TextAsset )加载和反序列化逻辑数据
  3. 对于每种逻辑发生:
    • 组件创建
    • CallOrder排序链接
    • 设置链接和变量引用
    • ExecuteOrderMono Component 方法排序


让我们更详细地考虑该链的各个方面。

二进制序列化和反序列化
 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); } } } } 


从文本资产(二进制文件)加载逻辑数据
 namespace uViLEd.Core { public partial class LogicStorage { public static LogicStorage Load(TextAsset textAsset) => Serialization.Deserialize(textAsset) as LogicStorage; } } 


逻辑启动
 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); } } 


组件创建
 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); } 

那么此函数会发生什么:

  1. 检查组件活动标志
  2. 从装配体中获取零部件的类型
  3. 组件实例是按类型创建的
  4. JSON的组件参数反序列化
  5. 链接在coroutineHost中设置
  6. 构造方法称为
  7. 临时副本将保存到组件实例
  8. 如果组件实现了IDisposable接口,则指向它的链接存储在相应的列表中
  9. 在组件中搜索Mono方法


建立连结
 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; } } } 

创建连接是整个系统内在最困难的时刻之一,我们将考虑每个阶段:

  1. 检查通讯活动标志
  2. 在创建的组件的临时列表中搜索连接的源组件和目标组件
  3. 检查连接类型:
    • 如果连接类型是对变量的引用,则使用反射设置必要的值
    • 如果连接正常,则还可以使用反射来设置输出点和输入点方法的必要值

  4. 对于正常通信,请首先检查组件是否继承IInputPointParseIOutputPointParse接口
  5. 根据上一段的结果,通过反射获得源组件中的LinkedInputPoints字段和目标组件中的handler方法。
  6. 通过将绕过MethodInfo.Invoke的方法调用转换为简单的Action<T>调用,可以获取方法处理程序。 但是,通过反射获得这样的链接太复杂了,因此,在OUTPUT_POINT<T>类中OUTPUT_POINT<T>了允许这样做的特殊方法。 对于此类的非通用版本,无需执行此类操作。

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

    当输入点不接受任何参数时,使用第一种方法。 第二种方法分别为带有参数的输入点。

  7. 通过反射,将到Action处理程序的链接添加到LinkedInputPoints字段(如上所示,它是一个列表)


使用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])); } } } 

每个方法都通过指向特殊类的链接存储在字典中,通过该类可以进行调用。 此类自动将方法转换为对Action的引用。 因此,与通常的MethodInfo.Invoke相比,对Mono方法的调用有了明显的加速。

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

注意 :用bool参数重载Call方法用于Mono方法,例如ApplicationPause等。

外部触发逻辑


逻辑的外部启动是在应用程序运行期间启动的逻辑,此类逻辑未在场景中设置,并且没有链接。 可以从捆绑软件或资源中下载。 通常,从外部开始与从场景开始处并没有什么不同,除了使用Mono方法。

外部(延迟)逻辑执行的方法代码
 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); } } 


如您所见,对Mono方法的所有引用都保存在本地字典中,然后启动Start方法,然后将所有其他方法添加到通用字典中。

作为实例运行逻辑


在应用程序运行期间,可能有必要在一定时间内运行某种逻辑,或者直到它执行其任务为止。 不允许使用以前的逻辑触发选项来执行此操作,因为在场景的整个生命周期中都会触发逻辑。

为了将逻辑作为实例运行,将保存到组件等所有实例的链接,在工作完成后可以将其删除,从而清除内存。

逻辑实例数据仓库类代码:

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

逻辑启动本身类似于前面描述的外部启动。
 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; } 


从代码中可以看出,在创建逻辑之后,有关逻辑的数据存储在专门类的实例中,并且在启动后,将返回逻辑实例的唯一标识符。

需要此标识符才能调用停止和清除内存功能。
 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); } 


关闭场景并清除内存


通过ScriptableObject.CreateInstance在运行的场景中创建ScriptableObject时 ,实例的行为与MonoBehaviour相同,即,从场景中卸载时,将为每个实例调用OnDestroy并将其从内存中删除。 但是,如前一篇文章所述,组件可以继承IDisposable ,因此我使用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; } 

注意 :如您所见,如果在卸载场景时存在逻辑实例,则将在其中进行清理。

Unity对象的问题和解决方案


在有关可视化编辑器的文章的第一部分中,我提到了维护与场景对象的链接的问题,这与无法还原序列化数据有关。 这是由于以下事实:每次启动场景时,场景对象的唯一标识符都不同。 解决该问题的方法是唯一的选择-这是转移场景中链接的存储。 为此,在逻辑控制器中添加了专门的存储库和包装器类,逻辑组件可通过该类来接收对Unity对象的引用,包括资源,预制件和其他资产。

存储代码
 [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; } 

:

  1. Id
  2. Obj


, .

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

: _objNotFound , , .


, , . , , , .. Unity . Update MonoBehaviour uViLEd - , , 1000 . — , , ( Android iOS ) . 70 150 ( ) :

  1. Android ( MediaTek 8-)
    — — ~750
    — ~250
  2. iOS (iPhone 5s)
    — — ~100
    — ~50

结论


uViLEd - , , , - - . , . , .

PS: , . , Unity 3d, ( ) .

Unity3d. 第一部分

Source: https://habr.com/ru/post/zh-CN466187/


All Articles