На защите GameDev'a: статический анализ и Unity

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

На защите GameDev'a: статический анализ и Unity

Введение

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

Джон Кармак (из статьи "Статический анализ кода").

Сфера разработки игр сопровождается принятием факта, что мечте одолеть все баги не суждено сбыться. Но разработчики не сдаются без боя и продолжают бороться с проблемами в коде, используя различные инструменты. Сегодня поговорим об одном из серьёзных "power up'ов" по поиску багов во время разработки на Unity.

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

Если у вас вдруг возникают вопрос из разряда: "А зачем нам нужон этот статический анализ? Нам линтера и тестировщиков хватает!" то можно ознакомиться с различными аргументами в статье "Зачем разработчикам игр на Unity использовать статический анализ?".

Давайте же начнем обзор темы с разбора механизмов по выявлению проблем в коде Unity-проектов.

Как это работает

Интеграция PVS-Studio с Unity отличается большим спектром выявляемых проблем. Это достигается посредством нескольких технологий в разных направлениях. Давайте рассмотрим их.

Статический анализ

Стоит понимать, что для анализа Unity-проектов не всегда требуется кардинально другой подход или разработка новых механизмов. Чаще всего большая часть проекта является классическим C# кодом, поэтому все основные технологии анализа (синтаксический, семантический, data-flow и т.д.) работают на всю мощность.

На защите GameDev'a: статический анализ и Unity

Примечание. Если вы хотите узнать, как работает PVS-Studio "под капотом", советую ознакомиться со статьей "Как работает статический анализ?".

Отдельно хочу отметить момент, который по-прежнему всплывает в рассуждениях о статическом анализе: "Зачем так мудрить (строить синтаксические деревья и т.п.), если можно использовать регулярные выражения". В реальности, к сожалению, ограничиться применением регулярных выражений (они тоже иногда используются) не получится. Большинство проблем в коде, которые выявляет статический анализатор, требуют более комплексного подхода.

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

Возьмём за основу диагностику V3001, суть который в выявлении ошибочного паттерна вида:

if (x > 0 && x > 0)

С первого взгляда может показаться, что подобный пример достаточно просто реализовать с помощью регулярных выражений. Например, ищем оператор `&&`, а потом проверяем, что слева и справа от него одинаковые выражения, ограниченные скобками.

Но тут мы и начинаем погружаться в кроличью нору, ведь код не всегда будет выглядеть таким образом. Можно же написать так:

if (x == x && y)

Таааак, теперь нужно учитывать `==`, потому что вокруг `&&` разные выражения, но ошибка есть. А еще придётся смотреть на приоритет операций. Также не будем забывать про все остальные операторы (`<, >, <=, >=, ==, !=, &&, ||, -, /, &, |, ^`), а, ну ещё различные способы записи... Я думаю, вы поняли :)

А теперь давайте взглянем, как будет выглядеть метод выявления этого паттерна, если у нас есть синтаксическое дерево:

if (Equal(left, right)) { // анализатор ругается }

И всё :)

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

Этот и другие примеры более подробно разобраны в статье "Статический анализ и регулярные выражения".

Аннотация методов

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

На защите GameDev'a: статический анализ и Unity

Аннотации являются одним из самых важных механизмов анализатора, предоставляя ему возможность понимать специфику проаннотированных 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.

На защите GameDev'a: статический анализ и Unity

В реальности же проблемы с производительностью в коде будут не столь очевидными. Например, в проекте 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:

На защите GameDev'a: статический анализ и Unity

После этого можно открывать свой проект в выбранной IDE, используя опцию Assets > Open C# Project в редакторе Unity.

Запустите анализ в IDE

Я буду использовать Visual Studio 2022. Чтобы проанализировать проект в данной версии IDE, можно использовать пункт меню Extensions > PVS-Studio > Check Solution:

На защите GameDev'a: статический анализ и Unity

Всё! Анализ кода будет запущен, и после этого вы сможете начать работу с предупреждениями анализатора.

Более подробно анализ проектов на Unity описан в документации.

Примечание. Если вы впервые используете PVS-Studio, то анализ проекта может выдать большое количество предупреждений на весь код.

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

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

На защите GameDev'a: статический анализ и Unity

Про использование механизма 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`. В дальнейшем можно будет обращаться к этой переменной без создания лишних экземпляров.

Заключение

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

Анонс. В скором времени будет ещё одна статья про интеграцию в игровой движок, но на этот раз про Unreal Engine. А по Unity выйдет статья с разбором проблем в VR-играх. Если вам такое интересно, то ждём вас в нашем блоге!

Вы можете попробовать PVS-Studio на своём проекте и протестировать все возможности анализатора с помощью полной триальной версии.

Если у вас есть пожелания по анализатору, статьям или возникли вопросы, то можете отправить их в форме обратной связи. И, конечно, ждём вас в комментариях :)

4
Начать дискуссию