Organisation du système d'événements dans Unity - à travers les yeux d'un game designer

Bonjour à tous!

Je m'excuse à l'avance pour l'amateurisme, mais j'ai lu un article sur la façon dont une personne a essayé de faire face à une connectivité d'entité excessive dans Unity, et j'ai pensé qu'il serait intéressant de parler de mon vélo, que j'ai assemblé pour créer des prototypes de jeu en tant que concepteur de jeux.

Ma tâche était de créer un système d'événements et de messages de différentes entités, en évitant la cohérence même lorsque chaque objet a un grand nombre de liens avec d'autres objets.

En conséquence, mon système me permet de ne pas faire de tels liens du tout. Cela résout le problème principal: il est pratique pour moi de travailler avec, il ne jette pas le code avec des ordures inutiles et, il semble, n'est pas aussi terrible en performances que les appels constants à GetComponent ().

Je serai heureux de toute critique au sujet de la raison pour laquelle cela n'est pas nécessaire et comment le faire tout de même.

Pour commencer, j'ai redéfini la fonctionnalité standard des événements Unity pour passer deux GameObject en paramètres: le sujet et l'objet événement:

[System.Serializable] public class Event : UnityEvent<GameObject, GameObject> {} 

Je stocke les types d'événements dans une classe statique avec toutes sortes de constantes:

 public enum EventTypes { TargetLock, TargetLost, TargetInRange, TargetOutOfRange, Attack, } 

La classe de gestionnaire de ces événements est triviale.
 public class EventManager : MonoBehaviour { Dictionary<EventTypes, Event> events; static EventManager eventManager; public static EventManager Instance { get { if (!eventManager) { eventManager = FindObjectOfType(typeof(EventManager)) as EventManager; if (!eventManager) { print("no event manager"); } else { eventManager.Init(); } } return eventManager; } } void Init() { if (events == null) { events = new Dictionary<EventTypes, Event>(); } } public static void StartListening(EventTypes eventType, UnityAction<GameObject, GameObject> listener) { if (Instance.events.TryGetValue(eventType, out Event thisEvent)) { thisEvent.AddListener(listener); } else { thisEvent = new Event(); thisEvent.AddListener(listener); Instance.events.Add(eventType, thisEvent); } } public static void StopListening(EventTypes eventType, UnityAction<GameObject, GameObject> listener) { if (eventManager == null) return; if (Instance.events.TryGetValue(eventType, out Event thisEvent)) { thisEvent.RemoveListener(listener); } } public static void TriggerEvent(EventTypes eventType, GameObject obj1, GameObject obj2) { if (Instance.events.TryGetValue(eventType, out Event thisEvent)) { thisEvent.Invoke(obj1, obj2); } } } 


J'ai ensuite créé le composant Événements, qui est attaché à chaque objet du jeu.
Dans ce document, je crée des paires gestionnaire d'événements pour tous les types d'événements dans le jeu.

 public class Events : MonoBehaviour { Dictionary<EventTypes, UnityAction<GameObject, GameObject>> eventHandlers; void HandlersInit() { eventHandlers = new Dictionary<EventTypes, UnityAction<GameObject, GameObject>> { { EventTypes.TargetLock, TargetLock }, { EventTypes.TargetLost, TargetLost }, { EventTypes.TargetInRange, TargetInRange }, { EventTypes.TargetOutOfRange, TargetOutOfRange }, { EventTypes.Attack, Attack }, }; } } 

En conséquence, le fichier est encombrant, mais il me convient qu'il en soit un - pour tous les objets à la fois.

J'ajoute des écouteurs on et off pour tous les événements du dictionnaire, donc tous les objets du jeu écoutent tous les événements du jeu, ce qui n'est pas optimal, mais, encore une fois, c'est pratique pour le prototypage lorsque je change le comportement de certaines entités à la volée:

  void OnEnable() { foreach (KeyValuePair<EventTypes, UnityAction<GameObject, GameObject>> pair in eventHandlers) StartListening(pair.Key, pair.Value); } void OnDisable() { foreach (KeyValuePair<EventTypes, UnityAction<GameObject, GameObject>> pair in eventHandlers) StopListening(pair.Key, pair.Value); } 

Maintenant, je dois comprendre à quel objet cette instance d'événements est attachée.

Pour ce faire, je recherche des liens vers des composants de gameObject: par exemple, si notre objet est Personnage, le champ correspondant deviendra! = Null:

  Monster _mob; Character _char; void ComponentsInit() { _mob = GetComponent<Monster>(); _char = GetComponent<Character>(); } 

C'est une opération coûteuse, mais je ne le fais qu'une seule fois dans Awake ().

Il reste maintenant à décrire les gestionnaires pour tous les types d'événements:

  void TargetLock(GameObject g1, GameObject g2) { if (_char) _char.TargetLock(g1, g2); if (_mob) _mob.TargetLock(g1, g2); } 

Le résultat est une grande liste de méthodes, une pour chaque type d'événement, à l'intérieur de chacune desquelles le gestionnaire correspondant est appelé à l'intérieur du composant, selon le type d'événement auquel cette instance est attachée.

En conséquence, à l'intérieur des composants Personnage ou Monstre, j'écris déjà quelque chose comme ça:

  public virtual void TargetLock(GameObject g1, GameObject g2) { if (g1 == gameObject) target = g2; if (g2 == gameObject) TargetedBy(g1); } 

Dans le même temps, je n'ai pas besoin de maintenir de références croisées entre les objets, je garde tous les nouveaux événements et leurs gestionnaires «principaux» en un seul endroit, et les objets finaux reçoivent toutes les informations dont ils ont besoin avec l'événement.

Jusqu'à présent, je n'ai rencontré aucun problème de performance notable: le système fonctionne "imperceptiblement" avec plus de 100 types d'événements et des dizaines d'objets sur l'écran, traitant même des événements sensibles au temps, comme les dommages causés par une collision avec une flèche à un personnage.

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


All Articles