﻿# Копаемся в открытом исходном коде Unity VR игр\. Часть 1: RocketMan

Это первая статья из небольшого цикла, посвящённого знакомству с некоторыми любопытными VR\-играми, а заодно и с примерами проблем в их исходном коде, которые нашлись с помощью PVS\-Studio\. Знакомьтесь, RocketMan\!

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

## О игре

[RocketMan](https://github.com/EliaFantini/RocketMan-a-VR-videogame-created-with-Unity/) — это очень короткая, простая, но всё же увлекательная игра\-головоломка\. По лору вы собрались сбежать с погибающей планеты на космической ракете, но всё пошло не так: ракета оказалась в совершенно непригодном для использования состоянии, системы вышли из строя, все отсеки заперты, а вы застряли в одном из них\. Теперь перед вами стоит задача выбраться из ловушки, привести в порядок один отсек за другим, в конечном итоге починить всю ракету и наконец улететь\!

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

Имея в своём распоряжении VR\-гарнитуру, я сам сыграл в игру и в целом получил положительные эмоции, несмотря на визуальную простоту, некорректность захвата некоторых предметов и одну внеплановую эвакуацию из ракеты через текстуры :\) 

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

Теперь, когда мы немного познакомились с игрой, почему бы не познакомиться с ошибками в её исходном коде, которые были найдены с помощью PVS\-Studio? Но погодите, а что такое PVS\-Studio?

## O PVS\-Studio

Если вдруг вы слышите это название впервые, PVS\-Studio — это инструмент, который автоматически проверяет качество и безопасность кода на C\#, Java, C или С\+\+ без его непосредственного выполнения\. Когда я говорю "код", то имею в виду не только отдельные классы или файлы, но и целые проекты и решения\. Опечатки, потенциальные исключения, логические ошибки, уязвимости безопасности — далеко неполный список того, что умеет находить этот инструмент\.

Также хочется отметить, что в PVS\-Studio уделено отдельное внимание специфике кода Unity\-скриптов\. В частности, в анализаторе есть:

* аннотации API из пространства имён UnityEngine, предоставляющие дополнительную информацию о назначении, принимаемых аргументах и возвращаемых значениях API\. Эта информация помогает детектировать больше ошибок, для выявления которых требуется хорошее понимание потоков данных в коде;
* диагностики, детектирующие специфичные для Unity\-скриптов проблемы;
* диагностики, выявляющие возможности для микрооптимизаций в блоках таких часто выполняемых функций, как `Update`;
* учёт перегрузки проверок на равенство/неравенство Unity\-объектов с `null`\. Анализатор понимает, что такие проверки могут также означать уничтоженность объекта в игре, что сокращает количество ложных предупреждений о потенциальном null\-разыменовании\.

Ещё больше узнать об этих особенностях можно в следующих статьях:

* [Как анализатор PVS\-Studio стал находить ещё больше ошибок в проектах на Unity](https://pvs-studio.ru/ru/blog/posts/csharp/0744/);
* [PVS\-Studio в разработке на Unity: новые специализированные диагностики](https://pvs-studio.ru/ru/blog/posts/csharp/1236/)\. 

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

Теперь давайте рассмотрим принцип базовой работы с анализатором\. Он достаточно прост:

* вместе с анализатором устанавливается специальное расширение для вашей IDE \(у меня это Visual Studio\);
* в инспекторе IDE выбираются файлы, проекты или решения, которые нужно проанализировать, и с помощью специальной опции в контекстном меню запускается анализ;

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

* Во время анализа на специальной панели будут постепенно появляться его результаты:

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

* Просматриваем предупреждения анализатора, которых может оказаться немало\. В этом случае можно воспользоваться многочисленными фильтрами и функцией массового обратимого [подавления](https://pvs-studio.ru/ru/docs/manual/0032/) предупреждений\. Она позволяет просматривать предупреждения частями, чтобы можно было распределить нагрузку\.

При желании больше информации об анализаторе вы можете найти на [сайте](https://pvs-studio.ru/ru/docs/manual/6607/?ysclid=md8hmmtsug555878439)\. А теперь давайте наконец приступим к обзору проблем, найденных в коде игры\.

## О исходном коде

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

Куда больше удивили скрипты Oculus, представляющие базовые компоненты для VR\-взаимодействий\. Большая часть ошибок была найдена именно в них, хоть их тоже немного\. Также стоит отметить, что в игре используется устаревшее VR\-API\. Но это не повод нам с вами не прокачать свой скилл программиста и не поучиться на них :\)

### Обзор ошибок

#### Неиспользованный итератор

```cpp
public void InsertChar(string c) 
{
  ....
  waiter(); //avoid double button clicks
}
IEnumerator waiter()
{
  yield return new WaitForSecondsRealtime(2);
}
```

Предупреждение PVS\-Studio: 

[V3206](https://pvs-studio.ru/ru/docs/warnings/v3206/) Unity Engine\. A direct call to the 'waiter' coroutine\-like method will not start it\. Use the 'StartCoroutine' method instead\. [Keyboard\_Tuto\.cs 26](https://github.com/EliaFantini/RocketMan-a-VR-videogame-created-with-Unity/blob/12daf818cc1994b240e5d78e65f633b6e300dfcc/CS-440/Assets/Scripts/Tutorial/Keyboard_Tuto.cs#L26)\.

В методе `InsertChar` есть вызов метода 'waiter', который по задумке должен приостанавливать поток выполнения на две секунды\. На это указывает возвращаемое значение `waiter` — `new WaitForSecondsRealtime(2)`\. 

В действительности же это работать не будет, причём сразу по двум причинам:

1. При вызове `waiter` будет возвращён объект\-итератор без выполнения логики в теле метода\. Чтобы эта логика все же выполнилась, нужно вызвать `waiter().MoveNext()`;
1. Сам по себе возврат `new WaitForSecondsRealtime(2)` не приостанавливает поток выполнения\. По сути, это итератор\-таймер, который при вызове у него 'MoveNext' возвращает логическое значение, указывающее, прошёл ли заданный отрезок времени или нет\. 

Вероятно, программист хотел предотвратить повторный вызов `InsertChar` из\-за случайного повторного клика по некоторой кнопке\. В таком случае корректный код может выглядеть так:

```cpp
bool _disableInsertChar = false;
public void InsertChar(string c) 
{
  if (_disableInsertChar)
    return;
  ....
  StartCoroutine(waiter()); 
}
IEnumerator waiter()
{
  _disableInsertChar = true;
  yield return new WaitForSecondsRealtime(2);
  _disableInsertChar = false;
}
```

В этой реализации `waiter` будет запущен как корутина Unity, т\.к\. возвращённый им итератор будет передан в качестве аргумента методу `StartCoroutine`\. Это значит, что логика в блоке `waiter` будет выполняться асинхронно, а возврат объекта `new WaitForSecondsRealtime(2)` действительно будет приостанавливать выполнение этой логики на две секунды \(т\.к\. этот класс предназначен именно для использования в корутинах\)\.

Новое поле `_disableInsertChar` будет установлено в `true` при запуске корутины, и в `false` — по истечению двух секунд и завершению работы корутины\. В начало метода `InsertChar` был добавлен возврат из метода, если `_disableInsertChar` равен `true`\.

#### Некорректная проверка на null ввиду неявной инициализации поля

```cpp
public class OVRTrackedKeyboard : MonoBehaviour
{
  ....
  public Transform ActiveKeyboardTransform;
  ....
}

public class OVRTrackedKeyboardHands : MonoBehaviour
{
  ....
  private void LateUpdate()
  {
    ....
    var position = KeyboardTracker.ActiveKeyboardTransform?.position;
    var rotation = KeyboardTracker.ActiveKeyboardTransform?.rotation;
    ....
  }
  ....
}
```

Предупреждение PVS\-Studio: 

[V3216](https://pvs-studio.ru/ru/docs/warnings/v3216/) \[CWE\-754\] Unity Engine\. Checking the 'ActiveKeyboardTransform' field with a specific Unity Engine type for null with '?\.' may not work correctly due to implicit field initialization by the engine\. [OVRTrackedKeyboardHands\.cs 296](https://github.com/EliaFantini/RocketMan-a-VR-videogame-created-with-Unity/blob/12daf818cc1994b240e5d78e65f633b6e300dfcc/CS-440/Assets/Oculus/VR/Scripts/OVRTrackedKeyboard/OVRTrackedKeyboardHands.cs#L296)\.

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

Для наглядности, давайте отладимся в простом `MonoBehaviour`\-скрипте, где есть лишь два публичных поля: одно c типом `Transform`, а другое — `MonoBehaviour`\. Оставив значения полей в инспекторе пустыми, давайте посмотрим, действительно ли они оба имеют значение null:

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

Как видим поле с типом `Transform` инициализировано некоторым объектом, который лишь "притворяется" null\-ом, но не является им\. Это значит, что такие проверки на null, как `?.`, `??`, `??=`, `is null` попросту не сработают, ведь для них поле инициализировано некоторым значением\.

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

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

Несмотря на эту оговорку, для проверки на null всё же стоит использовать операторы `==`, `!=` или сокращённые проверки `field`, `!field`, чтобы избежать неожиданных ошибок при отладке\. Оба эти способа имеют специальные перегрузки, учитывающие неявную инициализацию, а значит, будут всегда работать правильно\.

#### Путаница между \|\| и && операторами

```cpp
public static bool SaveCubemapCapture(....)
{
  ....
  if (    dirName[dirName.Length - 1] != '/' 
       || dirName[dirName.Length - 1] != '\\')
  {
    dirName += "/";
  }
  ....
}
```

Предупреждение PVS\-Studio: 

[V3022](https://pvs-studio.ru/ru/docs/warnings/v3022/) \[CWE\-571\] Expression is always true\. Probably the '&&' operator should be used here\. [OVRCubemapCapture\.cs 201](https://github.com/EliaFantini/RocketMan-a-VR-videogame-created-with-Unity/blob/12daf818cc1994b240e5d78e65f633b6e300dfcc/CS-440/Assets/Oculus/VR/Scripts/Util/OVRCubemapCapture.cs#L201)\.

Отвлечёмся от специфичных для Unity\-скриптов ошибок и рассмотрим вполне типичный случай\. Условие в приведённом коде всегда равно `true`, ведь в нем через оператор `||` выполняются проверки на неравенство `dirName[dirName.Length - 1]` двум разным значениям\. Таким образом, если первое подусловие будет ложно, то второе — истинно, и наоборот\.

Очевидно, этой проверкой программист хотел убедиться, что в конце строки имеется разделитель файлового пути\. Чтобы она заработала так, как ожидалось, достаточно заменить оператор `||` на `&&`\.

#### Неосвобождённая память после чтения потока

```cpp
private SceneInfo GetSceneInfo()
{
  SceneInfo sceneInfo = new SceneInfo();
  try
  {
    var reader = new StreamReader(sceneLoadDataPath); // <=
    ....
    while (!reader.EndOfStream)
    {
      sceneList.Add(reader.ReadLine());
    }
    sceneInfo.scenes = sceneList;
  }
  ....
}
```

Предупреждение PVS\-Studio: 

[V3114](https://pvs-studio.ru/ru/docs/warnings/v3114/) \[CWE\-404\] IDisposable object 'reader' is not disposed before method returns\. [OVRSceneLoader\.cs 212](https://github.com/EliaFantini/RocketMan-a-VR-videogame-created-with-Unity/blob/12daf818cc1994b240e5d78e65f633b6e300dfcc/CS-440/Assets/Oculus/VR/Scripts/OVRSceneLoader.cs#L212) 

Здесь анализатор обнаружил небольшую утечку вычислительных ресурсов\. После использования объекта типа `StreamReader` требуется явно освободить выделенную под сторонние \(недоступные для GC\) ресурсы память, вызвав метод `Dispose()`\.

Кроме того, можно воспользоваться конструкцией `using`, по завершению выполнения блока которой `Dispose` будет вызван неявно\.  Исправленный код может выглядеть так:

```cpp
private SceneInfo GetSceneInfo()
{
  SceneInfo sceneInfo = new SceneInfo();
  try
  {
    using (var reader = new StreamReader(sceneLoadDataPath))
    {
      ....
      while (!reader.EndOfStream)
      {
        sceneList.Add(reader.ReadLine());
      }
      sceneInfo.scenes = sceneList;

    }
  }
  ....
}
```

#### Одинаковые подвыражения слева и справа от оператора

**Issue 1**

```cpp
private static bool RenameSpatializerPluginToOld(string currentPluginPath)
{
  ....
  targetPluginPath = currentPluginPath + ".old" + index.ToString();
  targetPluginMetaPath = targetPluginPath + ".meta";

  if (!File.Exists(targetPluginPath) && !File.Exists(targetPluginPath))
    break;
  ....
}
```

Предупреждение PVS\-Studio: 

[V3001](https://pvs-studio.ru/ru/docs/warnings/v3001/) There are identical sub\-expressions '\!File\.Exists\(targetPluginPath\)' to the left and to the right of the '&&' operator\. [ONSPAudioPluginUpdater\.cs 138](https://github.com/EliaFantini/RocketMan-a-VR-videogame-created-with-Unity/blob/12daf818cc1994b240e5d78e65f633b6e300dfcc/CS-440/Assets/Oculus/Spatializer/editor/ONSPAudioPluginUpdater.cs#L138)\.

Типичная ошибка, допущенная по невнимательности\. В условии дважды подряд проверяется существование файла по одному и тому же пути из `targetPluginPath`\. Наличие рядом с условием ещё одной переменной, хранящей путь к файлу `targetPluginMetaPath`, наводит на мысль, что вторая проверка предназначалась для неё\.

Рассмотрим похожий случай\.

**Issue 2**

```cpp
protected void UpdateActive(OvrAvatar avatar, 
                            ovrAvatarVisibilityFlags mask)
{
  ....
  bool showFirstPerson = (mask & 
                          ovrAvatarVisibilityFlags.FirstPerson) != 0;

  bool showThirdPerson = (mask & 
                          ovrAvatarVisibilityFlags.ThirdPerson) != 0;

  gameObject.SetActive(showThirdPerson || showThirdPerson);
  ....
}
```

Предупреждение PVS\-Studio: 

[V3001](https://pvs-studio.ru/ru/docs/warnings/v3001/) There are identical sub\-expressions 'showThirdPerson' to the left and to the right of the '\|\|' operator\. [OvrAvatarRenderComponent\.cs 21](https://github.com/EliaFantini/RocketMan-a-VR-videogame-created-with-Unity/blob/12daf818cc1994b240e5d78e65f633b6e300dfcc/CS-440/Assets/Oculus/Avatar/Scripts/OvrAvatarRenderComponent.cs#L21)\.

Обратите внимание на бинарное выражение, передаваемое в метод `gameObject.SetActive`\. Оно представляет собой оператор `||`, слева и справа от которого проверяется одна и та же переменная `showThirdPerson`\. Как и в предыдущем случае, в ближайшем к условию коде есть ещё одна переменная `showFirstPerson`, которая, вероятно, должна была использоваться вместо одного из дубликатов\.

#### Цикл, который вовсе не цикл

```cpp
protected override void DoSelectUpdate(....)
{
  ....
  while (!outsideDelta)
  {
    ....
    if (....)
    {
      outsideDelta = true;
      break;
    }

    if (....)
    {
      outsideDelta = true;
      break; // <=
    }

    break; // <=
  }
  ....
}
```

Предупреждение PVS\-Studio: 

[V3020](https://pvs-studio.ru/ru/docs/warnings/v3020/) \[CWE\-670\] An unconditional 'break' within a loop\. [PokeInteractor\.cs 300](https://github.com/EliaFantini/RocketMan-a-VR-videogame-created-with-Unity/blob/12daf818cc1994b240e5d78e65f633b6e300dfcc/CS-440/Assets/Oculus/Interaction/Runtime/Scripts/Interaction/Models/Poke/PokeInteractor.cs#L300)\.

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

Так, в коде выше представлен цикл без операторов `continue` и безусловным `break` в конце, который явно является таким артефактом\. Следствием его наличия является серьёзная ошибка, из\-за которой у цикла всегда будет только одна итерация\.

### Обзор микрооптимизаций

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

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

В собственном коде RocketMan таких предупреждений оказалось не так уж много, всего 25\. Давайте рассмотрим некоторые из них\.

#### Выделение памяти при каждом обновлении кадра

```cpp
void Update()
{
  ....
  FindHoverObject(controllerPos, controllerRot);
  ....
}

void FindHoverObject(Vector3 controllerPos, Quaternion controllerRot)
{
  var objectsHit = Physics.RaycastAll(controllerPos, 
                                      controllerRot * Vector3.forward);
  ....
}
```

Предупреждение PVS\-Studio: [V4008](https://pvs-studio.ru/ru/docs/warnings/v4008/) Unity Engine\. Avoid using the 'Physics\.RaycastAll' memory allocation method in performance\-sensitive context inside 'FindHoverObject'\. Consider using the 'RaycastNonAlloc' non\-allocation method instead\. [ObjectManipulator\.cs 55](https://github.com/EliaFantini/RocketMan-a-VR-videogame-created-with-Unity/blob/12daf818cc1994b240e5d78e65f633b6e300dfcc/CS-440/Assets/Oculus/SampleFramework/Usage/Passthrough/Scripts/ObjectManipulator.cs#L55)\.

В этом примере `Physics.RaycastAll` вызывается при обновлении каждого кадра\. Но что же тут можно оптимизировать? Дело в том, что при каждом вызове `Physics.RaycastAll` \(несколько десятков раз в секунду\) выделяется память под новую коллекцию для возвращаемого результата\. Это приводит к учащению вызова GC, что в свою очередь замедляет работу приложения\. 

К счастью, у `Physics.RaycastAll` есть более оптимальная альтернатива — `Physics.RaycastNonAlloc`\. Разница между ними в том, что `Physics.RaycastNonAlloc` принимает в качестве аргумента ссылку на некоторый буфер, в который и записывает результат\. Сам буфер можно вынести в поле, инициализировать всего раз, а после перезаписывать, не освобождая и не выделяя повторно память\.

#### Вызов вычислительно дорогой функции при каждом обновлении

```cpp
void Update()
{
  ....
  if (grabObject)
  {
    ....
    ManipulateObject(grabObject, 
                     controllerPos, 
                     controllerRot);
    ....
  }
  ....
}
void ManipulateObject(....)
{
  ....
  if (obj.GetComponent<GrabObject>()) // <=
  {
     ....
     switch (obj.GetComponent<GrabObject>() // <=
                .objectManipulationType)
     ....
  }
  ....
}
```

Предупреждение PVS\-Studio: 

[V4005](https://pvs-studio.ru/ru/docs/warnings/v4005/) Expensive operation is performed inside the 'GetComponent' method\. Calling such method in performance\-sensitive context located inside the 'ManipulateObject' method can lead to decreased performance\. [ObjectManipulator\.cs 71](https://github.com/EliaFantini/RocketMan-a-VR-videogame-created-with-Unity/blob/12daf818cc1994b240e5d78e65f633b6e300dfcc/CS-440/Assets/Oculus/SampleFramework/Usage/Passthrough/Scripts/ObjectManipulator.cs#L71)\.

В этот раз анализатор обращает внимание на избыточные вызовы `obj.GetComponent<GrabObject>()` внутри метода `Update`\. `GetComponent` довольно дорогой с вычислительной точки зрения, а потому стоит избегать его использования внутри таких часто исполняемых методов, как `Update`\. 

Сложно придумать случай, когда без этого нельзя было бы обойтись, ведь компонент, получаемый с помощью `GetComponent`, можно один раз закэшировать в поле в таких методах, как `Start` и `Awake`\. А вместо того, чтобы регулярно добавлять/удалять компонент, можно включать и выключать его с помощью свойства `Behaviour.enabled`\.

#### Неоптимальное арифметическое выражение

```cpp
private void Update()
{
  ....
  if (referenceHand)
  {
    ....            
    CreateHandMesh(); // <=
  }
  ....
}

void CreateHandMesh()
{
  ....
  for (int i = 0; i < 5; i++)
  {
    ....
    AddKnuckleMesh(....); // <=
    AddKnuckleMesh(....); // <=
    ....
    AddKnuckleMesh(....); // <=
  }
  ....
}

void AddKnuckleMesh(....)
{
  ....
  Vector3 windingVec = rightVec;

  for (int i = 0; i < fanVerts * 2; i++)
  {
    ....
    Vector3 vertPos = handVertices[basePoint] + 
                      windingVec *       // <=
                      borderSize * 
                      handScale * 
                      (....);
    ....
  }
  ....
}
```

Предупреждение PVS\-Studio: 

[V4006](https://pvs-studio.ru/ru/docs/warnings/v4006/) Unity Engine\. Multiple operations between the 'windingVec' complex value and numeric values\. Prioritizing operations between numeric values can optimize execution time\. [HandMeshMask\.cs 70](https://github.com/EliaFantini/RocketMan-a-VR-videogame-created-with-Unity/blob/12daf818cc1994b240e5d78e65f633b6e300dfcc/CS-440/Assets/Oculus/SampleFramework/Usage/Passthrough/Scripts/HandMeshMask.cs#L70)\.

Посмотрите на выражение инициализации переменной `vertPos` в методе `AddKnuckleMesh`\. Оно представляет собой операцию сложения, один из операндов которой — цепочка операций умножения\. Именно эта цепочка и является местом в оптимизации, на которое указывает анализатор\. Что же в нём не так?

Обратите внимание, что самый первый множитель — переменная `windingVec` — имеет тип `Vector3`\. При умножении этого типа на число выполняется умножение каждого из его трёх компонентов на это число\. Это значит, что в ходе вычисления всей цепочки будет выполнено девять операций\. Это число можно уменьшить, если сначала перемножить между собой обычные числа, а уже результат умножить на вектор\. В этом случае будет выполнено уже шесть операций, а цепочка умножений будет выглядеть так:

```cpp
windingVec * (borderSize * handScale * (....))
```

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

* умножения выполняются внутри цикла, у которого в среднем, допустим, пять итераций \(9 \* 5 \= 45\);
* метод `AddKnuckleMesh` трижды вызывается внутри цикла с пятью итерациями в методе `CreateHandMesh` \(45 \* 3 \* 5 \= 675\);
* метод `CreateHandMesh` вызывается в методе `Update`, частота исполнения которого зависит от частоты кадров\. Допустим, в среднем 100 раз в секунду \(675 \* 100 \= 67500\);
* стоит также учесть, что вызов `CreateHandMesh` находится под условием\. Нельзя наверняка сказать, насколько сильно оно влияет на частоту исполнения кода внутри своего блока, поэтому просто допустим, что частота уменьшается в два раза \(67500 / 2 \= 33750\)\.

В итоге мы получили пусть и неточное, но достаточно наглядное представление о количестве операций умножения \(33750\), выполняемых за секунду только в рассматриваемом выражении\.

А теперь скорректируем это число с учётом оптимизации \(33750 / 9 \* 6 \= 22500\)\. Разница в 11250 вычислительных операций в секунду\. Неплохая оптимизация, правда?

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

Надеюсь, на примере анализа VR\-игры RocketMan мне удалось хотя бы отчасти продемонстрировать вам пользу статического анализатора кода в разработке на Unity\. Честно говоря, RocketMan — довольно небольшой проект, по\-настоящему же статический анализатор раскрывает себя на средних и больших кодовых базах\. 

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

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

До встречи в следующих статьях\!