Unity Оптимізуйте свою гру за допомогою Profiler

Продуктивність є ключовим аспектом будь-якої гри, і не дивно, якою б гарною не була гра, якщо вона погано працює на комп’ютері користувача, вона не буде такою приємною.

Оскільки не всі мають комп’ютер або пристрій високого класу (якщо ви націлені на мобільні пристрої), важливо пам’ятати про продуктивність протягом усього курсу розробки.

Є кілька причин, чому гра може працювати повільно:

  • Візуалізація (забагато високополігональних сіток, складних шейдерів або ефектів зображення)
  • Аудіо (переважно через неправильні налаштування імпорту аудіо)
  • Неоптимізований код (скрипти, які містять функції, що потребують продуктивності, у неправильних місцях)

У цьому підручнику я покажу, як оптимізувати ваш код за допомогою Unity Profiler.

Профайлер

Історично налагодження продуктивності в Unity було виснажливим завданням, але з того часу було додано нову функцію під назвою Profiler.

Profiler — це інструмент у Unity, який дозволяє швидко визначити вузькі місця у вашій грі, відстежуючи споживання пам’яті, що значно спрощує процес оптимізації.

Вікно Unity Profiler

Погана продуктивність

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

Перевірте приклад нижче:

У Сцені у мене є Куб із прикріпленим до нього сценарієм, який переміщує Куб з боку в бік і відображає назву об’єкта:

SC_ShowName.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SC_ShowName : MonoBehaviour
{
    bool moveLeft = true;
    float movedDistance = 0;

    // Start is called before the first frame update
    void Start()
    {
        moveLeft = Random.Range(0, 10) > 5;
    }

    // Update is called once per frame
    void Update()
    {
        //Move left and right in ping-pong fashion
        if (moveLeft)
        {
            if(movedDistance > -2)
            {
                movedDistance -= Time.deltaTime;
                Vector3 currentPosition = transform.position;
                currentPosition.x -= Time.deltaTime;
                transform.position = currentPosition;
            }
            else
            {
                moveLeft = false;
            }
        }
        else
        {
            if (movedDistance < 2)
            {
                movedDistance += Time.deltaTime;
                Vector3 currentPosition = transform.position;
                currentPosition.x += Time.deltaTime;
                transform.position = currentPosition;
            }
            else
            {
                moveLeft = true;
            }
        }
    }

    void OnGUI()
    {
        //Show object name on screen
        Camera mainCamera = Camera.main;
        Vector2 screenPos = mainCamera.WorldToScreenPoint(transform.position + new Vector3(0, 1, 0));
        GUI.color = Color.green;
        GUI.Label(new Rect(screenPos.x - 150/2, Screen.height - screenPos.y, 150, 25), gameObject.name);
    }
}

Дивлячись на статистику, ми бачимо, що гра працює зі швидкістю 800+ кадрів в секунду, тому це майже не впливає на продуктивність.

Але давайте подивимося, що станеться, якщо ми продублюємо Куб 100 разів:

Fps впав більш ніж на 700 балів!

ПРИМІТКА. Усі тести проводилися з вимкненим Vsync

Як правило, доцільно починати оптимізацію, коли гра починає зависати, зависає або кадри в секунду падають нижче 120.

Як використовувати Profiler?

Щоб почати використовувати Profiler, вам знадобиться:

  • Розпочніть гру, натиснувши «Грати».
  • Відкрийте Profiler, перейшовши до Window -> Analysis -> Profiler (або натисніть Ctrl + 7)

  • З’явиться нове вікно приблизно такого вигляду:

Вікно Unity 3D Profiler

  • Спочатку це може виглядати лякаюче (особливо з усіма цими діаграмами тощо), але це не та частина, яку ми розглядатимемо.
  • Перейдіть на вкладку «Шкала часу» та змініть її на «Ієрархія»:

  • Ви помітите 3 розділи (EditorLoop, PlayerLoop і Profiler.CollectEditorStats):

  • Розгорніть PlayerLoop, щоб побачити всі частини, на які витрачається обчислювальна потужність (ПРИМІТКА: якщо значення PlayerLoop не оновлюються, натисніть кнопку "Clear" у верхній частині вікна Profiler).

Щоб отримати найкращі результати, спрямуйте свого ігрового персонажа до ситуації (або місця), де гра найбільше затримується, і зачекайте кілька секунд.

  • Трохи почекавши, зупиніть гру та перегляньте список PlayerLoop

Вам потрібно подивитися на значення GC Alloc, що означає розподіл сміття. Це тип пам’яті, який було виділено component, але він більше не потрібен і чекає на звільнення за допомогою Garbage Collection. В ідеалі код не повинен генерувати жодного сміття (або бути максимально близьким до 0).

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

Щоб швидше знайти проблемні частини, клацніть стовпець GC Alloc, щоб відсортувати значення від більшого до нижчого)

  • На діаграмі використання ЦП клацніть будь-де, щоб перейти до цього кадру. Зокрема, нам потрібно дивитися на піки, де fps був найнижчим:

Діаграма використання ЦП Unity

Ось що показав Profiler:

GUI.Repaint виділяє 45,4 КБ, що досить багато, розширивши його, можна отримати більше інформації:

  • Це показує, що більшість розподілів надходить із методів GUIUtility.BeginGUI() і OnGUI() у сценарії SC_ShowName, знаючи, що ми можемо почати оптимізацію.

GUIUtility.BeginGUI() представляє порожній метод OnGUI() (так, навіть порожній метод OnGUI() виділяє досить багато пам’яті).

Використовуйте Google (або іншу пошукову систему), щоб знайти імена, які ви не впізнаєте.

Ось частина OnGUI(), яку потрібно оптимізувати:

    void OnGUI()
    {
        //Show object name on screen
        Camera mainCamera = Camera.main;
        Vector2 screenPos = mainCamera.WorldToScreenPoint(transform.position + new Vector3(0, 1, 0));
        GUI.color = Color.green;
        GUI.Label(new Rect(screenPos.x - 150/2, Screen.height - screenPos.y, 150, 25), gameObject.name);
    }

Оптимізація

Почнемо оптимізацію.

Кожен сценарій SC_ShowName викликає власний метод OnGUI(), що не добре, враховуючи, що у нас є 100 екземплярів. Отже, що з цим можна зробити? Відповідь: мати єдиний сценарій із методом OnGUI(), який викликає метод графічного інтерфейсу для кожного куба.

  • По-перше, я замінив стандартний OnGUI() у сценарії SC_ShowName на public void GUIMethod(), який буде викликаний з іншого сценарію:
    public void GUIMethod()
    {
        //Show object name on screen
        Camera mainCamera = Camera.main;
        Vector2 screenPos = mainCamera.WorldToScreenPoint(transform.position + new Vector3(0, 1, 0));
        GUI.color = Color.green;
        GUI.Label(new Rect(screenPos.x - 150/2, Screen.height - screenPos.y, 150, 25), gameObject.name);
    }
  • Потім я створив новий сценарій і назвав його SC_GUIMethod:

SC_GUIMethod.cs

using UnityEngine;

public class SC_GUIMethod : MonoBehaviour
{
    SC_ShowName[] instances; //All instances where GUI method will be called

    void Start()
    {
        //Find all instances
        instances = FindObjectsOfType<SC_ShowName>();
    }

    void OnGUI()
    {
        for(int i = 0; i < instances.Length; i++)
        {
            instances[i].GUIMethod();
        }
    }
}

SC_GUIMethod буде приєднаний до випадкового об’єкта на сцені та викликає всі методи GUI.

  • Ми перейшли від 100 індивідуальних методів OnGUI() до лише одного, давайте натиснемо відтворення та побачимо результат:

  • GUIUtility.BeginGUI() тепер виділяє лише 368 Б замість 36,7 КБ, велике зменшення!

Однак метод OnGUI() все ще виділяє пам’ять, але оскільки ми знаємо, що він викликає лише GUIMethod() зі сценарію SC_ShowName, ми переходимо безпосередньо до налагодження цього методу.

Але Profiler показує лише глобальну інформацію, як ми бачимо, що саме відбувається всередині методу?

Для налагодження всередині методу Unity має зручний API під назвою Profiler.BeginSample

Profiler.BeginSample дозволяє захопити певний розділ сценарію, показуючи, скільки часу знадобилося для виконання та скільки пам’яті було виділено.

  • Перш ніж використовувати клас Profiler у коді, нам потрібно імпортувати простір імен UnityEngine.Profiling на початку сценарію:
using UnityEngine.Profiling;
  • Зразок Profiler захоплюється шляхом додавання Profiler.BeginSample("SOME_NAME"); на початку запису та додавання Profiler.EndSample(); наприкінці захоплення, наприклад це:
        Profiler.BeginSample("SOME_CODE");
        //...your code goes here
        Profiler.EndSample();

Оскільки я не знаю, яка частина GUIMethod() спричиняє виділення пам’яті, я вклав кожен рядок у Profiler.BeginSample і Profiler.EndSample (але якщо ваш метод містить багато рядків, вам точно не потрібно вкладати кожен рядок, просто розділіть його на рівні частини, а потім працюйте звідти).

Ось останній метод із реалізованими зразками Profiler:

    public void GUIMethod()
    {
        //Show object name on screen
        Profiler.BeginSample("sc_show_name part 1");
        Camera mainCamera = Camera.main;
        Profiler.EndSample();

        Profiler.BeginSample("sc_show_name part 2");
        Vector2 screenPos = mainCamera.WorldToScreenPoint(transform.position + new Vector3(0, 1, 0));
        Profiler.EndSample();

        Profiler.BeginSample("sc_show_name part 3");
        GUI.color = Color.green;
        GUI.Label(new Rect(screenPos.x - 150/2, Screen.height - screenPos.y, 150, 25), gameObject.name);
        Profiler.EndSample();
    }
  • Тепер я натискаю Play і дивлюся, що відображається в Profiler:
  • Для зручності я шукав "sc_show_" в Profiler, оскільки всі зразки починаються з цього імені.

  • Цікаво... У частині 3 sc_show_names виділяється багато пам'яті, яка відповідає цій частині коду:
        GUI.color = Color.green;
        GUI.Label(new Rect(screenPos.x - 150/2, Screen.height - screenPos.y, 150, 25), gameObject.name);

Трохи погугливши, я виявив, що отримання імені об’єкта виділяє досить багато пам’яті. Рішення полягає в тому, щоб призначити ім’я об’єкта рядковій змінній у void Start(), таким чином її буде викликано лише один раз.

Ось оптимізований код:

SC_ShowName.cs

using UnityEngine;
using UnityEngine.Profiling;

public class SC_ShowName : MonoBehaviour
{
    bool moveLeft = true;
    float movedDistance = 0;

    string objectName = "";

    // Start is called before the first frame update
    void Start()
    {
        moveLeft = Random.Range(0, 10) > 5;
        objectName = gameObject.name; //Store Object name to a variable
    }

    // Update is called once per frame
    void Update()
    {
        //Move left and right in ping-pong fashion
        if (moveLeft)
        {
            if(movedDistance > -2)
            {
                movedDistance -= Time.deltaTime;
                Vector3 currentPosition = transform.position;
                currentPosition.x -= Time.deltaTime;
                transform.position = currentPosition;
            }
            else
            {
                moveLeft = false;
            }
        }
        else
        {
            if (movedDistance < 2)
            {
                movedDistance += Time.deltaTime;
                Vector3 currentPosition = transform.position;
                currentPosition.x += Time.deltaTime;
                transform.position = currentPosition;
            }
            else
            {
                moveLeft = true;
            }
        }
    }

    public void GUIMethod()
    {
        //Show object name on screen
        Profiler.BeginSample("sc_show_name part 1");
        Camera mainCamera = Camera.main;
        Profiler.EndSample();

        Profiler.BeginSample("sc_show_name part 2");
        Vector2 screenPos = mainCamera.WorldToScreenPoint(transform.position + new Vector3(0, 1, 0));
        Profiler.EndSample();

        Profiler.BeginSample("sc_show_name part 3");
        GUI.color = Color.green;
        GUI.Label(new Rect(screenPos.x - 150/2, Screen.height - screenPos.y, 150, 25), objectName);
        Profiler.EndSample();
    }
}
  • Давайте подивимося, що показує Profiler:

Усі зразки виділяють 0B, тому пам’ять більше не виділяється.

Рекомендовані статті
Поради щодо оптимізації для Unity
Покращення продуктивності мобільної гри в Unity
Налаштування імпорту аудіокліпів Unity для найкращої продуктивності
Як використовувати оновлення в Unity
Генератор білбордів для Unity
Як стати кращим програмістом в Unity
Як створити гру на основі FNAF в Unity