📜 ⬆️ ⬇️

Organization of the event system in Unity - through the eyes of game designer

Hello!

I apologize in advance for dilettantism, but I read an article about how a person tried to cope with excessive cohesion of entities in Unity, and thought it would be interesting to tell you about my bike, which I cracked to create game prototypes as a game designer.

I was faced with the task of creating a system of events and messages of various entities, avoiding the very connectedness, when each object has a large number of references to other objects.
')
As a result, my system allows not to make such links at all. It solves the main task: it is convenient for me to work with it, it does not litter the code with unnecessary garbage and, it seems, is not as terrible in performance as constant calls to GetComponent ().

I will be glad to any criticism on the topic, why it is not necessary to do this, and how to do it all the same.

To begin with, I redefined the standard Unity event functionality to pass two GameObjects as parameters: the subject and the event object:

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

Event types I store in a static class with all sorts of constants:

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

The handler class of these events is trivial.
 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); } } } 


Then I created the Events component, which is attached to every object in the game.
In it, I create a pair of "Event - Handler" for all types of events in the game.

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

As a result, the file is bulky, but it's convenient for me that it is one - for all objects at once.

I add to it the inclusion and deactivation of listeners for all events in the dictionary, so all game objects listen to all events of the game, which is not optimal, but, again, convenient for prototyping, when I change the behavior of certain entities on the fly:

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

Now I need to understand to which object this Events instance is attached.

For this, I’m looking for component references for gameObject: for example, if our object is Character, the corresponding field will be! = Null:

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

This is an expensive operation, but I only do it once in Awake ().

It now remains to describe the handlers for all types of events:

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

The result is a large list of methods, one for each type of event, within each of which the corresponding handler is invoked already inside the component, depending on what type of object this Events instance is attached to.

Accordingly, inside the Character or Monster components, I am already writing something like this:

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

At the same time, I do not need to maintain any cross-references between objects, I keep all new events and their “primary” handlers in one place, and the end objects receive all the information they need along with the event.

So far, I have not encountered noticeable performance issues: the system “imperceptibly” works with 100+ event types and dozens of objects on the screen, handling even time sensitive events like receiving a character's damage from a boom collision.

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


All Articles