Как должен выглядеть правильный Event Manager?
Почитала статьи, но везде все как то странно и местами не очень понятно. Какая у Event Manager ов общая структура, как их правильно делать? И еще маленький вопросик - получается, что проверка выполнения условий событий происходит в Update?
Ответы (1 шт):
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);
}
}
}
В итоге не только ивент менеджер, но и сами ивенты не нужны, все решается интерфейсами и взаимодействием объектов напрямую.