محرر المنطق المرئي لـ Unity3d. الجزء 2

مقدمة


مرحباً أيها القراء الأعزاء ، أود في مقال اليوم أن أسلط الضوء على موضوع هندسة نواة محرر المنطق البصري لـ Unity3d . هذا هو الجزء الثاني من السلسلة. يمكنك قراءة السابق واحد هنا . إذن ما الذي سنتحدث عنه؟ يعتمد المحرر المرئي على لب أساسي ، والذي يسمح لك بتشغيل وتحميل وتخزين بيانات المنطق. بدوره ، يستخدم kernel ، كما ذكر في المقالة السابقة ، ScriptableObject ، كفئة أساسية للعمل مع المكونات المنطقية. دعونا نفكر بمزيد من التفصيل في كل هذه الجوانب.

مقالات في السلسلة:

محرر المنطق البصري لل Unity3d. الجزء 1

لماذا هو ScriptableObject؟


قبل البدء في التطوير ، فكرت لفترة طويلة حول كيفية بناء النظام. في النموذج الأول - كانت MonoBehaviour ، لكن كان علي أن أتخلى عن هذه الفكرة ، لأن هذه البرامج النصية يجب أن تعلق على GameObject ، كمكونات. كانت الخطوة التالية هي فكرة استخدام الفصل الدراسي الخاص بك ، وهو ليس من نسل UnityEngine.Object . لكن هذا الخيار لم يتجذر ، على الرغم من أنه كان يعمل بشكل جيد ، لكنه استمر ، وكتب مُسلسله ، ومفتشه ، وجمع القمامة ، وما إلى ذلك. ونتيجة لذلك ، كانت الطريقة الوحيدة المعقولة هي استخدام ScriptableObject ، التي تشبه دورة حياتها MonoBehaviour ، إذا كانت الخلق يحدث أثناء تشغيل التطبيق من خلال ScriptableObject.CreateInstance . بالإضافة إلى ذلك ، تم حل هذه المشكلة تلقائيًا باستخدام JsonUtility (على الرغم من أنها لم تعد مشكلة الآن) ومفتش Unity .

هندسة معمارية


يوجد أدناه رسم تخطيطي عام لما يتكون منه uViLEd .

صورة

لننظر في كل عنصر بمزيد من التفصيل.

مراقب


وحدة التحكم هي العنصر الرئيسي في kernel ، وهو برنامج نصي MonoBehavior (الوحيد في النظام بأكمله). فئة وحدة التحكم هي وحدة مفردة ويمكن الوصول إليها من جميع مكونات المنطق. ماذا يفعل هذا الفصل:

  1. مخازن الوحدة كائن الارتباطات
  2. يبدأ المنطق في بداية المشهد
  3. يمثل الوصول إلى طرق لتشغيل المنطق من مصادر خارجية
  4. يوفر عمل طرق مونو في المكونات

رمز قاعدة فئة تحكم
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 Kernel


عنصر


في الجزء الأول ، قلت بالفعل أن المكون هو ScriptableObject . جميع المكونات هي أحفاد فئة LogicComponent ، والتي بدورها بسيطة.

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

هنا ، coroutineHost عبارة عن رابط لوحدة تحكم منطق ، والتي تم تقديمها للراحة فقط وتستخدم ، كما يوحي الاسم ، للعمل مع coroutines. يعد استخدام هذا التجريد ضروريًا لفصل المكونات عن الأكواد الأخرى الموجودة في مشروع الوحدة .

المتغيرات


المتغيرات ، كما هو مذكور في مقال سابق ، هي مكونات متخصصة لتخزين البيانات ، يتم تقديم الكود الخاص بها أدناه.

رمز التنفيذ المتغير
 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(); } } 

يبدو أن هذه الفئة قابلة للتسلسل ، ولكن JsonUtility من Unity ليست مستخدمة للتسلسل. بدلاً من ذلك ، يتم استخدام خيار ثنائي ، يتم حفظ نتائجه كملف بملحق البايت . لماذا يتم ذلك؟ بشكل عام ، السبب الرئيسي هو الأمان ، أي أنه من الممكن ، بالنسبة لخيار تحميل المنطق من مصدر خارجي ، تشفير البيانات ، وبشكل عام ، فإن إلغاء تسلسل مجموعة بايت يكون أكثر صعوبة من فتح json .

دعونا نلقي نظرة فاحصة على فئتي ComponentsStrorage و LinksStorage . يستخدم النظام 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
    • وضع الروابط والمراجع المتغيرة
    • فرز أساليب مكون أحادي بواسطة ExecuteOrder


دعونا نفكر بمزيد من التفصيل في كل جانب من جوانب هذه السلسلة.

التسلسل الثنائي وإلغاء التسلسل
 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. بالنسبة للاتصالات العادية ، تحقق أولاً مما إذا كان المكون يرث واجهات IInputPointParse و IOutputPointParse .
  5. اعتمادًا على نتائج الفقرة السابقة ، يتم الحصول على حقل LinkedInputPoints في المكون المصدر وطريقة المعالج في المكون الهدف من خلال الانعكاس.
  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. من خلال الانعكاس ، تتم إضافة ارتباط إلى معالج الإجراء إلى حقل LinkedInputPoints (والذي ، كما هو موضح أعلاه ، هو قائمة)


العمل مع أساليب مونو
 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])); } } } 

يتم تخزين كل طريقة في القاموس من خلال رابط لفئة خاصة يتم من خلالها إجراء المكالمة. يحول هذا الفصل تلقائيًا الطريقة إلى إشارة إلى الإجراء . وبالتالي ، هناك تسارع كبير في استدعاءات أساليب Mono مقارنةً بالطريقة المعتادة MethodInfo.Invoke .

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

ملاحظة : يتم استخدام التحميل الزائد لطريقة المكالمة باستخدام المعلمة المنطقية لطرق 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 في مشهد قيد التشغيل عبر ScriptableObject.CreateInstance ، يتصرف المثيل مثل 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; } 

ملاحظة : كما ترون ، في حالة وجود حالات منطقية في وقت تفريغ المشهد ، يحدث التنظيف فيها.

المشكلة والحل مع كائنات الوحدة


في الجزء الأول من المقالة حول المحرر المرئي ، ذكرت مشكلة الحفاظ على روابط لكائنات المشهد ، والتي تتعلق بعدم القدرة على استعادة البيانات المتسلسلة. هذا يرجع إلى حقيقة أن المعرف الفريد لكائنات المشهد مختلف في كل مرة يتم فيها تشغيل المشهد. الحل لهذه المشكلة هو الخيار الوحيد - وهو نقل تخزين الروابط في المشهد. لهذا الغرض ، تمت إضافة مستودع متخصص وفئة مجمّع إلى وحدة التحكم المنطقية التي من خلالها تتلقى مكونات المنطق إشارات إلى كائنات الوحدة ، بما في ذلك الموارد والأبنية الجاهزة والأصول الأخرى.

رمز للتخزين
 [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. معرف - معرف فريد للكائن الأصل أو المشهد
  2. OBJ - رابط لأشياء الأصل أو المشهد


كما ترون من التعليمات البرمجية ، لا شيء معقد للغاية.

الآن النظر في فئة المجمع لكائنات الوحدة
 [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 ضرورية حتى لا يتم البحث في المستودع في كل مرة إذا لم يكن هناك كائن فيها.

إنتاجية


الآن ، أعتقد أنه من المجدي التركيز على سؤال مثل الإنتاجية. إذا نظرت بعناية إلى الكود بأكمله أعلاه ، يمكنك أن تفهم أنه في تطبيق قيد التشغيل بالفعل ، لا يؤثر النظام على السرعة ، أي كل شيء سوف يعمل كتطبيق الوحدة المعتاد . قارنت تحديث MonoBehaviour المعتاد ومن خلال uViLEd ولم أتمكن من تحقيق أي فرق على الأقل يمكن الإشارة إليه هنا ، كل الأرقام تعادل حتى 1000 مكالمة لكل إطار. عنق الزجاجة الوحيد هو سرعة تحميل المشهد ، لكن حتى هنا لم أستطع الحصول على أرقام مهمة ، على الرغم من أن بين المنصات (راجعت Android و iOSالفرق كبير. في منطق 70 مكونًا وحوالي 150 اتصالًا (بما في ذلك الإشارات إلى المتغيرات) ، كانت الأرقام كما يلي:

  1. Android (متواضعة جدًا MediaTek 8-core)
    - في بداية التطبيق - حوالي 750 مللي ثانية
    - تشغيل المشهد في التطبيق قيد التشغيل ~ 250 مللي ثانية
  2. iOS (iPhone 5s)
    - في بداية التطبيق - ~ 100ms
    - تشغيل المشهد في التطبيق قيد التشغيل ~ 50ms

استنتاج


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

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

Unity3d. الجزء 1

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


All Articles