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

Приведи пример интересной ситуации при оптимизации

1.0 Junior🔥 141 комментариев
#Оптимизация

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

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

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

Пример оптимизации рендеринга множества объектов в Unity

Одна из самых интересных и сложных ситуаций в моей практике — оптимизация рендеринга 10,000+ анимированных персонажей в мобильной игре с открытым миром. Игра тормозила на 15 FPS на средних устройствах, хотя по логике должно было работать на 60 FPS.

Проблема и анализ

Первоначальная архитектура использовала стандартный подход:

  • Каждый персонаж — отдельный GameObject с SkinnedMeshRenderer
  • Анимация через Animator и Mecanim
  • Управление через скрипты на C#
// Исходный неоптимизированный код управления персонажем
public class NPCController : MonoBehaviour
{
    private Animator animator;
    private NavMeshAgent agent;
    
    void Update()
    {
        // Логика AI в каждом кадре
        UpdateMovement();
        UpdateAnimation();
    }
}

Профилирование в Unity Profiler показало:

  • CPU bottleneck: 35ms на анимацию (Mecanim overhead)
  • Draw calls: 5000+ из-за отдельных материалов
  • Batch breaking: из-за разных трансформаций и скининга

Решение: многоуровневая оптимизация

1. Инстансинг анимированных мешей

Замена отдельных SkinnedMeshRenderer на GPU Instancing с анимацией в шейдере:

// Вершинный шейдер с анимацией на GPU
v2f vert(appdata_t v, uint instanceID : SV_InstanceID)
{
    v2f o;
    float4 pos = mul(_BoneMatrices[instanceID][v.boneIndex], v.vertex);
    o.vertex = UnityObjectToClipPos(pos);
    return o;
}
// Управление инстансингом на C#
public class NPCBatchRenderer : MonoBehaviour
{
    private ComputeBuffer boneMatricesBuffer;
    private MaterialPropertyBlock propertyBlock;
    
    void Update()
    {
        // Обновление всех анимаций за один вызов
        UpdateAllAnimationsOnGPU();
        Graphics.DrawMeshInstanced(mesh, 0, material, matrices, count, propertyBlock);
    }
}

2. LOD система с дискретными уровнями

public class NPCLODSystem : MonoBehaviour
{
    private enum LODLevel
    {
        High = 0,    // Полная анимация, 500 полигонов
        Medium = 1,  // Упрощенная анимация, 200 полигонов  
        Low = 2,     // Билборд, 10 полигонов
        Culled = 3   // Не рисуется
    }
    
    void UpdateLOD(Vector3 cameraPosition)
    {
        float distance = Vector3.Distance(transform.position, cameraPosition);
        currentLOD = CalculateLODLevel(distance);
    }
}

3. Job System + Burst Compiler для логики AI

[BurstCompile]
public struct NPCJob : IJobParallelFor
{
    public NativeArray<Vector3> positions;
    public NativeArray<Vector3> velocities;
    public float deltaTime;
    
    public void Execute(int index)
    {
        // Векторизированные вычисления AI
        positions[index] += velocities[index] * deltaTime;
    }
}

Результаты и инсайты

После оптимизации:

  • FPS вырос с 15 до 55 на том же устройстве
  • Draw calls сократились с 5000 до ~200
  • Потребление CPU на анимацию упало с 35ms до 4ms
  • Потребление памяти снизилось на 40%

Ключевые выводы:

  1. Профилирование прежде оптимизации — без точных данных из Profiler мы бы оптимизировали не те узкие места
  2. Архитектурные изменения эффективнее микрооптимизаций — переход на инстансинг дал на порядок больше выигрыша, чем оптимизация отдельных методов
  3. Мобильные GPU любят инстансинг — это их сильная сторона в отличие от CPU скининга
  4. Сложность возрастает экспоненциально — каждая оптимизация создавала новые проблемы (например, управление коллизиями для инстансированных объектов)

Самое интересное — это компромиссы: мы пожертвовали индивидуальностью анимаций (все персонажи в одном батче имели одинаковые позы в один момент времени), но получили 4-кратный прирост производительности. В финальной версии мы использовали гибридный подход: ближние персонажи рендерились индивидуально, дальние — инстансированными батчами, что дало и производительность, и визуальное качество.