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 1Mengapa 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 .

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:
- Menyimpan Tautan Objek Persatuan
- Mulai logika di awal adegan
- Merupakan akses ke metode untuk menjalankan logika dari sumber eksternal
- Menyediakan karya metode Mono dalam komponen
Kode dasar kelas pengontrolnamespace 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:
- Identifier unik (string GUID ) ditemukan di Identifier
- Ketikkan nama
- Nama rakitan tempat tipe komponen berada
- Json string dengan data serialisasi (hasil JsonUtility.ToJson )
- 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:
- Tandai yang menunjukkan bahwa tautan ini adalah referensi ke variabel
- Tautkan Bendera Aktivitas
- Pengidentifikasi (string GUID ) komponen dengan titik output
- Pengidentifikasi (string GUID ) komponen titik masuk
- Nama titik output dari komponen sumber komunikasi
- Nama titik input dari komponen komunikasi target
- Nama bidang kelas untuk mengatur referensi variabel
- Perintah panggilan komunikasi
Jalankan dari repositori
Sebelum masuk ke rincian kode, pertama saya ingin memikirkan uraian tentang bagaimana controller memulai logika:
- Inisialisasi dimulai pada metode Awake pengontrol
- Daftar log adegan memuat dan deserializes data logika dari file biner ( TextAsset )
- 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:
- Bendera aktivitas komponen diperiksa
- Mendapatkan jenis komponen dari perakitan
- Sebuah instance komponen dibuat berdasarkan tipe
- Deserialisasi parameter komponen dari json
- Tautan diatur di coroutineHost
- Metode konstruktor disebut
- Salinan sementara disimpan ke instance komponen
- Jika komponen mengimplementasikan antarmuka IDisposable, tautan ke dalamnya disimpan dalam daftar yang sesuai
- 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:
- Cek tanda aktivitas komunikasi
- Dalam daftar sementara komponen yang dibuat dicari, komponen sumber koneksi dan komponen target
- 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
- Untuk komunikasi normal, periksa dulu apakah komponen mewarisi antarmuka IInputPointParse dan IOutputPointParse .
- Bergantung pada hasil paragraf sebelumnya, bidang LinkedInputPoints dalam komponen sumber dan metode penangan dalam komponen target diperoleh melalui refleksi.
- 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.
- 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:- Id - pengidentifikasi unik dari aset atau objek pemandangan
- 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:- Android (sangat biasa-biasa saja MediaTek 8-core)
- Di awal aplikasi - ~ 750ms
- Menjalankan adegan dalam aplikasi yang sedang berjalan ~ 250ms
- 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