Einführung
Hallo liebe Leser, im heutigen Artikel möchte ich das Thema Architektur des Kernels des visuellen Logik-Editors für
Unity3d hervorheben . Dies ist der zweite Teil der Serie. Den vorherigen können Sie hier lesen. Worüber werden wir also sprechen? Der visuelle Editor basiert auf dem Kern, mit dem Sie Logikdaten ausführen, laden und speichern können. Der Kernel verwendet wiederum, wie im vorherigen Artikel erwähnt,
ScriptableObject als Basisklasse für die Arbeit mit Logikkomponenten. Lassen Sie uns all diese Aspekte genauer betrachten.
Artikel in der Reihe:
→
Visual Logic Editor für Unity3d. Teil 1Warum ist ScriptableObject?
Bevor ich mit der Entwicklung begann, dachte ich lange darüber nach, worauf ich das System aufbauen sollte. In der allerersten Form war es
MonoBehaviour , aber ich musste diese Idee aufgeben, da diese Skripte als Komponenten an
GameObject hängen sollten. Der nächste Schritt war die Idee, eine eigene Klasse zu verwenden, die kein Nachkomme von
UnityEngine.Object ist . Diese Option hat sich jedoch nicht durchgesetzt, obwohl sie recht gut funktioniert hat. Sie hat sich jedoch in die
Länge gezogen und den Serializer, den Inspektor, den Garbage Collector usw. geschrieben. Daher war die Verwendung von
ScriptableObject , dessen Lebenszyklus
MonoBehaviour ähnelt, bei der Erstellung nur sinnvoll tritt auf,
während die Anwendung über
ScriptableObject.CreateInstance ausgeführt wird . Darüber hinaus wurde dieses Problem automatisch mit
JsonUtility (obwohl es jetzt kein Problem ist) und dem
Unity- Inspektor behoben.
Architektur
Unten sehen Sie ein verallgemeinertes Diagramm, aus dem der
uViLEd- Kern
besteht .

Betrachten wir jedes Element genauer.
Controller
Der Controller ist das Hauptelement des Kernels, bei dem es sich um ein
MonoBehavior- Skript handelt (das einzige im gesamten System). Die Controller-Klasse ist ein Singleton und für alle Komponenten der Logik zugänglich. Was diese Klasse macht:
- Speichert Unity- Objektverknüpfungen
- Startet die Logik am Anfang der Szene
- Stellt den Zugriff auf Methoden zum Ausführen von Logik aus externen Quellen dar
- Bietet die Arbeit von Mono- Methoden in Komponenten
Basiscode der Controller-Klassenamespace 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)); } } } }
Hinweis : Jede Szene verfügt über einen eigenen Controller und eine eigene Logik.
Hinweis : Weitere Details zum Laden von Logikdaten und deren Start werden separat erläutert.
Kernelemente des uViLEd-Kernels
Komponente
Im ersten Teil habe ich bereits gesagt, dass die Komponente ein
ScriptableObject ist . Alle Komponenten sind Nachkommen der
LogicComponent- Klasse, was wiederum einfach ist.
namespace uViLEd.Core { public abstract class LogicComponent : ScriptableObject { protected MonoBehaviour coroutineHost => _logicHost; private MonoBehaviour _logicHost; public virtual void Constructor() { } } }
Hier ist
coroutineHost eine Verknüpfung zu einer Logiksteuerung, die nur der
Einfachheit halber eingeführt wurde und, wie der Name schon sagt, für die Arbeit mit Coroutinen verwendet wird. Die Verwendung dieser Abstraktion ist erforderlich, um die Komponenten von anderem Code zu trennen, der im
Unity- Projekt vorhanden ist.
Variablen
Variablen sind, wie in einem vorherigen Artikel erwähnt, spezielle Komponenten zum Speichern von Daten. Der Code für diese Variablen wird unten dargestellt.
Variabler Implementierungscode 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; } } } } }
Hier ist
Variable eine grundlegende abstrakte Klasse für alle Variablen, die erforderlich ist, um sie von normalen Komponenten zu trennen. Die Hauptklasse ist
generisch , die die Daten selbst speichert und Ereignisse zum Festlegen und Ändern des Werts bereitstellt.
Kommunikation
Was sind die Zusammenhänge, die ich in einem früheren Artikel erzählt habe. Kurz gesagt, es ist eine virtuelle Entität, die es Komponenten ermöglicht, die Methoden des anderen zu verwenden und auch auf logische Variablen zu verweisen. Für den Programmierer ist diese Verbindung weich und im Code nicht sichtbar. Alle Kommunikationen werden während des Initialisierungsprozesses gebildet (lesen Sie weiter unten). Betrachten Sie die Klassen, mit denen Sie Beziehungen aufbauen können.
Einstiegspunkt namespace uViLEd.Core { public class INPUT_POINT<T> { public Action<T> Handler; } public class INPUT_POINT { public Action Handler; } }
Ausstiegspunkt 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(); } } } }
Variablenreferenz 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; } } } }
Hinweis : Hier lohnt es sich, einen Punkt zu erläutern. Automatische Handler zum Festlegen von Ereignissen und zum Ändern des Werts einer Variablen werden nur verwendet, wenn sie in der
Konstruktormethode festgelegt wurden , da zu diesem Zeitpunkt die Verweise auf die Variablen noch nicht festgelegt wurden.
Arbeite mit Logik
Lagerung
Im ersten Artikel über den visuellen Logikeditor wurde erwähnt, dass Logik eine Reihe von Variablen, Komponenten und deren Beziehungen ist:
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(); } }
Diese Klasse ist anscheinend serialisierbar, aber
die JsonUtility von Unity wird nicht für die Serialisierung verwendet. Stattdessen wird eine binäre Option verwendet, deren Ergebnis als Datei mit der Erweiterung
bytes gespeichert wird. Warum wird das gemacht? Im Allgemeinen ist der Hauptgrund die Sicherheit, dh für die Option, Logik von einer externen Quelle zu laden, ist es möglich, Daten zu verschlüsseln, und im Allgemeinen ist das Deserialisieren eines Byte-Arrays schwieriger als das Öffnen von
json .
Schauen wir uns die
Klassen ComponentsStrorage und
LinksStorage genauer an . Das System verwendet eine
GUID zur globalen Datenidentifikation. Unten finden Sie den Klassencode, der die Basis für Datencontainer darstellt.
namespace uViLEd.Core { [Serializable] public abstract class Identifier { public string Id { get; } public Identifier() { if (!string.IsNullOrEmpty(Id)) return; Id = System.Guid.NewGuid().ToString(); } } }
Betrachten Sie nun den Code der
ComponentsStorage- Klasse, die, wie der Name schon sagt, Daten zu den Komponenten der Logik speichert:
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>(); } }
Der Unterricht ist recht einfach. Die folgenden Informationen werden für jede Komponente gespeichert:
- Eindeutiger Bezeichner ( GUID- Zeichenfolge) in Bezeichner gefunden
- Geben Sie den Namen ein
- Der Name der Baugruppe, in der sich der Komponententyp befindet
- Json-String mit Serialisierungsdaten (Ergebnis von JsonUtility.ToJson )
- Flag für Komponentenaktivität (Status)
Schauen wir uns nun die
LinksStorage- Klasse an. Diese Klasse speichert Informationen zu den Beziehungen zwischen Komponenten sowie zu Verweisen auf Variablen.
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>(); } }
Grundsätzlich gibt es auch in dieser Klasse nichts Kompliziertes. Jeder Link enthält die folgenden Informationen:
- Flag, das angibt, dass dieser Link eine Referenz auf eine Variable ist
- Aktivitätsflag verknüpfen
- Die Kennung ( GUID- Zeichenfolge) der Komponente mit dem Ausgabepunkt
- Die Kennung ( GUID- Zeichenfolge) der Einstiegspunktkomponente
- Der Name des Ausgabepunkts der Kommunikationsquellenkomponente
- Name des Eingabepunkts der Zielkommunikationskomponente
- Klassenfeldname zum Festlegen einer Variablenreferenz
- Reihenfolge der Kommunikationsanrufe
Aus dem Repository ausführen
Bevor ich auf die Details des Codes eingehe, möchte ich zunächst auf die Beschreibung der Reihenfolge eingehen, in der die Steuerung die Logik startet:
- Die Initialisierung beginnt mit der Awake-Methode des Controllers
- Die Liste der Szenenlogiken lädt und deserialisiert die Logikdaten aus einer Binärdatei ( TextAsset ).
- Für jede Logik tritt auf:
- Komponentenerstellung
- Links nach CallOrder sortieren
- Festlegen von Links und Variablenreferenzen
- Sortieren von Monokomponentenmethoden nach ExecuteOrder
Lassen Sie uns jeden Aspekt dieser Kette genauer betrachten.
Binäre Serialisierung und Deserialisierung 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); } } } }
Laden von Logikdaten aus einem Text-Asset (Binärdatei) namespace uViLEd.Core { public partial class LogicStorage { public static LogicStorage Load(TextAsset textAsset) => Serialization.Deserialize(textAsset) as LogicStorage; } }
Logikstart 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); } }
Komponentenerstellung 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); }
Was passiert also in dieser Funktion:
- Das Komponentenaktivitätsflag ist aktiviert
- Abrufen des Komponententyps aus der Baugruppe
- Eine Komponenteninstanz wird nach Typ erstellt
- Deserialisierung von Komponentenparametern von json
- Der Link wird in coroutineHost gesetzt
- Konstruktormethode aufgerufen
- Eine temporäre Kopie wird in der Komponenteninstanz gespeichert
- Wenn die Komponente die IDisposable-Schnittstelle implementiert, wird der Link dazu in der entsprechenden Liste gespeichert
- Suche nach Mono- Methoden in der Komponente
Links erstellen 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; } } }
Das Herstellen einer Verbindung, einer der schwierigsten Momente im gesamten System, berücksichtigt jede Phase:
- Überprüft das Kommunikationsaktivitätsflag
- In der temporären Liste der erstellten Komponenten werden die Quellkomponente der Verbindung und die Zielkomponente durchsucht
- Überprüft die Art der Verbindung:
- Wenn der Verbindungstyp eine Referenz auf eine Variable ist, werden die erforderlichen Werte mithilfe der Reflexion festgelegt
- Wenn die Verbindung normal ist, wird die Reflexion auch verwendet, um die erforderlichen Werte für die Methoden der Ausgabe und der Eingabepunkte festzulegen
- Überprüfen Sie bei normaler Kommunikation zunächst, ob die Komponente die Schnittstellen IInputPointParse und IOutputPointParse erbt.
- Abhängig von den Ergebnissen des vorherigen Absatzes werden das LinkedInputPoints- Feld in der Quellkomponente und die Handlermethode in der Zielkomponente durch Reflexion erhalten.
- Die Methodenhandler werden erhalten, indem der Methodenaufruf unter Umgehung von MethodInfo.Invoke in einen einfachen
Action<T>
-Aufruf konvertiert wird. Das Erhalten einer solchen Verbindung durch Reflexion ist jedoch zu kompliziert, weshalb in der Klasse OUTPUT_POINT<T>
spezielle Methoden OUTPUT_POINT<T>
, die dies ermöglichen. Für eine nicht generische Version dieser Klasse ist keine solche Aktion erforderlich.
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; } }
Die erste Methode wird verwendet, wenn der Eingabepunkt keine Parameter akzeptiert. Die zweite Methode jeweils für den Eingabepunkt mit einem Parameter.
- Durch Reflexion wird dem Feld LinkedInputPoints (das, wie oben gezeigt, eine Liste ist) ein Link zum Aktionshandler hinzugefügt.
Arbeiten mit Monomethoden 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])); } } }
Jede Methode wird im Wörterbuch über eine Verknüpfung zu einer speziellen Klasse gespeichert, über die der Aufruf erfolgt. Diese Klasse konvertiert die Methode automatisch in einen Verweis auf
Action . Daher gibt es eine signifikante Beschleunigung der Aufrufe von
Mono- Methoden im Vergleich zu den üblichen
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); }
Hinweis : Das Überladen der
Call- Methode mit dem Parameter
bool wird für
Mono- Methoden wie
ApplicationPause usw. verwendet.
Externe Triggerlogik
Ein externer Start der Logik ist ein Start während des Anwendungsbetriebs. Diese Logik ist nicht in der Szene festgelegt und es besteht keine Verknüpfung dazu. Es kann aus dem Bundle oder aus Ressourcen heruntergeladen werden. Im Allgemeinen unterscheidet sich das Starten von außen nicht wesentlich vom Starten am Anfang der Szene, mit Ausnahme der Arbeit mit
Mono- Methoden.
Methodencode für die externe (verzögerte) Logikausführung 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); } }
Wie Sie sehen können, werden alle Verweise auf
Mono- Methoden im lokalen Wörterbuch gespeichert. Anschließend werden die Start-Methoden gestartet und anschließend alle anderen Methoden zum allgemeinen Wörterbuch hinzugefügt.
Logik als Instanz ausführen
Während des Betriebs der Anwendung kann es erforderlich sein, eine bestimmte Logik für eine bestimmte Zeit oder bis zur Ausführung ihrer Aufgabe auszuführen. Bisherige Optionen zum Auslösen von Logik dürfen dies nicht tun, da Logik während der gesamten Lebensdauer der Szene ausgelöst wird.
Um die Logik als Instanz auszuführen, werden Verknüpfungen zu allen Instanzen von Komponenten und dergleichen gespeichert, die nach Abschluss der Arbeit gelöscht werden können, wodurch der Speicher gelöscht wird.
Data Warehouse-Klassencode für Logikinstanzen:
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>(); }
Der Logikstart selbst ähnelt dem zuvor beschriebenen externen Start. 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; }
Aus dem Code ist ersichtlich, dass nach dem Erstellen der Logik Daten darüber in einer Instanz einer speziellen Klasse gespeichert werden und nach dem Starten eine eindeutige Kennung für die Instanz der Logik zurückgegeben wird.
Diese Kennung wird benötigt, um die Funktion zum Stoppen und Löschen des Speichers aufzurufen. 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); }
Die Szene herunterfahren und den Speicher löschen
Beim Erstellen eines
ScriptableObject in einer laufenden Szene über
ScriptableObject.CreateInstance verhält sich die Instanz genauso wie
MonoBehaviour , d. H. Beim Entladen aus der Szene wird
OnDestroy für jede aufgerufen und aus dem Speicher gelöscht. Wie bereits in einem früheren Artikel erwähnt, kann eine Komponente
IDisposable erben.
Daher bereinige ich sie in der
OnDisable- Methode:
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; }
Hinweis : Wie Sie sehen können, erfolgt in diesen Fällen eine Bereinigung, wenn zum Zeitpunkt des Entladens der Szene logische Instanzen vorhanden sind.
Problem und Lösung mit Unity-Objekten
Im ersten Teil des Artikels über den visuellen Editor habe ich das Problem der Aufrechterhaltung von Links zu Szenenobjekten erwähnt, das mit der Unfähigkeit zusammenhängt, serialisierte Daten wiederherzustellen. Dies liegt an der Tatsache, dass die eindeutige Kennung der Szenenobjekte bei jedem Start der Szene unterschiedlich ist. Die Lösung für dieses Problem ist die einzige Möglichkeit, die Speicherung von Links auf die Szene zu übertragen. Zu diesem Zweck wurden dem Logikcontroller ein spezielles Repository und eine Wrapper-Klasse hinzugefügt, über die die Logikkomponenten Verweise auf
Unity- Objekte erhalten, einschließlich Ressourcen, Prefabs und anderer Assets.
Code für die Speicherung [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; }
Hier:- ID - eindeutige Kennung des Assets oder Szenenobjekts
- Objekt - Link zu Asset- oder Szenenobjekten
Wie Sie dem Code entnehmen können, ist nichts super kompliziert.Betrachten Sie nun eine Wrapper-Klasse für Unity-Objekte [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; } }
Hinweis : Das Flag _objNotFound ist erforderlich, um das Repository nicht jedes Mal zu durchsuchen, wenn sich kein Objekt darin befindet. Leistung
Ich denke, es lohnt sich, sich mit Fragen wie der Produktivität zu befassen. Wenn Sie sich den gesamten obigen Code genau ansehen, können Sie verstehen, dass das System in einer bereits laufenden Anwendung die Geschwindigkeit nicht beeinflusst, d. H. Alles funktioniert wie gewohnt Unity- Anwendung . Ich habe das Update des üblichen MonoBehaviour und über uViLEd verglichen und konnte zumindest keinen Unterschied feststellen , der hier angegeben werden könnte. Alle Nummern sind Paritäten bis zu 1000 Anrufe pro Frame. Der einzige Engpass ist die Geschwindigkeit beim Laden der Szene, aber selbst hier konnte ich keine signifikanten Zahlen erhalten, obwohl zwischen den Plattformen (ich habe Android und iOS überprüft) Der Unterschied ist groß. In der Logik von 70 Komponenten und ungefähr 150 Verbindungen (einschließlich Verweisen auf Variablen) waren die Zahlen wie folgt:- Android (sehr mittelmäßig MediaTek 8-Core)
- Zu Beginn der Anwendung - ~ 750 ms
- Ausführen der Szene in der laufenden Anwendung ~ 250 ms
- iOS (iPhone 5s)
- Zu Beginn der Anwendung - ~ 100 ms
- Ausführen der Szene in der laufenden Anwendung ~ 50 ms
Fazit
Die Kernel-Architektur des uViLEd- Systems ist recht einfach und unkompliziert. Es ist nicht ratsam, sie indirekt über den visuellen Editor zu verwenden, obwohl ich nicht ausschließe, dass sich einige Lösungen für jemanden als nützlich erweisen. Trotzdem ist dieser Artikel wichtig für ein allgemeines Verständnis der Funktionsweise des Systems, dessen letzter wichtiger Teil der Herausgeber selbst ist. Und der letzte letzte Teil der Serie, der am ereignisreichsten sein wird, wird diesem Thema gewidmet sein.PS: Das Schreiben des Artikels war sehr lang, wofür ich mich entschuldige. Ursprünglich hatte ich vor, alle verbleibenden Teile auf einmal freizugeben, aber die Veröffentlichung neuer Versionen von Unity 3d sowie wichtige (angenehme und nicht so) Schocks änderten die Pläne.→ Visual Logic Editor für Unity3d. Teil 1