Éditeur de logique visuelle pour Unity3d. 2e partie

Présentation


Bonjour chers lecteurs, dans l'article d'aujourd'hui, je voudrais souligner le thème de l'architecture du noyau de l'éditeur de logique visuelle pour Unity3d . Ceci est la deuxième partie de la série. Vous pouvez lire le précédent ici . Alors de quoi parlerons-nous? L'éditeur visuel est basé sur le noyau principal, ce qui vous permet de démarrer, de charger et de stocker des données logiques. À son tour, le noyau utilise, comme cela a été mentionné dans l'article précédent, ScriptableObject , comme classe de base pour travailler avec des composants logiques. Examinons plus en détail tous ces aspects.

Articles de la série:

Éditeur de logique visuelle pour Unity3d. Partie 1

Pourquoi ScriptableObject?


Avant de commencer le développement, j'ai longuement réfléchi à la structure du système. Dans la toute première forme - c'était MonoBehaviour , mais j'ai dû abandonner cette idée, car ces scripts devraient se bloquer sur GameObject , en tant que composants. L'étape suivante a été l'idée d'utiliser votre propre classe, qui n'est pas un descendant de UnityEngine.Object . Mais cette option n'a pas pris racine, même si elle fonctionnait assez bien, mais elle a traîné, écrivant son sérialiseur, son inspecteur, son garbage collector, etc. En conséquence, la seule façon raisonnable était d'utiliser ScriptableObject , dont le cycle de vie est similaire à MonoBehaviour , si la création se produit lorsque l' application s'exécute via ScriptableObject.CreateInstance . De plus, ce problème a été résolu automatiquement à l'aide de JsonUtility (bien que ce ne soit plus un problème) et de l'inspecteur Unity .

L'architecture


Vous trouverez ci-dessous un diagramme généralisé de la composition du noyau uViLEd .

image

Examinons chaque élément plus en détail.

Contrôleur


Le contrôleur est l'élément principal du noyau, qui est un script MonoBehavior (le seul de tout le système). La classe de contrôleur est un singleton et est accessible à tous les composants de la logique. Ce que fait cette classe:

  1. Stocke les liens d'objet Unity
  2. Démarre la logique au début de la scène
  3. Représente l'accès aux méthodes pour exécuter la logique à partir de sources externes
  4. Fournit le travail des méthodes Mono dans les composants

Code de base de la classe du contrôleur
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)); } } } } 


Remarque : chaque scène a son propre contrôleur et son propre ensemble de logique.
Remarque : plus de détails sur le chargement des données logiques et leur lancement seront discutés séparément.

Éléments de base du noyau uViLEd


Composant


Dans la première partie, j'ai déjà dit que le composant est un ScriptableObject . Tous les composants sont des descendants de la classe LogicComponent , qui à son tour est simple.

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

Ici, coroutineHost est un lien vers un contrôleur logique, qui a été introduit juste pour plus de commodité et est utilisé, comme son nom l'indique, pour travailler avec des coroutines. L'utilisation de cette abstraction est nécessaire afin de séparer les composants des autres codes présents dans le projet Unity .

Variables


Les variables, comme mentionné dans un article précédent, sont des composants spécialisés pour le stockage de données, leur code est présenté ci-dessous.

Code de mise en œuvre variable
 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; } } } } } 


Ici, Variable est une classe abstraite de base pour toutes les variables, elle est nécessaire pour les séparer des composants ordinaires. La classe principale est générique , qui stocke les données elles-mêmes et fournit des événements pour définir la valeur et la modifier.

Les communications


Quelles sont les connexions, j'ai dit dans un précédent article. En bref, il s'agit d'une entité virtuelle qui permet aux composants d'utiliser les méthodes les uns des autres et de faire également référence aux variables logiques. Pour le programmeur, cette connexion est douce et non visible dans le code. Toutes les communications sont formées pendant le processus d'initialisation (lisez-le ci-dessous). Considérez les classes qui vous permettent de former des relations.

Point d'entrée
 namespace uViLEd.Core { public class INPUT_POINT<T> { public Action<T> Handler; } public class INPUT_POINT { public Action Handler; } } 


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


Référence variable
 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; } } } } 


Remarque : ici, il convient d'expliquer un point, les gestionnaires automatiques pour définir des événements et modifier la valeur d'une variable ne sont utilisés que lorsqu'ils sont définis dans la méthode Constructor , car à ce moment-là, les références aux variables n'ont pas encore été définies.

Travailler avec la logique


Stockage


Dans le premier article sur l'éditeur de logique visuelle, il a été mentionné que la logique est un ensemble de variables, de composants et des relations entre eux:

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

Cette classe est apparemment sérialisable, mais JsonUtility d' Unity n'est pas utilisé pour la sérialisation. Au lieu de cela, une option binaire est utilisée, dont le résultat est enregistré en tant que fichier avec l'extension d' octets . Pourquoi est-ce fait? En général, la principale raison est la sécurité, c'est-à-dire que pour l'option de chargement de la logique à partir d'une source externe, il est possible de chiffrer les données et, en général, la désérialisation d'un tableau d'octets est plus difficile que d'ouvrir json .

Examinons de plus près les classes ComponentsStrorage et LinksStorage . Le système utilise un GUID pour l'identification globale des données. Ci-dessous se trouve le code de classe, qui est la base des conteneurs de données.

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

Considérons maintenant le code de la classe ComponentsStorage , qui, comme son nom l'indique, stocke des données sur les composants de la logique:

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

La classe est assez simple. Les informations suivantes sont stockées pour chaque composant:

  1. Identifiant unique (chaîne GUID ) trouvé dans l' identifiant
  2. Tapez le nom
  3. Le nom de l'assemblage dans lequel se trouve le type de composant
  4. Chaîne Json avec données de sérialisation (résultat de JsonUtility.ToJson )
  5. Indicateur d'activité (état) du composant

Examinons maintenant la classe LinksStorage . Cette classe stocke des informations sur les relations entre les composants, ainsi que sur les références aux variables.

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

En principe, il n'y a rien de compliqué non plus dans cette classe. Chaque lien contient les informations suivantes:

  1. Indicateur signalant que ce lien fait référence à une variable
  2. Indicateur d'activité de lien
  3. L'identifiant (chaîne GUID ) du composant avec le point de sortie
  4. L'identifiant (chaîne GUID ) du composant point d'entrée
  5. Le nom du point de sortie du composant source de communication
  6. Nom du point d'entrée du composant de communication cible
  7. Nom de champ de classe pour définir une référence de variable
  8. Ordre d'appel de communication

Exécuter à partir du référentiel


Avant d'entrer dans les détails du code, je veux d'abord m'attarder sur la description de la séquence de démarrage de la logique par le contrôleur:

  1. L'initialisation démarre dans la méthode Awake du contrôleur
  2. La liste des logiques de scène charge et désérialise les données logiques d'un fichier binaire ( TextAsset )
  3. Pour chaque logique se produit:
    • Création de composants
    • Tri des liens par CallOrder
    • Définition de liens et de références de variables
    • Tri des méthodes de composant mono par ExecuteOrder


Examinons plus en détail chaque aspect de cette chaîne.

Sérialisation et désérialisation binaires
 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); } } } } 


Chargement de données logiques à partir d'un élément texte (fichier binaire)
 namespace uViLEd.Core { public partial class LogicStorage { public static LogicStorage Load(TextAsset textAsset) => Serialization.Deserialize(textAsset) as LogicStorage; } } 


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


Création de composants
 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); } 

Alors, que se passe-t-il dans cette fonction:

  1. L'indicateur d'activité du composant est vérifié
  2. Obtention du type de composant à partir de l'assemblage
  3. Une instance de composant est créée par type
  4. Désérialisation des paramètres des composants de json
  5. Le lien est défini dans coroutineHost
  6. Méthode constructeur appelée
  7. Une copie temporaire est enregistrée dans l'instance de composant
  8. Si le composant implémente l'interface IDisposable, le lien vers celui-ci est stocké dans la liste correspondante
  9. Recherche de méthodes Mono dans le composant


Création de liens
 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; } } } 

La création d'une connexion, l'un des moments les plus intrinsèquement difficiles de tout le système, considère chaque étape:

  1. Vérifie l'indicateur d'activité de communication
  2. Dans la liste temporaire des composants créés sont recherchés, le composant source de la connexion et la cible
  3. Vérifie le type de connexion:
    • Si le type de connexion est une référence à une variable, les valeurs nécessaires sont définies à l'aide de la réflexion
    • Si la connexion est normale, la réflexion est également utilisée pour définir les valeurs nécessaires pour les méthodes des points de sortie et d'entrée

  4. Pour les communications normales, vérifiez d'abord si le composant hérite des interfaces IInputPointParse et IOutputPointParse .
  5. En fonction des résultats du paragraphe précédent, le champ LinkedInputPoints dans le composant source et la méthode du gestionnaire dans le composant cible sont obtenus par réflexion.
  6. Les gestionnaires de méthodes sont obtenus en convertissant l'appel de méthode en contournant MethodInfo.Invoke en un appel Action<T> simple. Cependant, l'obtention d'un tel lien par réflexion est trop compliquée, par conséquent, des méthodes spéciales ont été OUTPUT_POINT<T> dans la OUTPUT_POINT<T> qui permettent cela. Pour une version non générique de cette classe, aucune action de ce type n'est requise.

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

    La première méthode est utilisée lorsque le point d'entrée n'accepte aucun paramètre. La deuxième méthode, respectivement, pour le point d'entrée avec un paramètre.

  7. Par réflexion, un lien vers le gestionnaire d' actions est ajouté au champ LinkedInputPoints (qui, comme illustré ci-dessus, est une liste)


Travailler avec des méthodes 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])); } } } 

Chaque méthode est stockée dans le dictionnaire via un lien vers une classe spéciale à travers laquelle l'appel est effectué. Cette classe convertit automatiquement la méthode en référence à Action . Ainsi, il y a une accélération significative des appels aux méthodes Mono par rapport à la méthode MethodInfo.Invoke habituelle.

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

Remarque : la surcharge de la méthode Call avec le paramètre bool est utilisée pour les méthodes Mono comme ApplicationPause , etc.

Logique de déclenchement externe


Un lancement externe de logique est un lancement pendant le fonctionnement de l'application, une telle logique n'est pas définie dans la scène et il n'y a aucun lien avec celle-ci. Il peut être téléchargé à partir du bundle ou des ressources. En général, partir de l'extérieur n'est pas très différent de commencer au début de la scène, à l'exception du travail avec les méthodes Mono .

Code de méthode pour l'exécution logique externe (différée)
 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); } } 


Comme vous pouvez le voir, toutes les références aux méthodes Mono sont enregistrées dans le dictionnaire local, après quoi les méthodes Start sont lancées, puis toutes les autres méthodes sont ajoutées au dictionnaire général.

Exécution de la logique en tant qu'instance


Pendant le fonctionnement de l'application, il peut être nécessaire d'exécuter une certaine logique pendant un certain temps ou jusqu'à ce qu'elle exécute sa tâche. Les options de déclenchement logique précédentes ne sont pas autorisées à le faire, car la logique est déclenchée tout au long de la vie de la scène.

Pour exécuter la logique en tant qu'instance, les liens vers toutes les instances de composants et similaires sont enregistrés, qui, une fois le travail terminé, peuvent être supprimés, ce qui efface la mémoire.

Code de classe d'entrepôt de données d'instance logique:

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

Le lancement logique lui-même est similaire au lancement externe décrit précédemment.
 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; } 


On peut voir d'après le code qu'après avoir créé la logique, les données à son sujet sont stockées dans une instance d'une classe spécialisée, et après le démarrage, un identifiant unique pour l'instance de logique est renvoyé.

Cet identifiant est nécessaire pour appeler la fonction d'arrêt et d'effacement de la mémoire.
 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); } 


Arrêter la scène et vider la mémoire


Lors de la création d'un ScriptableObject dans une scène en cours d'exécution via ScriptableObject.CreateInstance , l'instance se comporte de la même manière que MonoBehaviour , c'est-à-dire que lors du déchargement de la scène, OnDestroy sera appelé pour chacun et il sera supprimé de la mémoire. Cependant, comme cela a été dit dans un article précédent, un composant peut hériter d' IDisposable , donc je le nettoie dans la méthode 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; } 

Remarque : comme vous pouvez le voir, si des instances de logique existent au moment du déchargement de la scène, un nettoyage y a lieu.

Problème et solution avec les objets Unity


Dans la première partie de l'article sur l'éditeur visuel, j'ai mentionné le problème de la maintenance des liens vers les objets de scène, qui est lié à l'impossibilité de restaurer les données sérialisées. Cela est dû au fait que l'identifiant unique des objets de scène est différent à chaque lancement de la scène. La solution à ce problème est la seule option - il s'agit de transférer le stockage des liens dans la scène. À cet effet, un référentiel spécialisé et une classe wrapper ont été ajoutés au contrôleur logique à travers lequel les composants logiques reçoivent des références aux objets Unity , y compris les ressources, les préfabriqués et d'autres actifs.

Code de stockage
 [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; } 

Ici:

  1. Id - identifiant unique de l'actif ou de l'objet de scène
  2. Obj - lien vers des objets d'actif ou de scène


Comme vous pouvez le voir dans le code, rien de super compliqué.

Considérons maintenant une classe wrapper pour les objets 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; } } 

Remarque : l'indicateur _objNotFound est nécessaire afin de ne pas rechercher le référentiel à chaque fois s'il n'y a pas d'objet dedans.

Performances


Maintenant, je pense qu'il vaut la peine de s'attarder sur une question telle que la productivité. Si vous regardez attentivement l'intégralité du code ci-dessus, vous pouvez comprendre que dans une application déjà en cours d'exécution, le système n'affecte pas la vitesse, c'est-à-dire tout fonctionnera comme l' application Unity habituelle . J'ai comparé la mise à jour du MonoBehaviour habituel et via uViLEd et je n'ai pas pu obtenir au moins la différence qui pourrait être indiquée ici, tous les nombres sont en parité jusqu'à 1000 appels par trame. Le seul goulot d'étranglement est la vitesse de chargement de la scène, mais même ici, je n'ai pas pu obtenir de chiffres significatifs, bien qu'entre les plates-formes (j'ai vérifié Android et iOS) la différence est grande. Dans la logique de 70 composants et environ 150 connexions (y compris les références aux variables), les chiffres étaient les suivants:

  1. Android (MediaTek 8 cœurs très médiocre)
    - Au démarrage de l'application - ~ 750 ms
    - Exécution de la scène dans l'application en cours d'exécution ~ 250 ms
  2. iOS (iPhone 5s)
    - Au démarrage de l'application - ~ 100 ms
    - Exécution de la scène dans l'application en cours d'exécution ~ 50 ms

Conclusion


L'architecture du noyau du système uViLEd est assez simple et directe, il n'est pas conseillé de l'utiliser indirectement depuis l'éditeur visuel, bien que je n'exclue pas que certaines solutions s'avèrent utiles à quelqu'un. Néanmoins, cet article est important pour une compréhension générale du fonctionnement du système, dont la dernière partie importante est l'éditeur lui-même. Et la dernière partie finale de la série, qui sera la plus mouvementée, y sera consacrée.

PS: la rédaction de l'article a été très longue, ce dont je m'excuse. Initialement, j'avais prévu de sortir toutes les parties restantes en même temps, mais la sortie de nouvelles versions de Unity 3d, ainsi que des chocs vitaux (agréables et pas si), ont changé de plan.

Éditeur de logique visuelle pour Unity3d. Partie 1

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


All Articles