Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Производительность и оптимизация

В этом разделе описаны меры, принятые для обеспечения хорошей производительности проекта CollectorBots, а также рекомендации по дальнейшей оптимизации.

ObjectPool для повторного использования объектов

Одна из самых затратных операций в Unity — инстанцирование и уничтожение игровых объектов (Instantiate/Destroy). Для ресурсов и рабочих, которые часто создаются и удаляются, используется ObjectPool.

Как работает

  • Предзагрузка: при старте игры создаётся N экземпляров объекта (например, 10 ресурсов) и деактивируется.
  • Взятие объекта: когда нужен новый ресурс, пул возвращает один из деактивированных, активирует его и настраивает.
  • Возврат: после использования (например, ресурс собран) объект не уничтожается, а деактивируется и возвращается в пул.

Преимущества

  • Уменьшает нагрузку на сборщик мусора (GC), так как объекты не уничтожаются.
  • Снижает пиковые задержки при внезапном появлении множества объектов.
  • Позволяет контролировать максимальное количество объектов (например, не более 50 ресурсов одновременно).

Реализация

Класс ObjectPool находится в Assets/Scripts/Infrastructure/Factory/ObjectPool.cs. Он поддерживает события (PoolObjectCreatedEvent, PoolObjectGettedEvent, PoolObjectReleasedEvent), что позволяет отслеживать использование пула.

Перемещение рабочих реализовано через NavMeshAgent. Это эффективнее, чем ручное управление движением, потому что:

  • Предрасчитанные пути: навигационная сеть заранее baked, что позволяет быстро находить маршруты.
  • Оптимизация группового движения: Unity автоматически обрабатывает столкновения и перенаправление агентов.
  • Минимальные затраты CPU: расчёт пути происходит в отдельном потоке (если используется NavMeshAgent с autoRepath).

Настройки для производительности

  • Agent Radius: 0.5 (достаточно для избегания столкновений).
  • Obstacle Avoidance: Quality = High, но можно снизить до Medium, если много агентов.
  • Auto Repath: включено, но с разумным интервалом (0.5 сек).

Кэширование в AssetProvider

Загрузка ресурсов через Resources.Load — операция не бесплатная, особенно если вызывать её каждый кадр. AssetProvider реализует простое кэширование:

private readonly Dictionary<string, UnityEngine.Object> _cache = new();

public T LoadPrefab<T>(string path) where T : UnityEngine.Object
{
    if (_cache.TryGetValue(path, out var cached))
        return (T)cached;

    var asset = Resources.Load<T>(path);
    _cache[path] = asset;
    return asset;
}

Таким образом, каждый префаб или конфиг загружается только один раз за сессию.

Система событий с WeakReference (WeakReferenceComparer)

Система событий IntelligentEventAggregator хранит подписчиков как WeakReference. Это предотвращает утечки памяти, когда подписчик был уничтожен, но забыл отписаться.

Как это работает

  • При подписке создаётся WeakReference на делегат (или объект‑подписчик).
  • При публикации события проверяется, жив ли ещё подписчик (через IsAlive).
  • Если подписчик был собран GC, запись автоматически удаляется из списка подписок.

Класс WeakReferenceComparer обеспечивает корректное сравнение weak‑ссылок в коллекциях.

Оптимизация UI обновлений

UI элементы обновляются не каждый кадр, а только при изменении данных (реактивно). Например, ResourceStoragePresenter подписывается на ResourceStorageChangedEvent и обновляет StorageView только когда количество ресурсов действительно изменилось.

Рекомендации

  • Избегайте Update в UI скриптах: вместо этого используйте события.
  • Объединяйте несколько изменений: если за один кадр может прийти несколько событий, накапливайте изменения и применяйте их раз в кадр (через Canvas.Update или LayoutRebuilder).
  • Отключайте невидимые элементы: когда панель скрыта, можно временно отписаться от событий, чтобы не тратить ресурсы на обработку.

Оптимизация рендеринга

Статический батчинг

Объекты, которые не двигаются (Ground, базы), помечены как Static. Unity объединяет их в один draw call (статический батчинг), что значительно снижает нагрузку на GPU.

Динамический батчинг

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

Уровни детализации (LOD)

Для моделей с высоким полигональным count (база, рабочий) можно добавить LOD‑группы. На расстоянии будет использоваться упрощённая модель, что уменьшит количество отрисовываемых треугольников.

Оптимизация скриптов

Корутины vs Update

Вместо использования Update для периодических действий (сканирование, спавн) применяются корутины с WaitForSeconds. Это эффективнее, потому что корутина «спит» между итерациями и не нагружает CPU.

Пример:

private IEnumerator ScanCoroutine()
{
    while (true)
    {
        ScanForResources();
        yield return new WaitForSeconds(_scanInterval);
    }
}

Кэширование ссылок на компоненты

Избегайте вызовов GetComponent каждый кадр. Кэшируйте ссылки в Awake или Start.

private NavMeshAgent _agent;

private void Awake()
{
    _agent = GetComponent<NavMeshAgent>();
}

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

Для небольших данных, которые часто создаются (например, Rectangle, ResourceSpendRequest), используются структуры (value types). Это уменьшает нагрузку на кучу и сборщик мусора.

Профилирование и метрики

Unity Profiler

Регулярно запускайте Profiler (Window → Analysis → Profiler) для выявления узких мест:

  • CPU: ищите методы с высоким self‑time (особенно Update, FixedUpdate, корутины).
  • GPU: проверьте, не слишком ли много draw calls (цель — меньше 1000 для мобильных, меньше 2000 для PC).
  • Memory: отслеживайте утечки (рост Managed Memory, увеличение числа объектов в куче).

Frame Debugger

Frame Debugger (Window → Analysis → Frame Debugger) показывает, как именно рендерится каждый кадр: какие draw calls, шейдеры, текстуры. Помогает найти лишние перерисовки.

Рекомендации по дальнейшей оптимизации

1. Миграция на Addressable Assets

Сейчас ресурсы загружаются через Resources.Load, что означает, что все ассеты включаются в сборку, даже если они не используются. Addressable Assets позволяют:

  • Загружать ресурсы асинхронно.
  • Выгружать неиспользуемые ассеты.
  • Собирать контент в отдельные бандлы (например, для DLC).

2. Job System и Burst Compiler

Для вычислений, которые можно распараллелить (например, поиск ближайшего ресурса для множества рабочих), можно использовать Unity Job System и Burst Compiler. Это даст значительный прирост производительности на многоядерных процессорах.

3. GPU Instancing

Для отрисовки множества одинаковых объектов (ресурсы) включите GPU Instancing в материалах. Это уменьшит количество draw calls до одного на тип объекта.

4. Оптимизация анимаций

  • Используйте Animation Culling для отключения анимации у объектов вне камеры.
  • Переведите анимации рабочих в Humanoid тип, если возможно, для применения оптимизаций Unity.
  • Примените Level of Detail для анимаций (более простые анимации на расстоянии).

5. Сокращение использования GC

  • Избегайте аллокаций в Update (например, new List<>, new Vector3).
  • Используйте пулы не только для GameObjects, но и для коллекций (например, ListPool из UnityEngine.Pool).
  • Замените LINQ на обычные циклы там, где это критично (LINQ создаёт итераторы и лямбды, что приводит к аллокациям).

6. Асинхронная загрузка сцен

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

Заключение

Текущая архитектура проекта уже включает несколько важных оптимизаций (пулы, кэширование, weak‑ссылки, NavMesh). Однако всегда есть куда расти. Регулярное профилирование и следование рекомендациям выше позволят поддерживать высокий FPS даже при увеличении сложности игры.