
Organization of a system of events in Unity - through the eyes of a game designer
Hello!
I apologize in advance for amateurism, but I read an article about how a person tried to deal with excessive entity connectivity in Unity, and thought it would be interesting to talk about my bike, which I put together to create game prototypes as a game designer.
My task was to create a system of events and messages of various entities, avoiding the very coherence when each object has a large number of links to other objects.
As a result, my system allows me not to make such links at all. It solves the main problem: it’s 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 the constant calls to GetComponent ().
I will be glad to any criticism on the topic of why this is not necessary, and how to do it all the same.
To begin with, I redefined the standard functionality of Unity events to pass two GameObject as parameters: the subject and the event object:
I store event types in a static class with all kinds of constants:
Then I created the Events component, which is attached to every object in the game.
In it, I create Event-Handler pairs for all types of events in the game.
As a result, the file is cumbersome, but it’s convenient for me that it is one - for all objects at once.
I add turning on and turning off listeners for all events in the dictionary, so all game objects listen to all game events, which is not optimal, but, again, it’s convenient for prototyping when I change the behavior of certain entities on the fly:
Now I need to understand which object this Events instance is attached to.
To do this, I look for links to components from gameObject: for example, if our object is Character, the corresponding field will become! = Null:
This is an expensive operation, but I only do it once in Awake ().
Now it remains to describe the handlers for all types of events:
The result is a large list of methods, one for each type of event, inside each of which the corresponding handler is called inside the component, depending on what type of event this instance is attached to.
Accordingly, inside the Character or Monster components, I am already writing something like that:
At the same time, I don’t need to maintain any cross-references between objects, I keep all the new events and their “primary” handlers in one place, and the final objects receive all the information they need along with the event.
So far, I have not encountered any noticeable performance problems: the system "imperceptibly" works with 100+ types of events and dozens of objects on the screen, processing even time-sensitive events like taking damage from a collision with an arrow to a character.
I apologize in advance for amateurism, but I read an article about how a person tried to deal with excessive entity connectivity in Unity, and thought it would be interesting to talk about my bike, which I put together to create game prototypes as a game designer.
My task was to create a system of events and messages of various entities, avoiding the very coherence when each object has a large number of links to other objects.
As a result, my system allows me not to make such links at all. It solves the main problem: it’s 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 the constant calls to GetComponent ().
I will be glad to any criticism on the topic of why this is not necessary, and how to do it all the same.
To begin with, I redefined the standard functionality of Unity events to pass two GameObject as parameters: the subject and the event object:
[System.Serializable]
public class Event : UnityEvent {}
I store event types in a static class with all kinds of constants:
public enum EventTypes
{
TargetLock,
TargetLost,
TargetInRange,
TargetOutOfRange,
Attack,
}
The handler class of these events is trivial.
public class EventManager : MonoBehaviour
{
Dictionary 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();
}
}
public static void StartListening(EventTypes eventType, UnityAction 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 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 Event-Handler pairs for all types of events in the game.
public class Events : MonoBehaviour
{
Dictionary> eventHandlers;
void HandlersInit()
{
eventHandlers = new Dictionary>
{
{ EventTypes.TargetLock, TargetLock },
{ EventTypes.TargetLost, TargetLost },
{ EventTypes.TargetInRange, TargetInRange },
{ EventTypes.TargetOutOfRange, TargetOutOfRange },
{ EventTypes.Attack, Attack },
};
}
}
As a result, the file is cumbersome, but it’s convenient for me that it is one - for all objects at once.
I add turning on and turning off listeners for all events in the dictionary, so all game objects listen to all game events, which is not optimal, but, again, it’s convenient for prototyping when I change the behavior of certain entities on the fly:
void OnEnable()
{
foreach (KeyValuePair> pair in eventHandlers)
StartListening(pair.Key, pair.Value);
}
void OnDisable()
{
foreach (KeyValuePair> pair in eventHandlers)
StopListening(pair.Key, pair.Value);
}
Now I need to understand which object this Events instance is attached to.
To do this, I look for links to components from gameObject: for example, if our object is Character, the corresponding field will become! = Null:
Monster _mob;
Character _char;
void ComponentsInit()
{
_mob = GetComponent();
_char = GetComponent();
}
This is an expensive operation, but I only do it once in Awake ().
Now it 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, inside each of which the corresponding handler is called inside the component, depending on what type of event this instance is attached to.
Accordingly, inside the Character or Monster components, I am already writing something like that:
public virtual void TargetLock(GameObject g1, GameObject g2)
{
if (g1 == gameObject)
target = g2;
if (g2 == gameObject)
TargetedBy(g1);
}
At the same time, I don’t need to maintain any cross-references between objects, I keep all the new events and their “primary” handlers in one place, and the final objects receive all the information they need along with the event.
So far, I have not encountered any noticeable performance problems: the system "imperceptibly" works with 100+ types of events and dozens of objects on the screen, processing even time-sensitive events like taking damage from a collision with an arrow to a character.