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

Введение
Самым главным своим достижением в качестве программиста за последние годы я считаю знакомство с методикой статического анализа кода и её активное применение. Дело даже не столько в сотнях серьёзных багов, не допущенных в код благодаря ей, сколько в перемене, вызванной этим опытом в моём программистском мировоззрении в отношении вопросов надежности и качества программного обеспечения.
Джон Кармак (из статьи "Статический анализ кода").
Сфера разработки игр сопровождается принятием факта, что мечте одолеть все баги не суждено сбыться. Но разработчики не сдаются без боя и продолжают бороться с проблемами в коде, используя различные инструменты. Сегодня поговорим об одном из серьёзных "power up'ов" по поиску багов во время разработки на Unity.
Анализатор PVS-Studio имеет долгую историю взаимодействия с Unity: всё начиналось с простой проверки C# части движка, но сейчас анализатор имеет целые направления специализированных диагностик и механизмов для работы с ним. Так давайте же узнаем, к чему привели почти 10 лет улучшения интеграции PVS-Studio с игровым движком Unity.
Если у вас вдруг возникают вопрос из разряда: "А зачем нам нужон этот статический анализ? Нам линтера и тестировщиков хватает!" то можно ознакомиться с различными аргументами в статье "Зачем разработчикам игр на Unity использовать статический анализ?".
Давайте же начнем обзор темы с разбора механизмов по выявлению проблем в коде Unity-проектов.
Как это работает
Интеграция PVS-Studio с Unity отличается большим спектром выявляемых проблем. Это достигается посредством нескольких технологий в разных направлениях. Давайте рассмотрим их.
Статический анализ
Стоит понимать, что для анализа Unity-проектов не всегда требуется кардинально другой подход или разработка новых механизмов. Чаще всего бо́льшая часть проекта является классическим C# кодом, поэтому все основные технологии анализа (синтаксический, семантический, data-flow и т.д.) работают на всю мощность.

Примечание. Если вы хотите узнать, как работает PVS-Studio "под капотом", советую ознакомиться со статьей "Как работает статический анализ?".
Отдельно хочу отметить момент, который по-прежнему всплывает в рассуждениях о статическом анализе: "Зачем так мудрить (строить синтаксические деревья и т.п.), если можно использовать регулярные выражения". В реальности, к сожалению, ограничиться применением регулярных выражений (они тоже иногда используются) не получится. Большинство проблем в коде, которые выявляет статический анализатор, требуют более комплексного подхода.
Чтобы не ходить вокруг да около, предлагаю рассмотреть один из показательных примеров различий между использованием регулярных выражений и технологий статического анализа.
Возьмём за основу диагностику V3001, суть который в выявлении ошибочного паттерна вида:
if (x > 0 && x > 0)
С первого взгляда может показаться, что подобный пример достаточно просто реализовать с помощью регулярных выражений. Например, ищем оператор &&
, а потом проверяем, что слева и справа от него одинаковые выражения, ограниченные скобками.
Но тут мы и начинаем погружаться в кроличью нору, ведь код не всегда будет выглядеть таким образом. Можно же написать так:
if (x == x && y)
Таааак, теперь нужно учитывать ==
, потому что вокруг &&
разные выражения, но ошибка есть. А еще придётся смотреть на приоритет операций. Также не будем забывать про все остальные операторы (<, >, <=, >=, ==, !=, &&, ||, -, /, &, |, ^
), а, ну ещё различные способы записи... Я думаю, вы поняли :)
А теперь давайте взглянем, как будет выглядеть метод выявления этого паттерна, если у нас есть синтаксическое дерево:
if (Equal(left, right))
{
// анализатор ругается
}
И всё :)
После того как в дереве нам встречается оператор сравнения, мы смотрим на левую и правую ветку дерева. В итоге скобки, приоритеты операций и тому подобное уже не так страшны.
Этот и другие примеры более подробно разобраны в статье "Статический анализ и регулярные выражения".
Аннотация методов
Чтобы лучше выполнять поиск проблем в коде, анализатору может потребоваться дополнительный контекст. Именно это дают аннотации методов, позволяя инструменту проверять корректность их выполнения.

Аннотации являются одним из самых важных механизмов анализатора, предоставляя ему возможность понимать специфику проаннотированных Unity-методов за счёт дополнительной информации (об аргументах, возвращаемых значениях, особенностях и т.п.).
Примечание. Кстати, аннотации в PVS-Studio используются не только для Unity-методов. К примеру, в анализаторе проаннотированы методы классов из пространства имён System. Но помимо использования уже "вшитых" аннотаций, пользователи PVS-Studio могут сами проаннотировать свой код для более глубокого и качественного анализа. Подробнее об этом можно узнать в статье "Поймай уязвимость своими руками: пользовательские аннотации C# кода".
Для наглядного примера давайте рассмотрим один из популярных Unity-методов — GetComponent
. Уже из названия можно догадаться, что возвращаемое значение должно быть использовано (чтобы не быть голословным, документация это тоже подтверждает).
Но иногда программист ошибается и забывает это сделать. Например, как в проекте MixedRealityToolkit-Unity:
void OnEnable()
{
GameObject uiManager = GameObject.Find("UIRoot");
if (uiManager)
{
uiManager.GetComponent<UIManager>();
}
}
Предупреждение PVS-Studio: V3010 The return value of function 'GetComponent' is required to be utilized.
Главная боль подобных ошибок в том, что их тяжело заметить после того, как они были допущены: IDE, скорее всего, не подсветит, компилятор не будет ругаться, а в игре что-нибудь да сломается из-за нарушенной логики.
Статический анализ может выявить эти ошибки, обладая контекстом об исследуемых методах, который и предоставляют аннотации. Благодаря этому анализатор обладает информацией о том, что возвращаемое значение должно быть использовано, и способен выявить аномалию в примере.
Подробнее про аннотирование Unity-методов можно прочитать в статье "Как анализатор PVS-Studio стал находить ещё больше ошибок в проектах на Unity".
Оптимизация
Одной из особенностей интеграции PVS-Studio с Unity является направление "микрооптимизаций" — диагностик, направленных на (как несложно догадаться) повышение производительности проекта.
Примечание. На момент релиза PVS-Studio 7.37 в C# анализаторе диагностики микрооптимизаций доступны только для Unity проектов. Для С++ проектов они имеют более универсальное применение, их список можно увидеть в документации.
В основе диагностик микрооптимизаций для Unity лежат, помимо всего прочего, рекомендации из официальной документации движка. Главной трудностью при создании этого направления было то, что многие рекомендации сводились к одному: "Не используйте тяжёлые конструкции", только вот такими конструкциями могут являться захваты переменных, конкатенация, упаковка\распаковка и т.д. Безусловно, это ресурсоёмкие процессы, но при этом они распространены в коде, и срабатывания на все случаи их использования убили бы всё желание пользоваться анализатором.
Для того чтобы избежать тонну нерелевантных срабатываний, была разработана система расчёта потенциальной частоты выполнения методов. В некоторых местах кода оптимизация находится в уязвимом положении: например, Unity-метод Update
выполняется каждый кадр и, если поместить в него подобные методы, результат может быть не таким приятным. Но этого всё ещё мало — влияние не такое серьёзное.
Но давайте возьмём конкатенацию, в C# она приводит к созданию нового объекта строки при каждом "склеивании", а значит будет больше "мусорных объектов" и, соответственно, вызова garbage collector'а. А теперь давайте сделаем несколько конкатенаций, поместим их в цикл под несколько сотен итераций, и находиться это всё будет в Update
. Что произойдёт? Как говорится, думайте...
Примечание. Проблему множественной конкатенации может выявить Unity диагностика V4002.

В реальности же проблемы с производительностью в коде будут не столь очевидными. Например, в проекте Daggerfall правило V4001 указало на ряд случаев упаковки при вызове метода string.Format
:
public static string GetTerrainName(int mapPixelX, int mapPixelY)
{
return string.Format("DaggerfallTerrain [{0},{1}]",
mapPixelX,
mapPixelY);
}
Здесь вызывается перегрузка string.Format
, имеющая сигнатуру string.Format(string, object, object)
. В итоге неявным образом при вызове будет происходить упаковка, что может негативно сказываться на производительности. При этом избавиться от упаковки легко — достаточно лишь вызвать у переменных mapPixelX
и mapPixelY
метод ToString
.
Этот пример и другие особенности микрооптимизаций подробнее описаны в статье "PVS-Studio помогает оптимизировать проекты на Unity Engine".
Примечание. Но почему "микрооптимизация", а не просто "оптимизация"? Дело в том, что статический анализ помогает оптимизировать проект немного иначе, чем кажется. Результат от одной правки может быть незначительным, но при регулярном анализе и исправлении проблем появляется накопительный эффект, который и приводит к повышению производительности и эффективности кода. Если вас заинтересовала тема оптимизации с помощью статического анализа, предлагаю ознакомиться со статей "Поговорим о микрооптимизациях на примере кода Tizen"
Запуск анализа
Давайте немного отойдём от особенностей интеграции с Unity и узнаем, как запустить анализ Unity проекта. Это делается в несколько простых шагов:
Для начала самое важное
У вас должен быть установлен PVS-Studio :)
Если же вы ещё этого не сделали, то в этом вам поможет данная страница.
Откройте проект в Unity
Затем необходимо установить в настройках Unity предпочитаемый редактор скриптов.
Это можно сделать с помощью параметра External Script Editor на вкладке External Tools в окне Preferences.
Чтобы открыть это окно, используйте опцию меню Edit > Preferences в редакторе Unity:

После этого можно открывать свой проект в выбранной IDE, используя опцию Assets > Open C# Project в редакторе Unity.
Запустите анализ в IDE
Я буду использовать Visual Studio 2022. Чтобы проанализировать проект в данной версии IDE, можно использовать пункт меню Extensions > PVS-Studio > Check Solution:

Всё! Анализ кода будет запущен, и после этого вы сможете начать работу с предупреждениями анализатора.
Более подробно анализ проектов на Unity описан в документации.
Примечание. Если вы впервые используете PVS-Studio, то анализ проекта может выдать большое количество предупреждений на весь код.
Вам не обязательно исправлять всё сразу. Вы можете подавить все предупреждения и регулярно использовать анализ только на новом коде, периодически возвращаясь к старым предупреждениям.
Если же хочется попробовать инструмент в действии, но не тратить много времени на подготовку и настройку анализатора (что является важным и необходимым этапом при интеграции инструмента в разработку проекта, про это я делал доклад "Внедрение SAST без слёз"), то советую вам попробовать воспользоваться функцией Best Warnings, отбирающей самые "интересные" и вероятные срабатывания из проекта.

Про использование механизма Best Warnings и разбор ошибок, найденных с помощью него, можно почитать в статье "Быстро и легко ищем баги в играх на Unity".
Какие проблемы находим
Для Unity сейчас актуальны 3 направления диагностик:
General Analysis — классические C# диагностики;
Cпециализированные диагностики — диагностики, направленные на выявление проблем с качеством кода с учётом специфики Unity;
Микрооптимизации — диагностики, направленные на выявление "слабых" мест (с точки зрения оптимизации) в коде.
Давайте рассмотрим примеры из каждого направления.
General Analysis
Стоит учитывать, что для Unity проектов всё так же актуальны и классические диагностики для C#.
Пример N1
public void RemoveData(Data data)
{
if (data == null)
{
throw new GameFrameworkException(Utility.Text.Format("Data '{0}' is null.",
data.Name.ToString()));
}
....
}
Предупреждение PVS-Studio: V3080 Possible null dereference. Consider inspecting 'data'.
Разработчик проверяет параметр data
на валидность. Если он равен null
, то генерируется исключение. Вот только при формировании сообщения исключения происходит обращение к параметру data
, который в этот момент является null
. В итоге вместо GameFrameworkException
получаем исключение типа NullReferenceException
.
Этот пример взят из статьи "Быстро и легко ищем баги в играх на Unity".
Пример N2
override NNInfoInternal GetNearestForce (....)
{
....
for (int w = 0; w < wmax; w++) {
if (bestDistance < (w-2)*Math.Max(TileWorldSizeX, TileWorldSizeX)) break;
}
}
Предупреждение PVS-Studio: V3038 The 'TileWorldSizeX' argument was passed to 'Max' method several times. It is possible that other argument should be passed instead.
В метод Math.Max
передаётся переменная TileWorldSizeX
в качестве первого и второго аргумента. Но этот метод должен возвращать большее из двух переданных значений. Что-то пошло не так, и разработчик забыл поменять одну из переменных — в итоге будет возвращаться всегда одно и то же значение.
Этот пример взят из статьи "Исходный код на прощание: разбор ошибок в проектах закрывшейся инди-студии".
Специализированные диагностики
В анализаторе PVS-Studio есть категория диагностик "специально для Unity". Они направлены на выявление проблем с качеством кода Unity проектов и учитывают особенности разработки под Unity.
Про новые специализированные диагностики вы можете почитать в статье "PVS-Studio в разработке на Unity: новые специализированные диагностики". Давайте рассмотрим парочку из них:
V3214. Unity Engine. Using Unity API in the background thread may result in an error.
Начнём с диагностики, которая является новой не только для нашего инструмента, но и для Unity, т.к. связана с новым классом — Awaitable
.
Если вам интересны остальные нововведения в Unity, то можете ознакомиться с нашей обзорной статьёй "Что нового в Unity 6? Обзор нововведений и ошибок в исходном коде".
Анализатор обнаружил использование свойства, метода или конструктора после вызова Awaitable.BackgroundThreadAsync
, которые при выполнении в фоновом потоке могут привести к таким проблемам, как зависание или выброс исключения.
Согласно документации Unity, все API, взаимодействующие с движком, должны использоваться строго из главного потока.
Пример кода, на котором анализатор PVS-Studio сгенерирует предупреждение:
private async Awaitable LoadSceneAndDoHeavyComputation()
{
await Awaitable.BackgroundThreadAsync();
await SceneManager.LoadSceneAsync("MainScene");
....
}
public async Awaitable Update()
{
if (....)
await LoadSceneAndDoHeavyComputation();
....
}
При выполнении метода LoadSceneAndDoHeavyComputation
вызывается Awaitable.BackgroundThreadAsync
, который переносит выполнение последующего кода в рамках того же метода в фоновый поток.
Из-за этого проблемы могут возникнуть при вызове SceneManager.LoadSceneAsync
.
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.
И ещё одна диагностика, но на этот раз про неочевидные особенности Unity движка.
Анализатор обнаружил ненадёжную проверку на null
у поля, которое может быть инициализировано в инспекторе Unity.
Пример кода, на котором анализатор PVS-Studio сгенерирует предупреждение:
public class ActivateTrigger: MonoBehaviour
{
[SerializeField]
GameObject _target;
private void DoActivateTrigger()
{
var target = _target ?? gameObject;
....
}
}
В этом случае, если значение _target
ещё не менялось в процессе выполнения, проверка ??
будет считать _target
не равным null
независимо от того, было ли указано значение поля в инспекторе Unity или нет.
Подробнее о том, почему так вышло, вы можете узнать в документации.
Микрооптимизации
В дополнении к специализированным диагностикам, как мы уже говорили выше в статье, есть отдельное направление — диагностики микрооптимизации. Давайте взглянем на пример реального срабатывания:
private void LateUpdate()
{
....
if (ped != null)
this.FocusPos = ped.transform.position;
else if (Camera.main != null)
this.FocusPos = Camera.main.transform.position;
....
float relAngle = Camera.main != null ?
Camera.main.transform.eulerAngles.y : 0f;
....
}
Предупреждение PVS-Studio: V4005 Expensive operation is performed inside the 'Camera.main' property. Using such property in performance-sensitive context can lead to decreased performance.
Проблема заключается в многократном использовании Camera.main
, что приводит к повышению нагрузки на процессор.
Правильным подходом в данном случае будет создание дополнительной переменной, в которую запишется возвращаемое значение свойства Camera.main
. В дальнейшем можно будет обращаться к этой переменной без создания лишних экземпляров.
Примечание. Этот пример взят из статьи "Возвращаемся на Гроув-Стрит. Анализ движка Grand Theft Auto: San Andreas на Unity".
Заключение
При таком бурном развитии сферы, разрабатывать игры становится всё труднее и дороже. Поэтому разработчикам стоит обратить внимание на инструменты, повышающие качество кода и удешевляющие процессы поиска ошибок. Статический анализатор PVS-Studio может стать хорошим выбором для разработки вашей новой игры или поддержания (и улучшения!) уже существующего проекта.
Анонс. В скором времени будет ещё одна статья про интеграцию в игровой движок, но на этот раз про Unreal Engine. А по Unity выйдет статья с разбором проблем в VR-играх. Если вам такое интересно, то ждём вас в нашем блоге!
Вы можете попробовать PVS-Studio на своём проекте и протестировать все возможности анализатора с помощью полной триальной версии.
Если у вас есть пожелания по анализатору, статьям или возникли вопросы, то можете отправить их в форме обратной связи. И, конечно, ждём вас в комментариях :)
Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Gleb Aslamov. GameDev Guardian: static analysis and Unity.