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

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

![1269_GameDev_Unity_ru/image1.png](https://import.viva64.com/docx/blog/1269_GameDev_Unity_ru/image1.png)

## Введение

> Самым главным своим достижением в качестве программиста за последние годы я считаю знакомство с методикой статического анализа кода и её активное применение\. Дело даже не столько в сотнях серьёзных багов, не допущенных в код благодаря ей, сколько в перемене, вызванной этим опытом в моём программистском мировоззрении в отношении вопросов надежности и качества программного обеспечения\.
>
> 
>
> Джон Кармак \(из статьи "[Статический анализ кода](https://pvs-studio.ru/ru/blog/posts/a0087/)"\)\.

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

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


> Если у вас вдруг возникают вопрос из разряда: "А зачем нам нужон этот статический анализ? Нам линтера и тестировщиков хватает\\\!" то можно ознакомиться с различными аргументами в статье "\[Зачем разработчикам игр на Unity использовать статический анализ?\]\(https://pvs\-studio\.ru/ru/blog/posts/csharp/0945/\)"\\\.



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

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

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

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

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

![1269_GameDev_Unity_ru/image2.png](https://import.viva64.com/docx/blog/1269_GameDev_Unity_ru/image2.png)




> \*\*Примечание\\\.\*\* Если вы хотите узнать, как работает PVS\\\-Studio "под капотом", советую ознакомиться со статьей "\[Как работает статический анализ\]\(https://pvs\-studio\.ru/ru/blog/posts/1048/\)?"\\\.



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

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

Возьмём за основу диагностику [V3001](https://pvs-studio.ru/ru/docs/warnings/v3001/), суть который в выявлении ошибочного паттерна вида:

```cpp
if (x > 0 && x > 0)
```

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

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

```cpp
 if (x == x && y)
```

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

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

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

И всё :\)

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

Этот и другие примеры более подробно разобраны в статье "[Статический анализ и регулярные выражения](https://pvs-studio.ru/ru/blog/posts/cpp/0087/)"\.

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

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

![1269_GameDev_Unity_ru/image3.png](https://import.viva64.com/docx/blog/1269_GameDev_Unity_ru/image3.png)

Аннотации являются одним из самых важных механизмов анализатора, предоставляя ему возможность понимать специфику проаннотированных Unity\-методов за счёт дополнительной информации \(об аргументах, возвращаемых значениях, особенностях и т\.п\.\)\.


> \*\*Примечание\\\.\*\* Кстати, аннотации в PVS\\\-Studio используются не только для Unity\\\-методов\\\. К примеру, в анализаторе проаннотированы методы классов из пространства имён System\\\. Но помимо использования уже "вшитых" аннотаций, пользователи PVS\\\-Studio могут сами проаннотировать свой код для более глубокого и качественного анализа\\\. Подробнее об этом можно узнать в статье "\[Поймай уязвимость своими руками: пользовательские аннотации C\\\# кода\]\(https://pvs\-studio\.ru/ru/blog/posts/csharp/1175/\)"\\\.



Для наглядного примера давайте рассмотрим один из популярных Unity\-методов — `GetComponent`\. Уже из названия можно догадаться, что возвращаемое значение должно быть использовано \(чтобы не быть голословным, [документация](https://docs.unity3d.com/ScriptReference/GameObject.GetComponent.html) это тоже подтверждает\)\. 

Но иногда программист ошибается и забывает это сделать\. Например, как в проекте [MixedRealityToolkit\-Unity](https://github.com/microsoft/MixedRealityToolkit-Unity):

```cpp
void OnEnable()
{
  GameObject uiManager = GameObject.Find("UIRoot");

  if (uiManager)
  {
    uiManager.GetComponent<UIManager>();
  }
}
```

Предупреждение PVS\-Studio: [V3010](https://pvs-studio.ru/ru/docs/warnings/v3010/) The return value of function 'GetComponent' is required to be utilized\. 

Главная боль подобных ошибок в том, что их тяжело заметить после того, как они были допущены: IDE, скорее всего, не подсветит, компилятор не будет ругаться, а в игре что\-нибудь да сломается из\-за нарушенной логики\. 

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

Подробнее про аннотирование Unity\-методов можно прочитать в статье "[Как анализатор PVS\-Studio стал находить ещё больше ошибок в проектах на Unity](https://pvs-studio.ru/ru/blog/posts/csharp/0744/)"\.

### Оптимизация

Одной из особенностей интеграции PVS\-Studio с Unity является направление "[микрооптимизаций](https://pvs-studio.ru/ru/docs/warnings/#MicroOptimizationsCS)" — диагностик, направленных на \(как несложно догадаться\) повышение производительности проекта\. 


> \*\*Примечание\\\.\*\* На момент релиза PVS\\\-Studio 7\\\.37 в C\\\# анализаторе диагностики микрооптимизаций доступны только для Unity проектов\\\. Для С\\\+\\\+ проектов они имеют более универсальное применение, их список можно увидеть в \[документации\]\(https://pvs\-studio\.ru/ru/docs/warnings/\#MicroOptimizationsCPP\)\\\.



В основе диагностик микрооптимизаций для Unity лежат, помимо всего прочего, [рекомендации из официальной документации](https://docs.unity3d.com/Manual/performance-optimizing-code-managed-memory.html) движка\. Главной трудностью при создании этого направления было то, что многие рекомендации сводились к одному: "Не используйте тяжёлые конструкции", только вот такими конструкциями могут являться захваты переменных, конкатенация, упаковка\\распаковка и т\.д\. Безусловно, это ресурсоёмкие процессы, но при этом они распространены в коде, и срабатывания на все случаи их использования убили бы всё желание пользоваться анализатором\.

Для того чтобы избежать тонну нерелевантных срабатываний, была разработана система расчёта потенциальной частоты выполнения методов\. В некоторых местах кода оптимизация находится в уязвимом положении: например, Unity\-метод `Update` выполняется каждый кадр и, если поместить в него подобные методы, результат может быть не таким приятным\. Но этого всё ещё мало — влияние не такое серьёзное\.

Но давайте возьмём конкатенацию, в C\# она приводит к созданию нового объекта строки при каждом "склеивании", а значит будет больше "мусорных объектов" и, соответственно, вызова garbage collector'а\. А теперь давайте сделаем несколько конкатенаций, поместим их в цикл под несколько сотен итераций, и находиться это всё будет в `Update`\. Что произойдёт? Как говорится, думайте\.\.\. 


> \*\*Примечание\\\.\*\* Проблему множественной конкатенации может выявить \[Unity диагностика V4002\]\(https://pvs\-studio\.ru/ru/docs/warnings/v4002/\)\\\.



![1269_GameDev_Unity_ru/image4.png](https://import.viva64.com/docx/blog/1269_GameDev_Unity_ru/image4.png)

В реальности же проблемы с производительностью в коде будут не столь очевидными\. Например, в проекте [Daggerfall](https://github.com/Interkarma/daggerfall-unity) правило [V4001](https://pvs-studio.ru/ru/docs/warnings/v4001/) указало на ряд случаев упаковки при вызове метода `string.Format`:

```cpp
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](https://pvs-studio.ru/ru/blog/posts/csharp/1071/)"\.


> \*\*Примечание\\\.\*\* Но почему "микрооптимизация", а не просто "оптимизация"? Дело в том, что статический анализ помогает оптимизировать проект немного иначе, чем кажется\\\. Результат от одной правки может быть незначительным, но при регулярном анализе и исправлении проблем появляется накопительный эффект, который и приводит к повышению производительности и эффективности кода\\\. Если вас заинтересовала тема оптимизации с помощью статического анализа, предлагаю ознакомиться со статей "\[Поговорим о микрооптимизациях на примере кода Tizen\]\(https://pvs\-studio\.ru/ru/blog/posts/cpp/0520/?ysclid\=md36wcylup665726319\)"



## Запуск анализа

Давайте немного отойдём от особенностей интеграции с Unity и узнаем, как запустить анализ Unity проекта\. Это делается в несколько простых шагов:

**Для начала самое важное**

У вас должен быть установлен PVS\-Studio :\)

Если же вы ещё этого не сделали, то в этом вам поможет [данная](https://pvs-studio.ru/ru/pvs-studio/try-free/) страница\.

**Откройте проект в Unity**

Затем необходимо установить в настройках Unity предпочитаемый редактор скриптов\.

Это можно сделать с помощью параметра **External Script Editor** на вкладке **External Tools** в окне **Preferences**\. 

Чтобы открыть это окно, используйте опцию меню **Edit \> Preferences** в редакторе Unity:

![1269_GameDev_Unity_ru/image5.png](https://import.viva64.com/docx/blog/1269_GameDev_Unity_ru/image5.png)

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

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

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

![1269_GameDev_Unity_ru/image6.png](https://import.viva64.com/docx/blog/1269_GameDev_Unity_ru/image6.png)

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

Более подробно анализ проектов на Unity описан в [документации](https://pvs-studio.ru/ru/docs/manual/6607/)\.


> \*\*Примечание\\\.\*\* Если вы впервые используете PVS\\\-Studio, то анализ проекта может выдать большое количество предупреждений на весь код\\\. 
> 
> Вам не обязательно исправлять всё сразу\\\. Вы можете \[подавить\]\(https://pvs\-studio\.ru/ru/docs/manual/0032/\) все предупреждения и регулярно использовать анализ только на новом коде, периодически возвращаясь к старым предупреждениям\\\.



Если же хочется попробовать инструмент в действии, но не тратить много времени на подготовку и настройку анализатора \(что является важным и необходимым этапом при интеграции инструмента в разработку проекта, про это я делал доклад "[Внедрение SAST без слёз](https://pvs-studio.ru/ru/blog/video/11016/)"\), то советую вам попробовать воспользоваться функцией **Best Warnings**, отбирающей самые "интересные" и вероятные срабатывания из проекта\.

![1269_GameDev_Unity_ru/image7.png](https://import.viva64.com/docx/blog/1269_GameDev_Unity_ru/image7.png)

Про использование механизма **Best Warnings** и разбор ошибок, найденных с помощью него, можно почитать в статье "[Быстро и легко ищем баги в играх на Unity](https://pvs-studio.ru/ru/blog/posts/csharp/1111/)"\.

## Какие проблемы находим

Для Unity сейчас актуальны 3 направления диагностик:

* General Analysis — классические C\# диагностики; 
* Cпециализированные диагностики — диагностики, направленные на выявление проблем с качеством кода с учётом специфики Unity;
* Микрооптимизации — диагностики, направленные на выявление "слабых" мест \(с точки зрения оптимизации\) в коде\.

Давайте рассмотрим примеры из каждого направления\.

### General Analysis 

Стоит учитывать, что для Unity проектов всё так же актуальны и классические диагностики для C\#\. 

**Пример N1**

```cpp
public void RemoveData(Data data)
{
  if (data == null)
  {
    throw new GameFrameworkException(Utility.Text.Format("Data '{0}' is null.", 
                                                         data.Name.ToString()));
  }
  ....
}
```

Предупреждение PVS\-Studio: [V3080](https://pvs-studio.ru/ru/docs/warnings/v3080/) Possible null dereference\. Consider inspecting 'data'\. 

Разработчик проверяет параметр `data` на валидность\. Если он равен `null`, то генерируется исключение\. Вот только при формировании сообщения исключения происходит обращение к параметру `data`, который в этот момент является `null`\. В итоге вместо `GameFrameworkException` получаем исключение типа `NullReferenceException`\. 


> Этот пример взят из статьи "\[Быстро и легко ищем баги в играх на Unity\]\(https://pvs\-studio\.ru/ru/blog/posts/csharp/1111/\)"\\\.



**Пример N2**

```cpp
override NNInfoInternal GetNearestForce (....) 
{
  ....
  for (int w = 0; w < wmax; w++) {
    if (bestDistance < (w-2)*Math.Max(TileWorldSizeX, TileWorldSizeX)) break;
  }
}
```

Предупреждение PVS\-Studio: [V3038](https://pvs-studio.ru/ru/docs/warnings/v3038/) The 'TileWorldSizeX' argument was passed to 'Max' method several times\. It is possible that other argument should be passed instead\. 

В метод `Math.Max` передаётся переменная `TileWorldSizeX` в качестве первого и второго аргумента\. Но этот метод должен возвращать большее из двух переданных значений\. Что\-то пошло не так, и разработчик забыл поменять одну из переменных — в итоге будет возвращаться всегда одно и то же значение\.


> Этот пример взят из статьи "\[Исходный код на прощание: разбор ошибок в проектах закрывшейся инди\\\-студии\]\(https://pvs\-studio\.ru/ru/blog/posts/csharp/1169/\)"\\\.



### Специализированные диагностики

В анализаторе PVS\-Studio есть категория диагностик "специально для Unity"\. Они направлены на выявление проблем с качеством кода Unity проектов и учитывают особенности разработки под Unity\. 

Про новые специализированные диагностики вы можете почитать в статье "[PVS\-Studio в разработке на Unity: новые специализированные диагностики](https://pvs-studio.ru/ru/blog/posts/csharp/1236/)"\. Давайте рассмотрим парочку из них:

[**V3214**](https://pvs-studio.ru/ru/docs/warnings/v3214/)**\. Unity Engine\. Using Unity API in the background thread may result in an error\.**

Начнём с диагностики, которая является новой не только для нашего инструмента, но и для Unity, т\.к\. связана с новым классом — `Awaitable`\.


> Если вам интересны остальные нововведения в Unity, то можете ознакомиться с нашей обзорной статьёй "\[Что нового в Unity 6? Обзор нововведений и ошибок в исходном коде\]\(https://pvs\-studio\.ru/ru/blog/posts/csharp/1172/\)"\\\.



Анализатор обнаружил использование свойства, метода или конструктора после вызова `Awaitable.BackgroundThreadAsync`, которые при выполнении в фоновом потоке могут привести к таким проблемам, как зависание или выброс исключения\. 

[Согласно документации Unity](https://docs.unity3d.com/2023.2/Documentation/Manual/AwaitSupport.html), все API, взаимодействующие с движком, должны использоваться строго из главного потока\.

Пример кода, на котором анализатор PVS\-Studio сгенерирует предупреждение: 

```cpp

private async Awaitable LoadSceneAndDoHeavyComputation()
{
  await Awaitable.BackgroundThreadAsync();
  await SceneManager.LoadSceneAsync("MainScene");
  ....
}

public async Awaitable Update()
{
  if (....)
    await LoadSceneAndDoHeavyComputation(); 

  ....
}
```

При выполнении метода `LoadSceneAndDoHeavyComputation` вызывается `Awaitable.BackgroundThreadAsync`, который переносит выполнение последующего кода в рамках того же метода в фоновый поток\. 

Из\-за этого проблемы могут возникнуть при вызове `SceneManager.LoadSceneAsync`\.

[**V3216**](https://pvs-studio.ru/ru/docs/warnings/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 сгенерирует предупреждение: 

```cpp

public class ActivateTrigger: MonoBehaviour
{
  [SerializeField]
  GameObject _target;

  private void DoActivateTrigger()
  {
     var target = _target ?? gameObject;
    ....
  }
}
```

В этом случае, если значение `_target` ещё не менялось в процессе выполнения, проверка `??` будет считать `_target` не равным `null` независимо от того, было ли указано значение поля в инспекторе Unity или нет\.

Подробнее о том, почему так вышло, вы можете узнать в [документации](https://pvs-studio.ru/ru/docs/warnings/v3216/)\.

### Микрооптимизации

В дополнении к специализированным диагностикам, как мы уже говорили выше в статье, есть отдельное направление — диагностики микрооптимизации\. Давайте взглянем на пример реального срабатывания:

```cpp
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](https://pvs-studio.ru/ru/docs/warnings/v4005/) Expensive operation is performed inside the 'Camera\.main' property\. Using such property in performance\-sensitive context can lead to decreased performance\.

Проблема заключается в многократном использовании `Camera.main`, что приводит к [повышению нагрузки на процессор](https://docs.unity3d.com/ScriptReference/Camera-main.html)\.

Правильным подходом в данном случае будет создание дополнительной переменной, в которую запишется возвращаемое значение свойства `Camera.main`\. В дальнейшем можно будет обращаться к этой переменной без создания лишних экземпляров\.


> \*\*Примечание\\\.\*\* Этот пример взят из статьи "\[Возвращаемся на Гроув\\\-Стрит\\\. Анализ движка Grand Theft Auto: San Andreas на Unity\]\(https://pvs\-studio\.ru/ru/blog/posts/csharp/1083/\)"\\\.



## Заключение

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


> \*\*Анонс\\\.\*\* В скором времени будет ещё одна статья про интеграцию в игровой движок, но на этот раз про Unreal Engine\\\. А по Unity выйдет статья с разбором проблем в VR\\\-играх\\\. Если вам такое интересно, то ждём вас в \[нашем блоге\]\(https://pvs\-studio\.ru/ru/blog/posts/?tag\=GameDev\)\\\!



Вы можете [попробовать PVS\-Studio](https://pvs-studio.ru/ru/pvs-studio/try-free/) на своём проекте и протестировать все возможности анализатора с помощью полной триальной версии\. 

Если у вас есть пожелания по анализатору, статьям или возникли вопросы, то можете отправить их в [форме обратной связи](https://pvs-studio.ru/ru/about-feedback/?is_question_form_open=true)\. И, конечно, ждём вас в комментариях :\)