Как должен выглядеть правильный Event Manager?

Почитала статьи, но везде все как то странно и местами не очень понятно. Какая у Event Manager ов общая структура, как их правильно делать? И еще маленький вопросик - получается, что проверка выполнения условий событий происходит в Update?


Ответы (1 шт):

Автор решения: KOTlK

Event manager - это какая-то глобальная сущность в виде гномика, которая обрабатывает абсолютно все события в игре. Такой сущности существовать не должно. Как должны происходить подписки на событие через этот менеджер?

EventManager.AddListener<SomeEvent>(SomeDelegate); //Первый пример по запросу Event manager

Так?

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

Есть компонент Character, который зависит от другого Character:

using System;
using TMPro;
using UnityEngine;

namespace Events
{
    public class Character : MonoBehaviour, IDamageable
    {
        [SerializeField] private TMP_Text _healthView;
        [SerializeField] private float _maxHp = 100f;
        [SerializeField] private float _damage = 10f;
        
        //Зависимость передается через поле
        [SerializeField] private Character _target;
        
        private IHealth _health;

        public event Action Died;

        private void Awake()
        {
            _health = new Health(_maxHp);
            _health.AmountChanged += UpdateView;
            _target.Died += RemoveTarget;
        }

        private void OnDestroy()
        {
            _health.AmountChanged -= UpdateView;
            _target.Died -= RemoveTarget;
        }

        private void Update()
        {
            if (Input.GetKeyDown(KeyCode.Space))
            {
                _target.ApplyDamage(_damage);
            }
        }

        public void ApplyDamage(float amount)
        {
            if (_health.IsOver) throw new Exception("Can not apply damage to dead character");

            _health.Lose(amount);
            if (_health.IsOver) Died?.Invoke();
        }

        private void UpdateView(float health)
        {
            _healthView.text = health.ToString();
        }

        private void RemoveTarget()
        {
            _target.Died -= RemoveTarget;
            _target = null;
        }
    }
}

Здесь зависимость предается через поле, перетаскиванием таргета в инспекторе. При нажатии на пробел, таргету наносится дамаг, как только он умирает, поле таргет обнуляется. Если передавать зависимость через метод, то это выглядит так:

private bool _initialized = false;
public void Initialize(Character target)
{
    if (_initialized) throw new Exception($"{nameof(Character)} is already initialized");
            
    _target = target;
    _initialized = true;
}

То есть, сразу посмотрев на код, понятно от чего этот Character зависит, в отличии от ивент менеджера, используя который ты видишь, что твой персонаж зависит от какого-то ивента, который непонятно где вызывается. Как я и писал в комментарии, всякие менеджеры и контроллеры нужны только тем, кто не умеет передавать зависимости, чтобы у них был какой-то глобальный объект, желательно синглтон, который можно использовать из любого места в коде. Без ивентов, кстати, тоже можно спокойно обходиться. Вот как выглядит Health из примера выше с использованием ивентов:

using System;

namespace Events
{
    public interface IHealth
    {
        event Action<float> AmountChanged;
        bool IsOver { get; }
        void Lose(float amount);
        void Restore(float amount);
    }
}
using System;
using UnityEngine;

namespace Events
{
    public class Health : IHealth
    {
        public event Action<float> AmountChanged;
        
        private readonly float _max;
        private readonly float _min;
        private float _current;

        public Health(float max, float min = 0)
        {
            _max = max;
            _min = min;
            _current = _max;
        }

        public bool IsOver => _current <= _min;
        
        public void Lose(float amount)
        {
            if (amount <= 0) throw new ArgumentException();
            
            SetCurrent(_current - amount);
        }

        public void Restore(float amount)
        {
            if (amount <= 0) throw new ArgumentException();

            SetCurrent(_current + amount);
        }

        private void SetCurrent(float amount)
        {
            _current = Mathf.Clamp(amount, _min, _max);
            AmountChanged?.Invoke(_current);
        }
    }
}

Все эти ивенты можно заменить интерфейсом:

namespace Events
{
    public interface IVisualization<in TView>
    {
        void Visualize(TView view);
    }
}

Соответственно вьюшка хп:

namespace Events
{
    public interface IHealthView
    {
        void DisplayHp(float amount);
    }
}
using TMPro;
using UnityEngine;

namespace Events
{
    public class HealthView : MonoBehaviour, IHealthView
    {
        [SerializeField] private TMP_Text _text;
        
        public void DisplayHp(float amount)
        {
            _text.text = amount.ToString();
        }
    }
}

И сам Health:

namespace Events
{
    public interface IHealth : IVisualization<IHealthView>
    {
        bool IsOver { get; }
        void Lose(float amount);
        void Restore(float amount);
    }
}
using System;
using UnityEngine;

namespace Events
{
    public class Health : IHealth
    {
        private readonly float _max;
        private readonly float _min;
        private float _current;

        public Health(float max, float min = 0)
        {
            _max = max;
            _min = min;
            _current = _max;
        }

        public bool IsOver => _current <= _min;
        
        public void Lose(float amount)
        {
            if (amount <= 0) throw new ArgumentException();
            
            SetCurrent(_current - amount);
        }

        public void Restore(float amount)
        {
            if (amount <= 0) throw new ArgumentException();

            SetCurrent(_current + amount);
        }

        public void Visualize(IHealthView view)
        {
            view.DisplayHp(_current);
        }

        private void SetCurrent(float amount)
        {
            _current = Mathf.Clamp(amount, _min, _max);
        }
    }
}

Ну и Character:

using System;
using UnityEngine;

namespace Events
{
    public class Character : MonoBehaviour, IDamageable
    {
        [SerializeField] private HealthView _healthView;
        [SerializeField] private float _maxHp = 100f;
        [SerializeField] private float _damage = 10f;
        [SerializeField] private Character _target;
        
        private IHealth _health;

        public bool IsDead => _health.IsOver;

        private void Awake()
        {
            _health = new Health(_maxHp);
        }

        private void Update()
        {
            if (Input.GetKeyDown(KeyCode.Space))
            {
                _target.ApplyDamage(_damage);
                if (_target.IsDead) RemoveTarget();
            }
        }

        public void ApplyDamage(float amount)
        {
            if (_health.IsOver) throw new Exception("Can not apply damage to dead character");

            LoseHp(amount);
        }

        private void RemoveTarget()
        {
            _target = null;
        }

        private void LoseHp(float amount)
        {
            _health.Lose(amount);
            _health.Visualize(_healthView);
        }
    }
}


В итоге не только ивент менеджер, но и сами ивенты не нужны, все решается интерфейсами и взаимодействием объектов напрямую.

→ Ссылка