Мы используем куки, чтобы пользоваться сайтом было удобно.
Хорошо
to the top
>
>
>
PVS-Studio в разработке на Unity: новые…

PVS-Studio в разработке на Unity: новые специализированные диагностики

14 Мар 2025

По сей день Unity остаётся популярен у тысяч разработчиков. На этом движке созданы многие популярные игры, такие как V Rising, Beat Saber, Hearthstone, Genshin Impact и прочие. Насколько полезен был бы анализатор PVS-Studio разработчикам таких проектов? Какие ошибки и возможности для оптимизации кода он мог бы помочь найти? Давайте узнаем!

Введение

Для начала хочется отметить, возможно, очевидную вещь: написание кода для игры — занятие непростое. Поддержание качества этого кода — задача не менее трудная. Многие баги проскальзывают в релиз, после чего их находят игроки, что значительно ухудшает их первое впечатление и может стать ключевым фактором для решения написать негативный отзыв, обзор или вовсе удалить игру. Это в свою очередь может негативно отразиться на потенциальных продажах.

Помимо багов, у игровых проектов есть ещё один не менее, а может, и более критичный "камень преткновения": плохая оптимизация. Во многом это связано с тем, что обычно в игровом коде не используются преимущества параллелизма. Ещё разработчики часто пренебрегают оптимизацией таких вещей, как:

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

При этом они забывают, что их код может выполняться десятки раз в секунду. Если таких пренебрежений будет много, они могут значительно ухудшить производительность игры.

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

Вывод из всего сказанного один: чтобы увеличить шансы на успех вашей игры, количество багов нужно минимизировать, а оптимизацию поддерживать на высоком уровне с самого начала разработки. В особо амбициозных проектах для этого не обойтись без специальных инструментов, таких как PVS-Studio. Особенно теперь, когда анализатор научился находить как специфичные ошибки, так и возможности для оптимизации в коде Unity-проектов! Именно с этими нововведениями мы хотим познакомить вас в этой статье.

Что такое PVS-Studio?

На случай, если вы слышите это название впервые: PVS-Studio — статический анализатор кода, инструмент, который автоматически проверяет код вашего проекта без его фактического выполнения на наличие потенциальных ошибок, уязвимостей безопасности и возможностей для оптимизации.

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

Какие специфичные для Unity-проектов предупреждения теперь может выдавать PVS-Studio?

Первые наши Unity-диагностики по большей части основаны на рекомендациях и специфичных моментах, о которых мы узнали из документации движка. Сейчас таких диагностик всего 20. В рамках этого пункта предлагаем рассмотреть лишь некоторые из них. Полный перечень вы сможете найти в конце статьи.

Диагностики специфичных ошибок

V3216. Unity Engine. Checking a field with a specific Unity Engine type for null may not work correctly due to implicit field initialization by the engine.

Ссылка на документацию к диагностике.

Итак, первая диагностика в нашем обзоре предупреждает об ошибках, связанных с далеко неочевидным поведением: неявной инициализацией полей ваших компонентов самим движком. Это происходит, если тип поля UnityEngine.Object или наследуется от него (исключения — MonoBehaviour и SerializableObject), а сами поля могут быть отображены в инспекторе Unity.

Представим, что в нашем коде есть такое поле, значение которому не присваивается ни при его объявлении, ни в инспекторе Unity. В таком случае мы планируем инициализировать его некоторым значением по умолчанию во время выполнения. Это может выглядеть так:

[SerializeField] GameObject DefaultTarget;
public GameObject Target;
....

void Update()
{
  var target = Target ?? DefaultTarget;
  ....
}

В Update, если Target не было явно задано значение, мы ожидаем, что поле будет равно null. Но в действительности это не так, ведь Target будет неявно инициализирован объектом, с точки зрения Unity эквивалентным null:

Однако для оператора ?? этот объект является корректным, а потому переменной target значение DefaultTarget не присвоится.

Чтобы избежать подобных неожиданностей, для проверки на null следует использовать операторы ==, != или сокращённые проверки field, !field, которые имеют специальные перегрузки, учитывающие специфичные для Unity нюансы.

V3209. Unity Engine. Using await on 'Awaitable' object more than once can lead to exception or deadlock, as such objects are returned to the pool after being awaited.

Ссылка на документацию к диагностике.

Класс Awaitable — относительно новый механизм для асинхронного и параллельного программирования, который появился в 2023.1 версии Unity. По сути, он является аналогом Task. Основное отличие в том, что в некоторых сценариях использования Awaitable даёт значительный выигрыш в производительности. Получить больше информации об этом классе можно в документации Unity.

У Awaitable есть свои ограничения. Одно из них состоит в том, что к каждому экземпляру Awaitable оператор await может быть применён лишь раз. Это связано с тем, что они объединяются в пул, и при применении к ним await возвращаются в него. Повторное применение await к той же ссылке может привести к непредсказуемым негативным последствиям, таким как выброс исключения или "состояние гонки".

V3214. Unity Engine. Using Unity API in the background thread may result in an error.

Ссылка на документацию к диагностике.

Кроме асинхронного выполнения, класс Awaitable предоставляет возможность вынести выполнение в фоновый поток с помощью Awaitable.BackgroundThreeadAsync. Для этого нужно вызвать этот метод с помощью await, после чего весь последующий код в текущем методе будет выполнен в фоновом потоке, но уже при обработке следующего кадра. Для возврата выполнения в главный поток (также с задержкой в кадр) используется метод Awaitable.MainThreadAsync.

И здесь также есть своё ограничение: из фонового потока нельзя вызывать Unity API, меняющее какое-либо состояние в игре (загрузка новой сцены, изменение позиций объектов, запуск анимации и пр.). Это приведёт к непредсказуемым последствиям вроде зависания игры или выброса исключения. Однако можно безопасно использовать функции из Mathf, а также все основные структуры данных вроде Vector3 и Quaternion.

V3206. Unity Engine. A direct call to the coroutine-like method will not start it. Use the 'StartCoroutine' method instead.

Ссылка на документацию к диагностике.

Эта диагностика предупреждает о попытке запуска корутины как обычного метода, без вызова StartCoroutine. На первый взгляд, это ошибка новичка, однако она вполне может быть допущена из-за усталости или невнимательности, после чего потребуется потратить ещё некоторое время на выяснение, почему часть кода не работает без каких-либо сообщений об ошибках. Куда лучше после написания скрипта запустить анализатор, через несколько секунд получить предупреждение и сразу внести правку, не так ли?

V3215. Unity Engine. Passing a method name as a string literal into the 'StartCoroutine' is unreliable.

Ссылка на документацию к диагностике.

Продолжаем отлавливать возможные проблемы с корутинами. Эта диагностика предупреждает о запуске корутины через передачу в StartCoroutine строкового литерала. Само по себе это ошибкой не является, по крайне мере пока... Дело в том, что в какой-то момент название корутины может поменяться и, если это изменение также не будет учтено в подобных вызовах, они просто перестанут работать.

Лучше всего запускать корутину через передачу в StartCoroutine её IEnumerator, но если запуск по названию по каким-то причинам принципиален, стоит рассмотреть возможность использования оператора nameof.

Оптимизационные диагностики

Как упоминалось ранее, некоторый код Unity-скриптов имеет очень большую частоту выполнения, в частности, в таких методах, как Update, FixedUpdate и т.д. Это означает, что в нём следует избегать:

  • многократного выполнения вычислительно тяжёлых операций, результат которых можно закэшировать;
  • регулярного выделения и освобождения памяти (т.к. частое выделение памяти приводит к частому вызову сборщика мусора, что негативно отражается на производительности);
  • неэффективных математических операций со сложными структурами вроде Vector3 и Quaternion.

Однако в пылу разработки не всегда удаётся помнить про такие моменты, а потому хорошо иметь под рукой инструмент, который будет обращать ваше внимание на них, не правда ли?

V4005. Unity Engine. The expensive operation is performed inside method or property. Using such member in performance-sensitive context can lead to decreased performance.

Ссылка на документацию к диагностике.

Диагностика предупреждает о вызове распространённых и относительно тяжёлых функций, таких как FindObjectOfType, FindGameObjectWithTag, GetComponent и пр., внутри часто выполняемого кода.

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

V4006. Unity Engine. Multiple operations between complex and numeric values. Prioritizing operations between numeric values can optimize execution time.

Ссылка на документацию к диагностике.

Все мы привыкли думать, что порядок множителей в математическом выражении не имеет значения. Однако когда в этом выражении участвуют не только привычные числа, но и комплексные структуры, такие как вектора, это уже не совсем так. В этом случае от порядка выполнения операций умножения может зависеть фактическое количество выполняемых вычислений. Последнее же, в целях оптимизации, лучше стараться минимизировать.

Диагностика обращает ваше внимание на такие возможности оптимизации. Для наглядности давайте посчитаем количество выполняемых вычислений в следующих двух вариантах одного и того же выражения:

  • _requestedMoveDirection * _moveSpeed * Time.deltaTime;
  • _requestedMoveDirection * (_moveSpeed * Time.deltaTime).

Здесь _requestedMoveDirection имеет тип Vector3, тогда как _moveSpeed и Time.deltaTime — вещественные числа с типом float.

В первом случае:

  • При умножении _requestedMoveDirection на _moveSpeed фактически будут выполнены 3 операции умножения, так как при умножении Vector3 на число каждая из трёх компонент вектора умножается на это число. В результате будет получен новый вектор.
  • Результат предыдущей операции умножается на Time.deltaTime, для чего также потребуются 3 операции умножения.

Итого: 6 операций.

А теперь аналогичным образом разберём второй вариант:

  • _moveSpeed умножается на Time.deltaTime, т.к. это умножение числа на число, будет выполнена только 1 операция умножения, и результат также будет являться числом;
  • _requestedMoveDirection умножается на результат предыдущей операции. Для этого, как мы уже знаем, потребуются 3 операции умножения.

Итого: 4 операции.

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

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

  • Vector3 * float * float * float * float;
  • Vector3 * (float * float * float * float).

То получится уже 15 и 6 операций, соответственно.

V4007. Unity Engine. Avoid creating and destroying UnityEngine objects in performance-sensitive context. Consider activating and deactivating them instead.

Ссылка на документацию к диагностике.

Знали ли вы, что регулярное создание и уничтожение игровых объектов в методах вроде Update является плохой практикой? Это приводит к частому выделению и освобождению значительных объёмов памяти и заставляет сборщик мусора работать усерднее, замедляя вашу игру.

В большинстве случаев операции создания и уничтожения объектов можно заменить на операции включения, отключения и сброса состояния.

Так как отключённый объект всё равно "существует", занимаемая им память освобождаться не будет (а значит, и повторно выделяться). Кроме того, включение объекта обходится движку намного "дешевле" в плане вычислительных ресурсов, чем создание нового экземпляра объекта.

Если такой объект один (допустим, некоторое модальное окно), то можно создать его экземпляр, например, в методе Start, кэшировать в поле и далее в Update использовать это поле, при необходимости включая/отключая его на сцене.

Если регулярно создаваемых/уничтожаемых объектов (например, снарядов) множество, то в этом случае вам помогут пулы объектов. К слову, в Unity уже есть универсальные реализации пулов, для использования которых вам лишь надо определить несколько обработчиков.

V4008. Unity Engine. Avoid using memory allocation Physics APIs in performance-sensitive context.

Ссылка на документацию к диагностике.

В Unity 5.3 и более поздних версиях были представлены неаллоцирующие версии cast-методов в Physics.

Так, к примеру, для Physics.RaycastAll появился неаллоцирующий аналог Physics.RaycastNonAlloc. Вся разница между ними в том, что Physics.RaycastAll создаёт новую коллекцию для результата при каждом вызове. Physics.RaycastNonAlloc же принимает в качестве параметра уже созданный буфер, в который будут добавляться полученные RaycastHit. В первом случае при каждом вызове происходит новое выделение памяти, что, как нам уже известно, нехорошо. Во втором случае буфер можно вынести в поле и с помощью метода Clear сбрасывать его перед каждым новым использованием.

Стоит отметить, что, если буфер является обычным списком, вызов Clear не приведёт к освобождению памяти, выделенной под этот буфер. Если вы об этом не знали, возможно, вам будет интересно ознакомиться с нашей статьёй, посвящённой особенностям реализации списка в C#.

Полный список новых диагностик

Мы рассмотрели несколько примеров новых Unity-ориентированных диагностик, а полный список, как и обещал, вы можете найти ниже.

V3188. Unity Engine. The value of an expression is a potentially destroyed Unity object or null. Member invocation on this value may lead to an exception.

V3205. Unity Engine. Improper creation of 'MonoBehaviour' or 'ScriptableObject' object using the 'new' operator. Use the special object creation method instead.

V3206. Unity Engine. A direct call to the coroutine-like method will not start it. Use the 'StartCoroutine' method instead.

V3208. Unity Engine. Using 'WeakReference' with 'UnityEngine.Object' is not supported. GC will not reclaim the object's memory because it is linked to a native object.

V3209. Unity Engine. Using await on 'Awaitable' object more than once can lead to exception or deadlock, as such objects are returned to the pool after being awaited.

V3210. Unity Engine. Unity does not allow removing the 'Transform' component using 'Destroy' or 'DestroyImmediate' methods. The method call will be ignored.

V3211. Unity Engine. The operators '?.', '??' and '??=' do not correctly handle destroyed objects derived from 'UnityEngine.Object'.

V3212. Unity Engine. Pattern matching does not correctly handle destroyed objects derived from 'UnityEngine.Object'.

V3213. Unity Engine. The 'GetComponent' method must be instantiated with a type that inherits from 'UnityEngine.Component'.

V3214. Unity Engine. Using Unity API in the background thread may result in an error.

V3215. Unity Engine. Passing a method name as a string literal into the 'StartCoroutine' is unreliable.

V3216. Unity Engine. Checking a field with a specific Unity Engine type for null may not work correctly due to implicit field initialization by the engine.

V4001. Unity Engine. Boxing inside a frequently called method may decrease performance.

V4002. Unity Engine. Avoid storing consecutive concatenations inside a single string in performance-sensitive context. Consider using StringBuilder to improve performance.

V4003. Unity Engine. Avoid capturing variable in performance-sensitive context. This can lead to decreased performance.

V4004. Unity Engine. New array object is returned from method or property. Using such member in performance-sensitive context can lead to decreased performance.

V4005. Unity Engine. The expensive operation is performed inside method or property. Using such member in performance-sensitive context can lead to decreased performance.

V4006. Unity Engine. Multiple operations between complex and numeric values. Prioritizing operations between numeric values can optimize execution time.

V4007. Unity Engine. Avoid creating and destroying UnityEngine objects in performance-sensitive context. Consider activating and deactivating them instead.

V4008. Unity Engine. Avoid using memory allocation Physics APIs in performance-sensitive context.

Что ещё особенное есть в PVS-Studio?

Хочется отметить, что Unity-специфичные диагностики — это лишь малая часть возможностей PVS-Studio по анализу C# кода. Анализатор может находить множество ошибок, актуальных для всех C# проектов. Среди них диагностики, обнаруживающие:

  • потенциальные null-разыменования;
  • недостижимый код;
  • всегда true или всегда false условия;
  • ошибки из-за неправильного представления о приоритетах арифметических операций;
  • арифметическое переполнение;
  • десятки паттернов опечаток в коде;
  • и пр.

Ознакомиться с полным перечнем C# диагностик (и не только) вы можете на нашем сайте.

Кроме того, в анализаторе имеются механизмы, корректирующие и улучшающие работу некоторых общих диагностик c учётом специфики Unity-скриптов. Таким образом, анализатор:

  • знает, что сокращённые проверки target/!target эквивалентны проверкам target != null`target == null;
  • понимает, что вышеобозначенные проверки могут указывать не только на null-статус объекта, но и на его уничтоженность на игровой сцене;
  • благодаря механизму аннотаций (о котором у нас есть отдельная статья) учитывает специфику работы и использования основных API Unity, которую нельзя вычислить автоматически.

Заключение

Если вы разработчик на Unity, и анализатор кода PVS-Studio привлёк ваше внимание, хотелось бы порекомендовать к прочтению пару наших статей:

Возможно, в них вы найдёте ответы на вопросы, которые могли у вас возникнуть при прочтении этой статьи.

Кроме того, мне также было бы интересно увидеть ваше мнение по паре вопросов:

  • Что вы думаете о пользе использования в Unity-разработке таких инструментов, как PVS-Studio?
  • Знакомы ли вы с DOTS и, если да, хотели бы иметь под рукой инструмент, дающий советы по исправлению и оптимизации вашего ECS-кода?

Буду рад увидеть ваши ответы в комментариях :)

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

Последние статьи:

Опрос:

Дарим
электронную книгу
за подписку!

book terrible tips
Популярные статьи по теме


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

Следующие комментарии next comments
close comment form
close form

Заполните форму в два простых шага ниже:

Ваши контактные данные:

Шаг 1
Поздравляем! У вас есть промокод!

Тип желаемой лицензии:

Шаг 2
Team license
Enterprise license
** Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности
close form
Запросите информацию о ценах
Новая лицензия
Продление лицензии
--Выберите валюту--
USD
EUR
RUB
* Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности

close form
Бесплатная лицензия PVS‑Studio для специалистов Microsoft MVP
* Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности

close form
Для получения лицензии для вашего открытого
проекта заполните, пожалуйста, эту форму
* Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности

close form
Я хочу принять участие в тестировании
* Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности

close form
check circle
Ваше сообщение отправлено.

Мы ответим вам на


Если вы так и не получили ответ, пожалуйста, проверьте, отфильтровано ли письмо в одну из следующих стандартных папок:

  • Промоакции
  • Оповещения
  • Спам