← Назад к вопросам

Как реализуешь взаимодействие внутри при построении архитектуры?

2.3 Middle🔥 111 комментариев
#Паттерны проектирования

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI6 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Подход к реализации взаимодействий в архитектуре Unity-проекта

Взаимодействие внутри приложения — это краеугольный камень игровой архитектуры. Я реализую его через комбинацию паттернов проектирования, систем событий и принципов слабой связанности. Ключевая цель — создать гибкую, тестируемую и масштабируемую систему, где компоненты общаются, не зная деталей реализации друг друга.

Основные стратегии и паттерны

1. Система событий (Event Bus / Message Bus)

Это основа для глобального или контекстного взаимодействия. Я избегаю прямых ссылок GetComponent<>() или синглтонов для коммуникации, где это возможно.

// Пример декларации простой системы событий
public static class GameEvents
{
    public static event Action<Item> OnItemCollected;
    public static event Action<int> OnPlayerHealthChanged;

    public static void TriggerItemCollected(Item item) => OnItemCollected?.Invoke(item);
    public static void TriggerPlayerHealthChanged(int newHealth) => OnPlayerHealthChanged?.Invoke(newHealth);
}

// Использование в потребителе
public class UIManager : MonoBehaviour
{
    private void OnEnable() => GameEvents.OnPlayerHealthChanged += UpdateHealthBar;
    private void OnDisable() => GameEvents.OnPlayerHealthChanged -= UpdateHealthBar;

    private void UpdateHealthBar(int health)
    {
        // Логика обновления UI
    }
}

Для более сложных проектов я использую интерфейс-ориентированную событийную модель или готовые решения вроде UnityEvent для настройки в инспекторе.

2. Паттерн Наблюдатель (Observer) и C# события

Для тесного, но декoupled взаимодействия внутри доменной логики.

public class PlayerHealth : MonoBehaviour
{
    public event Action<int> HealthChanged;
    public event Action Died;

    private int _currentHealth;

    public void TakeDamage(int damage)
    {
        _currentHealth -= damage;
        HealthChanged?.Invoke(_currentHealth);

        if (_currentHealth <= 0)
            Died?.Invoke();
    }
}

3. Dependency Injection (Внедрение зависимостей)

Через конструктор, методы или поля (с атрибутом [Inject]) с использованием фреймворков типа Zenject (VContainer) или через самописный Service Locator.

// Использование Zenject
public class EnemyAI : MonoBehaviour
{
    private IPlayerService _playerService;

    [Inject]
    public void Construct(IPlayerService playerService)
    {
        _playerService = playerService;
    }

    private void Update()
    {
        var playerPos = _playerService.GetPosition();
        // Логика преследования
    }
}

4. Разделение на слои (Layered Architecture)

  • Presentation Layer (View): MonoBehaviour-компоненты, отвечающие за визуал, анимации, звуки. Получают данные через события или интерфейсы.
  • Domain Layer (Model/Logic): Чистые C# классы, содержащие игровую логику (инвентарь, статистика, AI-деревья). Не зависят от Unity Engine.
  • Data Layer: Работа с сохранениями, конфигами, сетевыми запросами.

Взаимодействие между слоями происходит сверху вниз через интерфейсы.

Практическая реализация на примере подбора предмета

  1. Model (ItemData): Чистый C# класс с данными (id, название, бонус).
  2. View (PickupItem): MonoBehaviour на префабе предмета. При коллизии вызывает метод сервиса IInventoryService.AddItem(itemData).
  3. Service (InventoryService): Реализует логику добавления. При успешном добавлении генерирует событие OnInventoryUpdated.
  4. UI (InventoryUI): Подписывается на OnInventoryUpdated и обновляет интерфейс.

Критерии выбора подхода

  • Сфера видимости: Локальное взаимодействие (в пределах префаба) — UnityEvent или прямые ссылки через инспектор. Глобальное — Event Bus.
  • Сложность логики: Для простых игр достаточно C# событий. Для сложных RPG/стратегий с десятками систем — Dependency Injection и четкое разделение слоев.
  • Тестируемость: Всегда предпочитаю интерфейсы и внедрение зависимостей, так как это позволяет легко подменять реализации в юнит-тестах.
  • Производительность: Для высокочастотных взаимодействий (каждый кадр) избегаю делегатов в пользу прямых вызовов или Job System для данных.

Золотые правила

  • Избегайте жестких связей. FindObjectOfType, статические синглтоны-менеджеры и частые GetComponent — главные источники хрупкости кода.
  • Подписчики должны отписываться. Всегда очищайте подписки на события в OnDisable() или OnDestroy().
  • События — для уведомлений, а не для передачи управления. Не стройте сложную логику потока выполнения только на событиях.
  • Инспектор — ваш друг. Используйте [SerializeField] для настройки связей, но для динамических или сложных зависимостей применяйте DI.

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