Editor logika visual untuk Unity3d. Bagian 2

Pendahuluan


Halo para pembaca, dalam artikel hari ini saya ingin menyoroti tema arsitektur kernel dari editor logika visual untuk Unity3d . Ini adalah bagian kedua dari seri ini. Anda dapat membaca yang sebelumnya di sini . Jadi apa yang akan kita bicarakan? Editor visual didasarkan pada inti, yang memungkinkan Anda untuk menjalankan, memuat, dan menyimpan data logika. Pada gilirannya, kernel menggunakan, seperti yang disebutkan dalam artikel sebelumnya, ScriptableObject , sebagai kelas dasar untuk bekerja dengan komponen logika. Mari kita pertimbangkan lebih detail semua aspek ini.

Artikel dalam seri:

Editor logika visual untuk Unity3d. Bagian 1

Mengapa ScriptableObject?


Sebelum memulai pengembangan, saya berpikir lama tentang apa yang harus membangun sistem. Dalam bentuk pertama - itu adalah MonoBehaviour , tapi saya harus meninggalkan ide ini, karena skrip ini harus menggantung di GameObject , sebagai komponen. Langkah selanjutnya adalah gagasan untuk menggunakan kelas Anda sendiri, yang bukan keturunan UnityEngine.Object . Tetapi opsi ini tidak berakar, meskipun cukup berhasil, tetapi menyeret, menulis serializer, inspektur, pengumpul sampah, dll. Akibatnya, satu-satunya cara yang masuk akal adalah menggunakan ScriptableObject , yang siklus hidupnya mirip dengan MonoBehaviour , jika pembuatannya terjadi saat aplikasi sedang berjalan melalui ScriptableObject.CreateInstance . Selain itu, masalah ini secara otomatis diselesaikan menggunakan JsonUtility (meskipun sekarang tidak lagi menjadi masalah) dan inspektur Unity .

Arsitektur


Di bawah ini adalah diagram umum tentang inti uViLEd .

gambar

Mari kita bahas setiap elemen secara lebih rinci.

Pengendali


Controller adalah elemen utama dari kernel, yang merupakan skrip MonoBehavior (satu-satunya di seluruh sistem). Kelas controller adalah singleton dan dapat diakses oleh semua komponen logika. Apa yang dilakukan kelas ini:

  1. Menyimpan Tautan Objek Persatuan
  2. Mulai logika di awal adegan
  3. Merupakan akses ke metode untuk menjalankan logika dari sumber eksternal
  4. Menyediakan karya metode Mono dalam komponen

Kode dasar kelas pengontrol
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)); } } } } 


Catatan : setiap adegan memiliki pengontrolnya sendiri dan logikanya sendiri.
Catatan : rincian lebih lanjut tentang memuat data logika dan peluncurannya akan dibahas secara terpisah.

Elemen Inti dari Kernel uViLEd


Komponen


Pada bagian pertama, saya sudah mengatakan bahwa komponennya adalah ScriptableObject . Semua komponen adalah turunan dari kelas LogicComponent , yang pada gilirannya sederhana.

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

Di sini, coroutineHost adalah tautan ke pengontrol logika, yang diperkenalkan hanya untuk kenyamanan dan digunakan, sesuai namanya, untuk bekerja dengan coroutine. Penggunaan abstraksi ini diperlukan untuk memisahkan komponen dari kode lain yang ada dalam proyek Unity .

Variabel


Variabel, seperti yang disebutkan dalam artikel sebelumnya, adalah komponen khusus untuk menyimpan data, kode untuk mereka disajikan di bawah ini.

Kode Implementasi Variabel
 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; } } } } } 


Di sini Variabel adalah kelas abstrak dasar untuk semua variabel, perlu untuk memisahkan mereka dari komponen biasa. Kelas utama adalah generik , yang menyimpan data itu sendiri dan menyediakan acara untuk mengatur nilai dan mengubahnya.

Komunikasi


Apa hubungannya, saya katakan di artikel sebelumnya. Secara singkat, ini adalah entitas virtual yang memungkinkan komponen untuk menggunakan metode masing-masing dan juga merujuk ke variabel logika. Untuk programmer, koneksi ini lunak dan tidak terlihat dalam kode. Semua komunikasi terbentuk selama proses inisialisasi (baca di bawah ini). Pertimbangkan kelas yang memungkinkan Anda membentuk hubungan.

Titik masuk
 namespace uViLEd.Core { public class INPUT_POINT<T> { public Action<T> Handler; } public class INPUT_POINT { public Action Handler; } } 


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


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


Catatan : di sini perlu dijelaskan satu titik, penangan otomatis untuk mengatur peristiwa dan mengubah nilai variabel hanya digunakan ketika mereka ditetapkan dalam metode Konstruktor , karena pada saat itu referensi ke variabel belum ditetapkan.

Bekerja dengan logika


Penyimpanan


Dalam artikel pertama pada editor logika visual, disebutkan bahwa logika adalah serangkaian variabel, komponen, dan hubungan di antara mereka:

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

Kelas ini tampaknya serializable, tetapi Unity's JsonUtility tidak digunakan untuk serialisasi. Sebagai gantinya, opsi biner digunakan, yang hasilnya disimpan sebagai file dengan ekstensi byte . Mengapa ini dilakukan? Secara umum, alasan utama adalah keamanan, yaitu, untuk opsi memuat logika dari sumber eksternal, dimungkinkan untuk mengenkripsi data, dan secara umum, deserialisasi array byte lebih sulit daripada open json .

Mari kita lihat lebih dekat kelas ComponentsStrorage dan LinksStorage . Sistem menggunakan GUID untuk identifikasi data global. Di bawah ini adalah kode kelas, yang merupakan basis untuk wadah data.

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

Sekarang pertimbangkan kode kelas ComponentsStorage , yang, seperti namanya, menyimpan data tentang komponen-komponen logika:

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

Kelasnya cukup sederhana. Informasi berikut disimpan untuk setiap komponen:

  1. Identifier unik (string GUID ) ditemukan di Identifier
  2. Ketikkan nama
  3. Nama rakitan tempat tipe komponen berada
  4. Json string dengan data serialisasi (hasil JsonUtility.ToJson )
  5. Bendera aktivitas komponen (negara bagian)

Sekarang mari kita lihat kelas LinksStorage . Kelas ini menyimpan informasi tentang hubungan antara komponen, serta tentang referensi ke variabel.

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

Pada prinsipnya, tidak ada yang rumit di kelas ini juga. Setiap tautan berisi informasi berikut:

  1. Tandai yang menunjukkan bahwa tautan ini adalah referensi ke variabel
  2. Tautkan Bendera Aktivitas
  3. Pengidentifikasi (string GUID ) komponen dengan titik output
  4. Pengidentifikasi (string GUID ) komponen titik masuk
  5. Nama titik output dari komponen sumber komunikasi
  6. Nama titik input dari komponen komunikasi target
  7. Nama bidang kelas untuk mengatur referensi variabel
  8. Perintah panggilan komunikasi

Jalankan dari repositori


Sebelum masuk ke rincian kode, pertama saya ingin memikirkan uraian tentang bagaimana controller memulai logika:

  1. Inisialisasi dimulai pada metode Awake pengontrol
  2. Daftar log adegan memuat dan deserializes data logika dari file biner ( TextAsset )
  3. Untuk setiap logika terjadi:
    • Pembuatan Komponen
    • Menyortir tautan berdasarkan CallOrder
    • Mengatur tautan dan referensi variabel
    • Metode Penyortiran Komponen Mono oleh ExecuteOrder


Mari kita pertimbangkan secara lebih rinci setiap aspek dari rantai ini.

Serialisasi dan deserialisasi biner
 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); } } } } 


Memuat data logika dari aset teks (file biner)
 namespace uViLEd.Core { public partial class LogicStorage { public static LogicStorage Load(TextAsset textAsset) => Serialization.Deserialize(textAsset) as LogicStorage; } } 


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


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

Jadi apa yang terjadi dalam fungsi ini:

  1. Bendera aktivitas komponen diperiksa
  2. Mendapatkan jenis komponen dari perakitan
  3. Sebuah instance komponen dibuat berdasarkan tipe
  4. Deserialisasi parameter komponen dari json
  5. Tautan diatur di coroutineHost
  6. Metode konstruktor disebut
  7. Salinan sementara disimpan ke instance komponen
  8. Jika komponen mengimplementasikan antarmuka IDisposable, tautan ke dalamnya disimpan dalam daftar yang sesuai
  9. Mencari metode Mono di komponen


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

Membuat koneksi, salah satu momen paling sulit secara intrinsik di seluruh sistem, pertimbangkan setiap tahap:

  1. Cek tanda aktivitas komunikasi
  2. Dalam daftar sementara komponen yang dibuat dicari, komponen sumber koneksi dan komponen target
  3. Memeriksa jenis koneksi:
    • Jika jenis koneksi adalah referensi ke variabel, maka nilai yang diperlukan diatur menggunakan refleksi
    • Jika koneksi normal, maka refleksi juga digunakan untuk mengatur nilai yang diperlukan untuk metode output dan titik input

  4. Untuk komunikasi normal, periksa dulu apakah komponen mewarisi antarmuka IInputPointParse dan IOutputPointParse .
  5. Bergantung pada hasil paragraf sebelumnya, bidang LinkedInputPoints dalam komponen sumber dan metode penangan dalam komponen target diperoleh melalui refleksi.
  6. Penangan metode diperoleh dengan mengonversi pemanggilan metode dengan mem- bypass MethodInfo.Invoke menjadi panggilan Action<T> sederhana. Namun, mendapatkan tautan seperti itu melalui refleksi terlalu rumit, oleh karena itu, metode khusus telah OUTPUT_POINT<T> di kelas OUTPUT_POINT<T> yang memungkinkan ini. Untuk versi non- generik dari kelas ini, tidak diperlukan tindakan seperti itu.

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

    Metode pertama digunakan ketika titik input tidak menerima parameter apa pun. Metode kedua, masing-masing, untuk titik input dengan parameter.

  7. Melalui refleksi, tautan ke penangan Tindakan ditambahkan ke bidang LinkedInputPoints (yang, seperti yang ditunjukkan di atas, adalah daftar)


Bekerja dengan Metode 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])); } } } 

Setiap metode disimpan dalam kamus melalui tautan ke kelas khusus yang digunakan untuk membuat panggilan. Kelas ini secara otomatis mengubah metode ini menjadi referensi ke Action . Dengan demikian, ada percepatan yang signifikan dari panggilan ke metode Mono dibandingkan dengan MethodInfo.Invoke yang biasa.

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

Catatan : kelebihan metode Panggilan dengan parameter bool digunakan untuk metode Mono seperti ApplicationPause , dll.

Logika pemicu eksternal


Peluncuran logika eksternal adalah peluncuran selama operasi aplikasi, logika tersebut tidak diatur dalam adegan dan tidak ada tautan ke sana. Itu dapat diunduh dari bundel, atau dari sumber daya. Secara umum, mulai dari luar tidak jauh berbeda dengan mulai di awal adegan, dengan pengecualian bekerja dengan metode Mono .

Kode metode untuk eksekusi logika eksternal (tangguhan)
 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); } } 


Seperti yang Anda lihat, semua referensi ke metode Mono disimpan dalam kamus lokal, setelah itu metode Start diluncurkan dan kemudian semua metode lain ditambahkan ke kamus umum.

Menjalankan logika sebagai contoh


Selama pengoperasian aplikasi, mungkin perlu untuk menjalankan logika tertentu untuk waktu tertentu, atau sampai ia melakukan tugasnya. Opsi pemicu logika sebelumnya tidak diizinkan untuk melakukan hal ini, karena logika dipicu sepanjang umur adegan.

Untuk menjalankan logika sebagai instance, tautan ke semua instance komponen dan sejenisnya disimpan, yang, setelah menyelesaikan pekerjaan, dapat dihapus, sehingga membersihkan memori.

Kode Kelas Gudang Data Instans Logika:

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

Peluncuran logika itu sendiri mirip dengan peluncuran eksternal yang dijelaskan sebelumnya.
 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; } 


Dapat dilihat dari kode bahwa setelah membuat logika, data tentangnya disimpan dalam instance kelas khusus, dan setelah memulai, pengidentifikasi unik untuk instance logika dikembalikan.

Pengenal ini diperlukan untuk memanggil fungsi stop and clear memory.
 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); } 


Mematikan adegan dan membersihkan memori


Saat membuat ScriptableObject dalam adegan berjalan melalui ScriptableObject.CreateInstance , instance berperilaku sama dengan MonoBehaviour , yaitu, ketika membongkar dari tempat kejadian, OnDestroy akan dipanggil untuk masing-masing dan itu akan dihapus dari memori. Namun, seperti yang dikatakan dalam artikel sebelumnya, komponen dapat mewarisi IDisposable , jadi saya membersihkannya dalam metode 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; } 

Catatan : seperti yang Anda lihat, jika ada logika pada saat bongkar muat adegan, maka pembersihan terjadi di dalamnya.

Masalah dan solusi dengan objek Unity


Di bagian pertama artikel tentang editor visual, saya menyebutkan masalah mempertahankan tautan ke objek pemandangan, yang terkait dengan ketidakmampuan untuk memulihkan data serial. Hal ini disebabkan oleh fakta bahwa pengidentifikasi unik objek pemandangan berbeda setiap kali adegan diluncurkan. Solusi untuk masalah ini adalah satu-satunya pilihan - ini adalah untuk mentransfer penyimpanan tautan di tempat kejadian. Untuk tujuan ini, repositori khusus dan kelas pembungkus ditambahkan ke pengontrol logika di mana komponen logika menerima referensi ke objek Unity , termasuk sumber daya, cetakan dan aset lainnya.

Kode untuk penyimpanan
 [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; } 

Di sini:

  1. Id - pengidentifikasi unik dari aset atau objek pemandangan
  2. Obj - tautan ke aset atau objek pemandangan


Seperti yang dapat Anda lihat dari kode, tidak ada yang super rumit.

Sekarang pertimbangkan kelas pembungkus untuk objek 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; } } 

Catatan : flag _objNotFound diperlukan agar tidak mencari repositori setiap kali jika tidak ada objek di dalamnya.

Performa


Sekarang, saya pikir ada gunanya memikirkan pertanyaan seperti produktivitas. Jika Anda hati-hati melihat seluruh kode di atas, Anda dapat memahami bahwa dalam aplikasi yang sudah berjalan, sistem tidak memengaruhi kecepatan, mis. semuanya akan berfungsi seperti aplikasi Unity biasa . Saya membandingkan pembaruan MonoBehaviour biasa dan melalui uViLEd dan tidak dapat mencapai setidaknya perbedaan yang dapat ditunjukkan di sini, semua nomor paritas hingga 1000 panggilan per frame. Satu-satunya hambatan adalah kecepatan pemuatan adegan, tetapi bahkan di sini saya tidak bisa mendapatkan angka signifikan, meskipun di antara platform (saya memeriksa Android dan iOS) perbedaannya besar. Dalam logika 70 komponen dan sekitar 150 koneksi (termasuk referensi ke variabel), angkanya adalah sebagai berikut:

  1. Android (sangat biasa-biasa saja MediaTek 8-core)
    - Di awal aplikasi - ~ 750ms
    - Menjalankan adegan dalam aplikasi yang sedang berjalan ~ 250ms
  2. iOS (iPhone 5s)
    - Di awal aplikasi - ~ 100ms
    - Menjalankan adegan di aplikasi yang sedang berjalan ~ 50ms

Kesimpulan


Arsitektur kernel sistem uViLEd cukup sederhana dan mudah, tidak disarankan untuk menggunakannya secara tidak langsung dari editor visual, meskipun saya tidak mengecualikan bahwa beberapa solusi terbukti bermanfaat bagi seseorang. Namun demikian, artikel ini penting untuk pemahaman umum tentang cara kerja sistem, bagian terpenting terakhir adalah editor itu sendiri. Dan bagian terakhir terakhir dari seri, yang akan menjadi yang paling penting, akan dikhususkan untuk ini.

PS: penulisan artikel itu sangat panjang, yang saya minta maaf. Awalnya, saya berencana untuk melepaskan semua bagian yang tersisa sekaligus, tetapi rilis versi baru Unity 3d, serta guncangan penting (menyenangkan dan tidak begitu), mengubah rencana.

Editor logika visual untuk Unity3d. Bagian 1

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


All Articles