Реализовать систему событий (Event System)
Условие
Реализуйте простую систему событий для отделения игровых систем друг от друга.
Требования
- Создайте класс EventManager с паттерном Singleton
- Реализуйте методы:
- Subscribe<T>(Action<T> handler) - подписка на событие
- Unsubscribe<T>(Action<T> handler) - отписка от события
- Publish<T>(T eventData) - публикация события
- События должны быть типизированными
- Не забудьте об отписке при уничтожении объектов
Пример использования
// Определение события
public class PlayerDiedEvent
{
public Vector3 DeathPosition;
public string CauseOfDeath;
}
// Подписка
void OnEnable()
{
EventManager.Instance.Subscribe<PlayerDiedEvent>(OnPlayerDied);
}
void OnDisable()
{
EventManager.Instance.Unsubscribe<PlayerDiedEvent>(OnPlayerDied);
}
void OnPlayerDied(PlayerDiedEvent e)
{
Debug.Log($"Player died at {e.DeathPosition} from {e.CauseOfDeath}");
}
// Публикация
EventManager.Instance.Publish(new PlayerDiedEvent
{
DeathPosition = transform.position,
CauseOfDeath = "Fall damage"
});
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Решение
Зачем нужна система событий?
Система событий (Event System) — это паттерн для слабой связанности между компонентами игры. Вместо того чтобы один объект напрямую вызывал методы другого (что создаёт жёсткие зависимости), они используют события как посредников. Например, вместо прямого вызова UIManager.UpdateScore() из PlayerScript, Player опубликует событие ScoreChangedEvent, а UI подпишется на него. Это делает код более модульным, тестируемым и расширяемым.
Реализация EventManager
using UnityEngine;
using System;
using System.Collections.Generic;
/// <summary>Глобальный менеджер для публикации и подписки на события</summary>
public class EventManager : Singleton<EventManager>
{
// Dictionary для хранения подписчиков на каждый тип события
private Dictionary<Type, Delegate> eventDictionary = new Dictionary<Type, Delegate>();
private object lockObject = new object();
/// <summary>Подписать обработчик на событие типа T</summary>
public void Subscribe<T>(Action<T> handler) where T : class
{
if (handler == null)
{
Debug.LogError("Попытка подписать null обработчик");
return;
}
lock (lockObject)
{
Type eventType = typeof(T);
if (eventDictionary.ContainsKey(eventType))
{
// Уже есть подписчики на это событие
eventDictionary[eventType] =
Delegate.Combine(eventDictionary[eventType], handler);
}
else
{
// Первый подписчик на это событие
eventDictionary[eventType] = handler;
}
Debug.Log($"Подписка на событие {eventType.Name}");
}
}
/// <summary>Отписать обработчик от события типа T</summary>
public void Unsubscribe<T>(Action<T> handler) where T : class
{
if (handler == null)
{
Debug.LogError("Попытка отписать null обработчик");
return;
}
lock (lockObject)
{
Type eventType = typeof(T);
if (eventDictionary.ContainsKey(eventType))
{
eventDictionary[eventType] =
Delegate.Remove(eventDictionary[eventType], handler);
// Если нет больше подписчиков, удаляем тип события
if (eventDictionary[eventType] == null)
{
eventDictionary.Remove(eventType);
}
Debug.Log($"Отписка от события {eventType.Name}");
}
else
{
Debug.LogWarning($"Попытка отписать от события {eventType.Name}, которого нет");
}
}
}
/// <summary>Опубликовать событие типа T</summary>
public void Publish<T>(T eventData) where T : class
{
if (eventData == null)
{
Debug.LogError("Попытка опубликовать null событие");
return;
}
lock (lockObject)
{
Type eventType = typeof(T);
if (eventDictionary.ContainsKey(eventType))
{
try
{
var handler = eventDictionary[eventType] as Action<T>;
handler?.Invoke(eventData);
Debug.Log($"Событие {eventType.Name} опубликовано. " +
$"Подписчиков: {handler?.GetInvocationList().Length ?? 0}");
}
catch (Exception e)
{
Debug.LogError($"Ошибка при обработке события {eventType.Name}: {e.Message}");
}
}
}
}
/// <summary>Получить количество подписчиков на событие</summary>
public int GetSubscriberCount<T>() where T : class
{
lock (lockObject)
{
Type eventType = typeof(T);
if (eventDictionary.ContainsKey(eventType))
{
var handler = eventDictionary[eventType] as Action<T>;
return handler?.GetInvocationList().Length ?? 0;
}
return 0;
}
}
/// <summary>Очистить все события (используй осторожно!)</summary>
public void ClearAll()
{
lock (lockObject)
{
eventDictionary.Clear();
Debug.Log("Все события очищены");
}
}
/// <summary>Очистить события определённого типа</summary>
public void Clear<T>() where T : class
{
lock (lockObject)
{
Type eventType = typeof(T);
if (eventDictionary.ContainsKey(eventType))
{
eventDictionary.Remove(eventType);
Debug.Log($"Событие {eventType.Name} очищено");
}
}
}
}
Определение событий
/// <summary>Событие: здоровье игрока изменилось</summary>
public class HealthChangedEvent
{
public int CurrentHealth;
public int MaxHealth;
public int DamageDealt;
public HealthChangedEvent(int current, int max, int damage)
{
CurrentHealth = current;
MaxHealth = max;
DamageDealt = damage;
}
}
/// <summary>Событие: игрок мёртв</summary>
public class PlayerDiedEvent
{
public Vector3 DeathPosition;
public string CauseOfDeath;
public float TimeAlive;
}
/// <summary>Событие: враг спауна</summary>
public class EnemySpawnedEvent
{
public Transform EnemyTransform;
public int EnemyId;
public string EnemyType;
}
/// <summary>Событие: игрок получил предмет</summary>
public class ItemPickedUpEvent
{
public string ItemName;
public int Quantity;
public Vector3 PickupPosition;
}
Примеры использования
Пример 1: UI обновляется при изменении здоровья
public class HealthBar : MonoBehaviour
{
[SerializeField] private Image healthBarImage;
private void OnEnable()
{
// Подписываемся на событие при включении
EventManager.Instance.Subscribe<HealthChangedEvent>(OnHealthChanged);
}
private void OnDisable()
{
// Отписываемся при отключении (ВАЖНО!)
EventManager.Instance.Unsubscribe<HealthChangedEvent>(OnHealthChanged);
}
private void OnHealthChanged(HealthChangedEvent evt)
{
float healthPercent = (float)evt.CurrentHealth / evt.MaxHealth;
healthBarImage.fillAmount = healthPercent;
Debug.Log($"Здоровье: {evt.CurrentHealth}/{evt.MaxHealth}");
}
}
// Использование: игрок публикует событие при получении урона
public class Player : MonoBehaviour
{
private int currentHealth = 100;
private int maxHealth = 100;
public void TakeDamage(int damage)
{
currentHealth -= damage;
// Публикуем событие
EventManager.Instance.Publish(new HealthChangedEvent(
currentHealth,
maxHealth,
damage
));
if (currentHealth <= 0)
{
Die();
}
}
private void Die()
{
EventManager.Instance.Publish(new PlayerDiedEvent
{
DeathPosition = transform.position,
CauseOfDeath = "Enemy attack",
TimeAlive = Time.timeSinceLevelLoad
});
Destroy(gameObject);
}
}
Пример 2: Несколько систем слушают одно событие
// Система звуков реагирует на смерть
public class SoundManager : MonoBehaviour
{
[SerializeField] private AudioClip deathSound;
private AudioSource audioSource;
private void OnEnable()
{
EventManager.Instance.Subscribe<PlayerDiedEvent>(OnPlayerDied);
}
private void OnDisable()
{
EventManager.Instance.Unsubscribe<PlayerDiedEvent>(OnPlayerDied);
}
private void OnPlayerDied(PlayerDiedEvent evt)
{
audioSource.PlayOneShot(deathSound);
}
}
// Система частиц также реагирует на смерть
public class ParticleManager : MonoBehaviour
{
[SerializeField] private GameObject deathParticlePrefab;
private void OnEnable()
{
EventManager.Instance.Subscribe<PlayerDiedEvent>(OnPlayerDied);
}
private void OnDisable()
{
EventManager.Instance.Unsubscribe<PlayerDiedEvent>(OnPlayerDied);
}
private void OnPlayerDied(PlayerDiedEvent evt)
{
Instantiate(deathParticlePrefab, evt.DeathPosition, Quaternion.identity);
}
}
Пример 3: Система сохранения следит за подсчётом очков
public class SaveManager : MonoBehaviour
{
private int totalScore = 0;
private void OnEnable()
{
EventManager.Instance.Subscribe<HealthChangedEvent>(OnHealthChanged);
}
private void OnDisable()
{
EventManager.Instance.Unsubscribe<HealthChangedEvent>(OnHealthChanged);
}
private void OnHealthChanged(HealthChangedEvent evt)
{
// Например, добавляем очки за каждый полученный урон
totalScore += evt.DamageDealt;
SaveProgress();
}
private void SaveProgress()
{
PlayerPrefs.SetInt("TotalScore", totalScore);
PlayerPrefs.Save();
}
}
Ключевые особенности
1. Потокобезопасность — используется lock для защиты от race conditions.
2. Типизация — generic типы <T> позволяют компилятору проверить типы событий.
3. Автоматическая очистка — при удалении всех подписчиков тип события удаляется из словаря.
4. Обработка исключений — если обработчик выбросит исключение, оно логируется, но другие обработчики продолжат работать.
5. Отладка — логирование помогает отследить, кто подписывается и публикует события.
Важные правила
✓ ВСЕГДА отписывайся в OnDisable() — иначе произойдёт утечка памяти. ✓ Используй одноименные методы — OnEnable подписка, OnDisable отписка. ✓ Называй события в прошедшем времени — PlayerDiedEvent, not PlayerDying. ✓ Передавай контекст в событиях — включай необходимые данные в класс события.
Эта реализация обеспечивает слабую связанность, высокую модульность и легкую расширяемость игровой архитектуры.