GameEvent — Система событий

Обзор

GameEvent — система событий, которая позволяет компонентам взаимодействовать без прямых ссылок друг на друга.

Поддерживает от 0 до 10 параметров с полной типобезопасностью.


Базовые события

GameEvent (без параметров)

public class PlayerComponent : Component
{
    public GameEvent OnDeath { get; } = new();
    public GameEvent OnRevive { get; } = new();
}

// Подписка
var player = entity.GetComponent<PlayerComponent>();
player.OnDeath += () => Console.WriteLine("Игрок умер");

// Вызов
player.OnDeath.Invoke();

// Оператор подписки
player.OnDeath += () => Console.WriteLine("Еще один обработчик");

GameEvent (один параметр)

public class DamageComponent : Component
{
    public GameEvent<float> OnDamageReceived { get; } = new();
}

// Подписка
var damage = entity.GetComponent<DamageComponent>();
damage.OnDamageReceived += (amount) => 
    Console.WriteLine($"Получен урон: {amount}");

// Вызов
damage.OnDamageReceived.Invoke(25.5f);

GameEvent<T0, T1> (два параметра)

public class CollisionComponent : Component
{
    public GameEvent<Entity, string> OnCollisionEnter { get; } = new();
}

// Подписка
var collision = entity.GetComponent<CollisionComponent>();
collision.OnCollisionEnter += (other, tag) => 
    Console.WriteLine($"Столкновение с {other.Name} ({tag})");

// Вызов
collision.OnCollisionEnter.Invoke(otherEntity, "Wall");

Все типы GameEvent

Доступны GameEvent<T0> до GameEvent<T0...T13> для 1-14 параметров:

// 1 параметр
public class Event1 : Component
{
    public GameEvent<int> OnEvent { get; } = new();
}

// 2 параметра
public class Event2 : Component
{
    public GameEvent<int, string> OnEvent { get; } = new();
}

// 3 параметра
public class Event3 : Component
{
    public GameEvent<int, string, float> OnEvent { get; } = new();
}

// ... до 10 параметров

// 10 параметров
public class Event10 : Component
{
    public GameEvent<int, string, float, bool, int, int, int, int, int, int> OnEvent { get; } = new();
}

Операторы и методы

Использование операторов

var onDamage = new GameEvent<float>();

// Оператор +=
onDamage += (amount) => Console.WriteLine($"Урон: {amount}");

// Оператор -=
onDamage -= (amount) => Console.WriteLine($"Урон: {amount}");

// Invoke()
onDamage.Invoke(50);

Метод Add() и Remove()

var onEvent = new GameEvent<string>();

// Добавить обработчик
onEvent.Add((message) => Console.WriteLine(message));

// Удалить обработчик (если известна функция)
void Handler(string msg) => Console.WriteLine(msg);
onEvent.Remove(Handler);

Цепочка вызовов

var onEvent = new GameEvent<int>();

// Методы Add() и Remove() возвращают этот объект
onEvent
    .Add((val) => Console.WriteLine($"Первый: {val}"))
    .Add((val) => Console.WriteLine($"Второй: {val}"));

onEvent.Invoke(42);

Сложные примеры

Система урона и исцеления

public class HealthComponent : Component
{
    [SerializableField]
    public float MaxHealth { get; set; } = 100;
    
    [SerializableField]
    public float CurrentHealth { get; set; } = 100;
    
    [SerializableField]
    public GameEvent<float> OnHealthChanged { get; } = new();
    
    [SerializableField]
    public GameEvent<float, string> OnDamage { get; } = new();
    
    public void TakeDamage(float amount, string source = "Unknown")
    {
        CurrentHealth = Math.Max(0, CurrentHealth - amount);
        OnDamage.Invoke(amount, source);
        OnHealthChanged.Invoke(CurrentHealth);
    }
    
    public void Heal(float amount)
    {
        CurrentHealth = Math.Min(MaxHealth, CurrentHealth + amount);
        OnHealthChanged.Invoke(CurrentHealth);
    }
}

// Использование
var health = entity.GetComponent<HealthComponent>();

health.OnHealthChanged += (newHealth) => 
{
    Console.WriteLine($"Здоровье: {newHealth}/{health.MaxHealth}");
};

health.OnDamage += (amount, source) => 
{
    Console.WriteLine($"Получен урон {amount} от {source}");
};

health.TakeDamage(25, "Enemy");      // Здоровье: 75/100, Получен урон 25 от Enemy
health.Heal(10);                     // Здоровье: 85/100

Система инвентаря

public class InventoryComponent : Component
{
    [SerializableField]
    public GameEvent<string> OnItemAdded { get; } = new();
    
    [SerializableField]
    public GameEvent<string> OnItemRemoved { get; } = new();
    
    [SerializableField]
    public GameEvent<string, int> OnItemQuantityChanged { get; } = new();
    
    private Dictionary<string, int> _items = new();
    
    public void AddItem(string itemName, int quantity = 1)
    {
        if (_items.ContainsKey(itemName))
            _items[itemName] += quantity;
        else
            _items[itemName] = quantity;
        
        OnItemAdded.Invoke(itemName);
        OnItemQuantityChanged.Invoke(itemName, _items[itemName]);
    }
    
    public void RemoveItem(string itemName)
    {
        if (_items.Remove(itemName))
            OnItemRemoved.Invoke(itemName);
    }
}

Система достижений

public class AchievementComponent : Component
{
    [SerializableField]
    public GameEvent<string, string> OnAchievementUnlocked { get; } = new();
    
    [SerializableField]
    public GameEvent<string, int> OnProgressUpdated { get; } = new();
}

public class AchievementSystem : System
{
    private Dictionary<string, int> _progress = new()
    {
        { "Enemies_Killed", 0 },
        { "Distance_Traveled", 0 }
    };
    
    public override void OnUpdate(Entity entity, float deltaTime)
    {
        var achievement = entity.GetComponent<AchievementComponent>();
        if (achievement == null) return;
        
        // Проверка достижений
        foreach (var (key, value) in _progress)
        {
            OnProgressUpdated?.Invoke(key, value);
            
            if (key == "Enemies_Killed" && value >= 100)
                achievement.OnAchievementUnlocked.Invoke("Killer", "Убийте 100 врагов");
            
            if (key == "Distance_Traveled" && value >= 10000)
                achievement.OnAchievementUnlocked.Invoke("Traveler", "Пройдите 10000 м");
        }
    }
}

Обработка ошибок

public class SafeGameEvent
{
    private GameEvent<float> _onEvent = new();
    
    public void Subscribe(Action<float> handler)
    {
        try
        {
            _onEvent.Add(handler);
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Ошибка подписки: {ex.Message}");
        }
    }
    
    public void TriggerSafe(float value)
    {
        try
        {
            _onEvent.Invoke(value);
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Ошибка вызова события: {ex.Message}");
        }
    }
}

Интеграция с системами

public class InputComponent : Component
{
    public GameEvent OnJumpPressed { get; } = new();
    public GameEvent OnAttackPressed { get; } = new();
    public GameEvent<float> OnMovement { get; } = new();
}

public class MovementSystem : System
{
    public override void OnUpdate(Entity entity, float deltaTime)
    {
        var input = entity.GetComponent<InputComponent>();
        var transform = entity.GetComponent<TransformComponent>();
        
        if (input == null || transform == null) return;
        
        // Прыжок при нажатии кнопки
        input.OnJumpPressed += () =>
        {
            var velocity = entity.GetComponent<VelocityComponent>();
            if (velocity != null)
                velocity.VelocityY = 10;
        };
        
        // Движение при вводе
        input.OnMovement += (direction) =>
        {
            transform.X += direction * 100 * deltaTime;
        };
    }
}

Лучшие практики

✅ Правильно

// События в компонентах
public class MyComponent : Component
{
    public GameEvent<int> OnValueChanged { get; } = new();
}

// Слабая связь между компонентами
input.OnAttack += () => weapon.Fire();

// Цепочка событий
health.OnHealthChanged += (h) => 
{
    ui.OnHealthUpdated.Invoke(h);
};

❌ Неправильно

// Не вызывайте события в других компонентах
component1.myEvent.Invoke();  // ❌ Событие должно быть приватным для другого класса

// Не игнорируйте исключения в обработчиках
onEvent += (value) => throw new Exception("Ошибка!");  // ❌ Может сломать систему

// Не создавайте циклические зависимости
eventA.Add(() => eventB.Invoke());  // ❌ Может привести к бесконечной петле
eventB.Add(() => eventA.Invoke());

Производительность


© 2026 Igdrasil Project. Все права защищены.