🔊События

Автор: Дмитрий Прокопьев

У вас когда-нибудь случалось такое, что один компонент влез в зону ответственности другого? Ситуация, когда на такой компонент сваливалась вся работа и из за этого он становился перегруженным? С этим рано или поздно сталкиваются все, но не все знают, как решить эту проблему.

Пример проблемы

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

public class Wallet : MonoBehaviour
{
    public int Money { get; private set; }
    
    public void AddMoney(int amount)
    {
        if (amount <= 0)
            return;
        
        Money += amount;
    }
    
    public bool TrySpendMoney(int amount)
    {
        if (amount <= 0)
            return;
        
        bool isEnough = Money >= amount;
        
        if (isEnough)
            Money -= amount;
        
        return isEnough;
    }
}

У этого кошелька одна простая ответственность: проводить транзакции и обеспечивать их корректность. Но теперь нам прислали новую задачу: отображать количество средств на экране. Не проблема, мы уже реализовали класс WalletView, который умеет отображать количество средств. Осталось лишь вызвать у него метод:

[SerializeField] private WalletView _walletView;
//...
_walletView.Display(Money);

Нам опять пришла новая задача! Теперь каждый раз, когда происходит транзакция, нужно сделать сохранение в файл, чтобы запомнить сколько денег теперь есть у игрока. Не проблема, мы уже реализовали класс FileSaver, который умеет сохранять количество денег. Нужно лишь вызвать у него метод:

[SerializeField] private FileSaver _fileSaver;
//...
_fileSaver.SaveMoney(Money);

Удивительно, опять новая задача! Теперь магазин должен предлагать игроку новые товары, когда тот произвел транзакцию. Не проблема, обратимся к классу Shop:

[SerializeField] private Shop _shop;
//...
_shop.OfferItems(Money);

Видите, куда это идет? Вот так теперь выглядит метод AddMoney у кошелька:

public void AddMoney(int amount)
{
    if (amount <= 0)
        return;
        
    Money += amount;
    
    _walletView.Display(Money);
    _fileSaver.SaveMoney(Money);
    _shop.OfferItems(Money);
}

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

Суть проблемы

Где в этой ситуации корень зла? Когда мы допустили роковую ошибку? Мы сделали это тогда, когда повесили на кошелек задачи других компонентов. Но как этого избежать, если только кошелек знает, что происходит с деньгами?

Например, кошелек может уведомить всех, что происходит, но не указывать компонентам, что именно делать. Тогда ответственность за реакцию компонентов переляжет с кошелька на них самих. Мысленно представим такой диалог:

- Wallet (кричит через громкоговоритель): Количество денег изменилось! Теперь у нас 50 монет. Сами решайте что делать с этим, на этом мои полномочия все.

- WalletView: Мне это важно. Выведу на экран число 50

- FileSaver: Мне это важно. Запишу в файл число 50

- PlayerMover: Меня это не беспокоит, направление движения игрока не зависит от финансового положения. Я ничего не слышал

- Shop: Мне это важно. Обновлю ассортимент товаров: добавлю те, которые можно купить за 50 монет

- Coin: Меня не интересует, сколько денег у игрока. Я ничего не слышал

Заметьте, как кошелек больше не отвечает за остальных! Он сообщил информацию и на этом успокоился. Так мы и можем решить эту проблему.

И какое отношение к этому имеют события?

Событие (англ. Event) - это механизм, позволяющий организовать общение между компонентами описанным выше образом. Название "Событие" появилось, потому что сообщение, которое передает компонент, говорит, что "что-то случилось" или "произошло какое-то событие". То есть событием называется само "сообщение".

Тот компонент, который сообщает информацию через событие, называется владельцем события. Помните, как некоторые компоненты услышали сообщение, а некоторые нет? Одни называются "слушателями" (англ. Listeners) или "подписчиками" события, потому что они получают из него информацию. А другие никак не называются, не хотят - не слушают.

Чтобы стать слушателем события, нужно на него подписаться (англ. Subscribe), а чтобы перестать быть слушателем нужно отписаться (англ. Unsubscribe).

Что с этим делать дальше?

Теперь нам известно, зачем нужны события, и мы знаем основные термины. Осталось только почитать про типы событий и их синтаксис: UnityAction и UnityEvent

Last updated

Was this helpful?