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

Как оптимизировал прошлый проект?

1.0 Junior🔥 161 комментариев
#Оптимизация#Опыт и софт-скиллы

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

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

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

Общий подход к оптимизации

Мой подход к оптимизации всегда комплексный и начинается с профилирования. Я использую Unity Profiler (CPU, GPU, Memory, Rendering), а также статические анализаторы (например, от Unity или JetBrains) для выявления узких мест. Процесс делится на три этапа: анализ, стратегическая оптимизация (архитектура) и тактическая (локальные исправления).

Архитектурные улучшения и управление памятью

1. Оптимизация пулинга объектов

В проектах с частым созданием/уничтожением объектов (пули, эффекты, мобы) я внедрял систему пулинга. Это радикально снижает нагрузку на GC (Garbage Collector).

public class ObjectPool : MonoBehaviour
{
    [SerializeField] private GameObject prefab;
    [SerializeField] private int initialSize = 10;

    private Queue<GameObject> pool = new Queue<GameObject>();

    private void Start()
    {
        for (int i = 0; i < initialSize; i++)
        {
            CreatePooledObject();
        }
    }

    private GameObject CreatePooledObject()
    {
        GameObject obj = Instantiate(prefab);
        obj.SetActive(false);
        pool.Enqueue(obj);
        return obj;
    }

    public GameObject GetObject()
    {
        if (pool.Count == 0)
        {
            CreatePooledObject();
        }
        GameObject obj = pool.Dequeue();
        obj.SetActive(true);
        return obj;
    }

    public void ReturnObject(GameObject obj)
    {
        obj.SetActive(false);
        pool.Enqueue(obj);
    }
}

2. Минимизация аллокаций памяти

  • Избегание аллокаций в критических циклах: Я заменял foreach на for в горячих точках, так как foreach может создавать аллокации через энумератор. Также избегал создания новых массивов/списков внутри методов Update().
  • Использование структур вместо классов для небольших, часто создаваемых данных (например, данных о попадании), если они не требуют ссылочной семантики.
  • Кэширование ссылок: Все компоненты и трансформы кэшируются в Awake() или Start(), чтобы избегать дорогостоящих вызовов GetComponent() и transform каждый кадр.
private Rigidbody rb;
private Transform myTransform;

private void Awake()
{
    rb = GetComponent<Rigidbody>(); // Кэшируем
    myTransform = transform;
}

private void Update()
{
    // Вместо GetComponent<Rigidbody>() или transform каждый кадр
    rb.AddForce(Vector3.up);
    Vector3 pos = myTransform.position;
}

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

1. Работа с текстурами и мешами

  • Атласирование текстур: Для UI и 2D-объектов объединял мелкие текстуры в атласы, сокращая количество материалов и смен состояния рендера (render state changes).
  • Оптимизация моделей: Убеждался, что импортированные меши имеют правильную масштабирование и разумное количество полигонов. Использовал сжатие текстур (ETC2, ASTC для мобильных платформ) и правильные форматы (DXT5 для PC).
  • Статическая батчинг (Static Batching) для статических объектов уровня и динамическая батчинг (Dynamic Batching) для небольших движущихся объектов с одинаковыми материалами (внимательно следил за ограничениями по вершинам).

2. Управление освещением и пост-процессом

  • Для статического освещения использовал Baked GI (Global Illumination), полностью исключая вычисления реального времени для сложных сцен.
  • Для динамических объектов применял Light Probes.
  • Отключал или снижал качество пост-процесс эффектов (например, глубина резкости, motion blur) на мобильных платформах или в стрессовых ситуациях.
  • Реализовывал LOD (Level Of Detail) системы для сложных мешей, особенно на открытых локациях.

Оптимизация кода и логики

1. Распределение вычислений

  • Корутины и Invoke использовались для действий, не требующих точности каждый кадр (например, проверка состояния AI с интервалом 0.5 сек вместо Update()).
  • Event-driven архитектура: Заменял постоянные проверки в Update() (if (condition)) на события. Например, вместо проверки if (health <= 0) каждый кадр у всех врагов, компонент здоровья вызывал событие OnDeath, на которое реагировал менеджер.

2. Использование Jobs System и Burst Compiler

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

using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;

public class SimpleJobSystem : MonoBehaviour
{
    private NativeArray<float> data;

    private void Start()
    {
        data = new NativeArray<float>(1000, Allocator.Persistent);
        // Заполняем данные...
        MyParallelJob job = new MyParallelJob { Data = data };
        JobHandle handle = job.Schedule(data.Length, 64);
        handle.Complete();
        // Используем результаты...
        data.Dispose();
    }

    struct MyParallelJob : IJobParallelFor
    {
        public NativeArray<float> Data;
        public void Execute(int index)
        {
            Data[index] = math.sqrt(Data[index]); // Параллельное вычисление
        }
    }
}

Профилирование на целевых платформах и итог

Ключевой момент — профилирование на целевой платформе (например, на реальном Android устройстве через Android Profiler или Frame Debugger). Оптимизации для PC часто не работают на мобильных устройствах из-за различий в GPU и CPU.

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

Как оптимизировал прошлый проект? | PrepBro