События (Events) и Действия (Actions) — это механизм в C#, который позволяет объектам общаться друг с другом, не зная о существовании друг друга. Это называется слабой связностью (loose coupling).
- Аналогия из жизни
- Базовый синтаксис (C# Action)
- Объявление события
- Подписка (Subscribe)
- Отписка (Unsubscribe) — ⚠️ КРИТИЧНО ВАЖНО
- Вызов события (Invoke)
- Практический пример для стратегии (SLG)
- Шаг 1: Издатель (ResourceManager)
- Шаг 2: Подписчик 1 (UI Manager)
- Шаг 3: Подписчик 2 (Achievement System)
- UnityEvent vs C# Action
- Главные правила и ловушки
- Ловушка 1: Утечка памяти (Memory Leak)
- Ловушка 2: Статические события
- Ловушка 3: Порядок выполнения
- Ловушка 4: Отписка во время вызова
- Продвинутый уровень: Event Bus (Шина событий)
- Когда НЕ стоит использовать события?
- Чек-лист для внедрения в игру
Аналогия из жизни
Представьте рассылку новостей:
- Издатель (Publisher): Журнал. Он просто рассылает выпуск. Ему всё равно, кто его читает.
- Подписчик (Subscriber): Вы. Вы подписываетесь на журнал. Когда выходит выпуск, вы его получаете.
- Связь: Журнал не хранит ваш номер телефона в коде, у него есть просто список подписчиков.
В игре:
- Издатель: Менеджер ресурсов (золото изменилось).
- Подписчики: UI (обновить цифру), Достижения (проверить условие), Аналитика (записать событие), Звук (проиграть монетку).
Базовый синтаксис (C# Action)
В Unity чаще всего используют делегат Action (не возвращает значение) или Func (возвращает значение).
Объявление события
// Статическое событие (доступно отовсюду)
public static Action<int> OnGoldChanged;
// Или событие экземпляра (безопаснее)
public event Action<int> OnGoldChanged;Подписка (Subscribe)
// Знак += означает "подписаться"
ResourceManager.OnGoldChanged += UpdateUI;Отписка (Unsubscribe) — ⚠️ КРИТИЧНО ВАЖНО
// Знак -= означает "отписаться"
// Обязательно делайте это при уничтожении объекта, иначе будет утечка памяти!
ResourceManager.OnGoldChanged -= UpdateUI;Вызов события (Invoke)
// Знак ?. нужен, чтобы проверить, есть ли подписчики (чтобы не было ошибки null)
OnGoldChanged?.Invoke(newGoldAmount);Практический пример для стратегии (SLG)
Допустим, у вас есть Менеджер Ресурсов. Когда меняется золото, нужно:
- Обновить текст в UI.
- Проверить достижение «Богач».
- Отправить статистику.
Шаг 1: Издатель (ResourceManager)
using System;
using UnityEngine;
public class ResourceManager : MonoBehaviour
{
// Событие: передает новое количество золота
public static Action<int> OnGoldChanged;
private int _gold = 0;
public void AddGold(int amount)
{
_gold += amount;
// Сообщаем всем подписчикам: "Золото изменилось!"
OnGoldChanged?.Invoke(_gold);
}
public void SpendGold(int amount)
{
if (_gold >= amount)
{
_gold -= amount;
OnGoldChanged?.Invoke(_gold);
}
}
}Шаг 2: Подписчик 1 (UI Manager)
using UnityEngine;
using UnityEngine.UI;
public class UIManager : MonoBehaviour
{
[SerializeField] private Text _goldText;
private void OnEnable()
{
// Подписываемся, когда объект активирован
ResourceManager.OnGoldChanged += UpdateGoldText;
}
private void OnDisable()
{
// Отписываемся, когда объект деактивирован (важно для памяти!)
ResourceManager.OnGoldChanged -= UpdateGoldText;
}
private void UpdateGoldText(int newGold)
{
_goldText.text = newGold.ToString();
}
}Шаг 3: Подписчик 2 (Achievement System)
public class AchievementSystem : MonoBehaviour
{
private void OnEnable()
{
ResourceManager.OnGoldChanged += CheckRichAchievement;
}
private void OnDisable()
{
ResourceManager.OnGoldChanged -= CheckRichAchievement;
}
private void CheckRichAchievement(int gold)
{
if (gold >= 10000)
{
Debug.Log("Достижение разблокировано: Золотой магнат!");
// UnlockAchievement("rich_man");
}
}
}Результат:
ResourceManagerничего не знает про UI или Достижения. Вы можете удалить UI, и игра не сломается (просто не обновится цифра).
UnityEvent vs C# Action
В Unity есть два типа событий. Важно понимать разницу.
|
Характеристика
|
C# Action / Event
|
UnityEvent
|
|---|---|---|
|
Где настраивается
|
В коде
|
В Inspector (можно перетащить объект)
|
|
Производительность
|
⚡ Высокая (быстрее)
|
🐢 Ниже (использует рефлексию)
|
|
Гибкость
|
Можно передавать любые данные
|
Ограниченный набор типов
|
|
Для кого
|
Программисты
|
Дизайнеры уровней / новички
|
|
Пример
|
public static Action<int> OnHit; |
public UnityEvent<int> OnHit; |
Рекомендация: Для логики игры (экономика, бой) используйте C# Action. Для интерфейсов (кнопки, простые клики) можно использовать UnityEvent, чтобы дизайнеры могли настраивать реакции без кода.
Главные правила и ловушки
Ловушка 1: Утечка памяти (Memory Leak)
Если вы подписались на статическое событие, но не отписались при уничтожении объекта, объект не удалится из памяти (Garbage Collector не сможет его собрать).
- Правило: Всегда отписывайтесь в OnDisable() или OnDestroy().
- Правило: Подписывайтесь в OnEnable() или Start().
Ловушка 2: Статические события
Статические события (public static Action) живут всю жизнь приложения.
- Риск: Если подписчик удалится, а событие останется, при вызове события произойдет ошибка (если не использовать ?.).
- Совет: Для больших проектов лучше использовать Event Bus (патрон событий), а не кучу статических событий в разных классах.
Ловушка 3: Порядок выполнения
Неизвестно, какой подписчик получит событие первым.
- Риск: Если логика зависит от порядка (сначала UI, потом Звук), события могут не подойти.
- Решение: В таких случаях лучше прямой вызов или очередь событий.
Ловушка 4: Отписка во время вызова
Нельзя отписываться от события внутри метода, который это событие вызвал (ошибка Collection was modified).
- Решение: Используйте флаг isDestroyed или отписывайтесь в следующем кадре.
Продвинутый уровень: Event Bus (Шина событий)
Для большой стратегии куча статических событий превратится в кашу. Лучше создать единый центр событий.
public static class GameEvents
{
// Экономика
public static Action<int> OnGoldChanged;
public static Action<int> OnFoodChanged;
// Строительство
public static Action<string> OnBuildingCompleted;
// Бой
public static Action<Vector3> OnEnemySpotted;
}Подписка: GameEvents.OnGoldChanged += …
Это упорядочивает код и делает события видимыми в одном месте.
Когда НЕ стоит использовать события?
- Если связь однозначная: Если у Пули есть ссылка на Врага, и она должна нанести урон именно ему — используйте прямой вызов enemy.TakeDamage(). События тут избыточны.
- Если нужен ответ: События (Action) не возвращают значение. Если вам нужно спросить «Можно ли построить?» и получить «Да/Нет», используйте прямой запрос или Func.
- Если это сбивает с толку: Если новичок в команде не понимает, откуда вызывается функция — лучше использовать прямой код. Отладка событий сложнее (нельзя поставить breakpoint на вызове, если не знаешь подписчика).
Чек-лист для внедрения в игру
- Выделите глобальные события: (Ресурсы, Квесты, Уровень игрока). Создайте класс
GameEvents. - UI подписывается на данные: UI никогда не меняет данные сам, он только слушает события изменений.
- Следите за OnDestroy: Пройдитесь по всем скриптам с подпиской и убедитесь, что есть отписка.
- Используйте параметры: Передавайте в событие нужные данные (
int amount,BuildingType type), чтобы подписчик не лез в базу данных снова.
События — это мощный инструмент для архитектуры. В экономической стратегии они помогут вам легко добавлять новые фичи (например, новое достижение), не переписывая старый код менеджера ресурсов.





