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

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

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

## Введение

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

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

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

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

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

Вывод из всего сказанного один: чтобы увеличить шансы на успех вашей игры, количество багов нужно минимизировать, а оптимизацию поддерживать на высоком уровне с самого начала разработки\. В особо амбициозных проектах для этого не обойтись без специальных инструментов, таких как 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\.

Ссылка на [документацию к диагностике](https://pvs-studio.ru/ru/docs/warnings/v3216/)\.

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

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



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

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

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

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

Однако для оператора `??` этот объект является корректным, а потому переменной `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\.

Ссылка на [документацию к диагностике](https://pvs-studio.ru/ru/docs/warnings/v3209/)\.

Класс `Awaitable` — относительно новый механизм для асинхронного и параллельного программирования, который появился в 2023\.1 версии Unity\. По сути, он является аналогом `Task`\. Основное отличие в том, что в некоторых сценариях использования `Awaitable` даёт значительный выигрыш в производительности\. Получить больше информации об этом классе можно в [документации Unity](https://docs.unity3d.com/6000.0/Documentation/Manual/async-await-support.html)\.

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

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

Ссылка на [документацию к диагностике](https://pvs-studio.ru/ru/docs/warnings/v3214/)\.

Кроме асинхронного выполнения, класс `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\.

Ссылка на [документацию к диагностике](https://pvs-studio.ru/ru/docs/warnings/v3206/)\.

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

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

Ссылка на [документацию к диагностике](https://pvs-studio.ru/ru/docs/warnings/v3215/)\.

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

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

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

Как упоминалось ранее, некоторый код Unity\-скриптов имеет очень большую частоту выполнения, в частности, в таких методах, как [`Update`](https://docs.unity3d.com/6000.0/Documentation/ScriptReference/MonoBehaviour.Update.html), [`FixedUpdate`](https://docs.unity3d.com/2023.2/Documentation/ScriptReference/MonoBehaviour.FixedUpdate.html?source=post_page---------------------------) и т\.д\. Это означает, что в нём следует избегать:

* многократного выполнения вычислительно тяжёлых операций, результат которых можно закэшировать;
* регулярного выделения и освобождения памяти \(т\.к\. частое выделение памяти приводит к частому вызову сборщика мусора, что негативно отражается на производительности\);
* неэффективных математических операций со сложными структурами вроде `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\.

Ссылка на [документацию к диагностике](https://pvs-studio.ru/ru/docs/warnings/v4005/)\.

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

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

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

Ссылка на [документацию к диагностике](https://pvs-studio.ru/ru/docs/warnings/v4006/)\.

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

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

1. `_requestedMoveDirection * _moveSpeed * Time.deltaTime`;
1. `_requestedMoveDirection * (_moveSpeed * Time.deltaTime)`\.

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

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

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

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

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

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

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

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

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

1. `Vector3 * float * float * float * float`;
1. `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\.

Ссылка на [документацию к диагностике](https://pvs-studio.ru/ru/docs/warnings/v4007/)\.

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

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

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

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

Если регулярно создаваемых/уничтожаемых объектов \(например, снарядов\) множество, то в этом случае вам помогут пулы объектов\. К слову, в Unity уже есть универсальные реализации [пулов](https://docs.unity3d.com/ScriptReference/Pool.ObjectPool_1.html), для использования которых вам лишь надо определить несколько обработчиков\.

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

Ссылка на [документацию к диагностике](https://pvs-studio.ru/ru/docs/warnings/v4008/)\.

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

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

Стоит отметить, что, если буфер является обычным списком, вызов `Clear` не приведёт к освобождению памяти, выделенной под этот буфер\. Если вы об этом не знали, возможно, вам будет интересно ознакомиться с нашей [статьёй](https://pvs-studio.ru/ru/blog/posts/csharp/0995/), посвящённой особенностям реализации списка в C\#\.

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

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

<details>
   <summary>Полный список диагностик, ориентированных на Unity\\\-проекты</summary>

[V3188](https://pvs-studio.ru/ru/docs/warnings/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](https://pvs-studio.ru/ru/docs/warnings/v3205/)\. Unity Engine\. Improper creation of 'MonoBehaviour' or 'ScriptableObject' object using the 'new' operator\. Use the special object creation method instead\.

[V3206](https://pvs-studio.ru/ru/docs/warnings/v3206/)\. Unity Engine\. A direct call to the coroutine\-like method will not start it\. Use the 'StartCoroutine' method instead\.

[V3208](https://pvs-studio.ru/ru/docs/warnings/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](https://pvs-studio.ru/ru/docs/warnings/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](https://pvs-studio.ru/ru/docs/warnings/v3210/)\. Unity Engine\. Unity does not allow removing the 'Transform' component using 'Destroy' or 'DestroyImmediate' methods\. The method call will be ignored\.

[V3211](https://pvs-studio.ru/ru/docs/warnings/v3211/)\. Unity Engine\. The operators '?\.', '??' and '??\=' do not correctly handle destroyed objects derived from 'UnityEngine\.Object'\.

[V3212](https://pvs-studio.ru/ru/docs/warnings/v3212/)\. Unity Engine\. Pattern matching does not correctly handle destroyed objects derived from 'UnityEngine\.Object'\.

[V3213](https://pvs-studio.ru/ru/docs/warnings/v3213/)\. Unity Engine\. The 'GetComponent' method must be instantiated with a type that inherits from 'UnityEngine\.Component'\.

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

[V3215](https://pvs-studio.ru/ru/docs/warnings/v3215/)\. Unity Engine\. Passing a method name as a string literal into the 'StartCoroutine' is unreliable\.

[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\.

[V4001](https://pvs-studio.ru/ru/docs/warnings/v4001/)\. Unity Engine\. Boxing inside a frequently called method may decrease performance\.

[V4002](https://pvs-studio.ru/ru/docs/warnings/v4002/)\. Unity Engine\. Avoid storing consecutive concatenations inside a single string in performance\-sensitive context\. Consider using StringBuilder to improve performance\.

[V4003](https://pvs-studio.ru/ru/docs/warnings/v4003/)\. Unity Engine\. Avoid capturing variable in performance\-sensitive context\. This can lead to decreased performance\.

[V4004](https://pvs-studio.ru/ru/docs/warnings/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](https://pvs-studio.ru/ru/docs/warnings/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](https://pvs-studio.ru/ru/docs/warnings/v4006/)\. Unity Engine\. Multiple operations between complex and numeric values\. Prioritizing operations between numeric values can optimize execution time\.

[V4007](https://pvs-studio.ru/ru/docs/warnings/v4007/)\. Unity Engine\. Avoid creating and destroying UnityEngine objects in performance\-sensitive context\. Consider activating and deactivating them instead\.

[V4008](https://pvs-studio.ru/ru/docs/warnings/v4008/)\. Unity Engine\. Avoid using memory allocation Physics APIs in performance\-sensitive context\.


</details>
## Что ещё особенное есть в PVS\-Studio?

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

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

Ознакомиться с полным перечнем C\# диагностик \(и не только\) вы можете на нашем [сайте](https://pvs-studio.ru/ru/docs/warnings/)\.

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

* знает, что сокращённые проверки `target`/`!target` эквивалентны проверкам `target != null`\`target \=\= null`;`
* понимает, что вышеобозначенные проверки могут указывать не только на null\-статус объекта, но и на его уничтоженность на игровой сцене;
* благодаря механизму аннотаций \(о котором у нас есть отдельная [статья](https://pvs-studio.ru/ru/blog/posts/csharp/0744/)\) учитывает специфику работы и использования основных API Unity, которую нельзя вычислить автоматически\.

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

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

* [Зачем разработчикам игр на Unity использовать статический анализ?](https://pvs-studio.ru/ru/blog/posts/csharp/0945/)
* [Быстро и легко ищем баги в играх на Unity \(для C\# разработчиков\)](https://pvs-studio.ru/ru/blog/posts/csharp/1111/)\.

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

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

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

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

Также хотелось бы упомянуть, что вы всегда можете бесплатно опробовать PVS\-Studio на собственных проектах, просто запросив триал на [официальном сайте](https://pvs-studio.ru/ru/pvs-studio/try-free/)\. До встречи в следующих статьях\!