Pull to refresh

Организация системы эвентов в Unity — глазами геймдизайнера

Reading time4 min
Views4.7K
Всем привет!

Заранее извиняюсь за дилетантизм, но я почитал статью о том, как человек пытался бороться с чрезмерной связностью сущностей в Unity, и подумал, что было бы интересно рассказать о своём велосипеде, который я сколотил для создания игровых прототипов как геймдизайнер.

Передо мной стояла задача создать систему эвентов и сообщения различных сущностей, избежав той самой связанности, когда у каждого объекта присутствует большое количество ссылок на другие объекты.

В итоге моя система позволяет не делать таких ссылок вовсе. Она решает главную задачу: мне с ней удобно работать, она не захламляет код лишним мусором и, вроде бы, не так ужасна по производительности как постоянные вызовы GetComponent().

Буду рад любой критике на тему, почему так делать не надо, и как делать всё-таки надо.

Для начала я переопределил стандартный функционал эвентов Unity для передачи двух GameObject в качестве параметров: субъекта и объекта эвента:

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

Типы эвентов я храню в статическом классе со всякого рода константами:

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

Класс-обработчик этих эвентов тривиален
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);
        }
    }
}


Затем я создал компонент Events, который прикрепляется к каждому объекту в игре.
В нём я создаю пары «Эвент — хэндлер» для всех типов эвентов в игре.


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

В итоге файл получается громоздким, но для меня удобно, что он один — для всех объектов разом.

Я добавляю в него включение и выключение листенеров для всех эвентов в словаре, таким образом все объекты игры слушают все эвенты игры, что не оптимально, но, опять же, удобно для прототипирования, когда я на лету меняю поведение тех или иных сущностей:

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

Теперь мне нужно понять, к какому объекту прикреплён этот инстанс Events.

Для этого я ищу у gameObject ссылки на компоненты: например, если наш объект — Character, соответствующее поле станет != null:

    Monster _mob;
    Character _char;

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

Это дорогая операция, но я делаю её только один раз в Awake().

Теперь осталось описать хэндлеры для всех типов эвентов:

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

В итоге получается большой список методов, по одному для каждого типа эвента, внутри каждого из которых происходит вызов соответствующего хэндлера уже внутри компонента — в зависимости от того, к объекту какого типа прикреплён данный инстанс Events.

Соответственно, внутри компонентов Character или Monster я уже пишу что-то в этом роде:

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

При этом мне не нужно поддерживать никаких перекрёстных ссылок между объектами, все новые эвенты и их «первичные» хэндлеры я держу в одном месте, а конечные объекты получают всю необходимую им информацию вместе с эвентом.

Пока что я не столкнулся с заметными проблемами с производительностью: система «незаметно» работает с 100+ типами эвентов и десятками объектов на экране, обрабатывая даже чувствительные ко времени события вроде получения урона персонажем от коллизии со стрелой.
Tags:
Hubs:
Total votes 11: ↑8 and ↓3+5
Comments14

Articles