﻿# Статический анализ в видеоигровой индустрии: топ\-10 программных ошибок

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

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

## Почему вам следует использовать статический анализ

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

Почему статический анализ полезен в сфере разработки программного обеспечения в целом?

Есть несколько основных причин:

* Стоимость и сложность исправления ошибки возрастает со временем\. Одно из основных преимуществ статического анализа \- обнаружение ошибок на ранних этапах разработки \(можно находить ошибку в момент написания кода\)\. Следовательно, используя статический анализатор, можно упростить жизнь себе и коллегам, обнаруживая и исправляя многие ошибки до того, как они станут причиной головной боли\.
* С помощью статического анализа можно выявлять множество различных паттернов ошибок \(copy\-paste, опечатки, неправильное использование функций и т\.д\.\)\.
* Как правило, статический анализ хорош в выявлении тех проблем, которые плохо выявляются с помощью динамического анализа\. Впрочем, справедливо и обратное утверждение\.
* Негативные моменты использования \(например, ложные срабатывания\), как правило, 'сглаживаются' командами\-разработчиками серьёзных статических анализаторов за счёт различных функций подавления предупреждений \(единичное, по паттерну и т\.п\.\), отключения неактуальных диагностик, исключения из анализа файлов и директорий\. Благодаря этому можно значительно уменьшить процент 'шума', правильно настроив анализатор\. Мой коллега Андрей Карпов продемонстрировал в [статье про проверку EFL Core Libraries](https://pvs-studio.ru/ru/blog/posts/cpp/0523/), что в случае настройки анализатора количество ложных срабатываний составит всего 10\-15%\.

Но так или иначе, это всё теория, а вам, наверняка, интересны более практические примеры\. Что же, они представлены ниже\.

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

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

Если вы дочитали до этого момента, что\-то рассказывать вам о том, что такое Unreal Engine или что за компания такая \- Epic Games \- не стоит, и, если эти ребята не пользуются у вас авторитетом \- напишите мне, кто тогда пользуется\.

Команда PVS\-Studio несколько раз сотрудничала с командой Epic Games, помогая им внедрять и использовать статический анализ в проекте Unreal Engine и исправлять ошибки \\ ложные срабатывания, обнаруженные анализатором\. Уверен, это был интересный и полезный опыт для обеих сторон\.

Одним из следствий стало добавление в Unreal Engine специального флага, который позволил удобно интегрировать статический анализ в сборочную систему Unreal Engine проектов\.

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

### Джон Кармак о статическом анализе

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

Джон Кармак, один из самых известных разработчиков в сфере видеоигровой индустрии, в своё время признал активное использование методики статического анализа одним из главных своих достижений как программиста: "_The most important thing I have done as a programmer in recent years is to aggressively pursue static code analysis\._" В следующий раз, когда кто\-то из ваших знакомых скажет, что статический анализ – инструмент для новичков, покажите ему эту цитату\. Своё знакомство со статическим анализом Кармак описал в [соответствующей статье](https://www.gamasutra.com/view/news/128836/InDepth_Static_Code_Analysis.php), с которой я настоятельно рекомендую ознакомиться: как для мотивации, так и для общего развития\. 

### Ошибки, найденные с помощью статического анализа в играх и игровых движках

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

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

В выигрыше остаются все:

* Разработчики проектов получают уведомления об ошибках и могут их поправить\. В идеале, конечно, подход должен быть иным, нежели исправление ошибок по логу \\ статье – стоит самостоятельно проверить анализатором проект и изучить выданные предупреждения\. Это важно хотя бы по той причине, что авторы статей могут что\-то упускать из виду или непреднамеренно делать акцент на тех ошибках, которые не критичны для проекта\.
* Разработчики анализатора могут улучшить инструмент по результатам анализа, а также демонстрируют возможности анализатора по обнаружению ошибок\.
* Читатели видят, какие паттерны ошибок допускаются, набираются опыта, знакомятся с методикой статического анализа\.

В общем – чем не доказательство эффективности и пользы?

### Команды, уже использующие статический анализ

Пока кто\-то размышляет о том, стоит ли внедрять статический анализ в свой процесс разработки, некоторые команды уже вовсю его используют и извлекают пользу\! Например, Rocksteady, Epic Games, ZeniMax Media, Oculus, Codemasters, Wargaming и другие \([источник](https://pvs-studio.ru/ru/about/customers/)\)\.



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

## Топ\-10 ошибок из программного кода в видеоигровой индустрии

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

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

**Десятое место**

Источник: [Ищем аномалии в X\-Ray Engine](https://pvs-studio.ru/ru/blog/posts/cpp/0405/)

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

На десятом месте расположилась ошибка из игрового движка X\-Ray Engine, использовавшегося в играх S\.T\.A\.L\.K\.E\.R\. Те, кто играл в игры из этой серии, наверняка помнят много забавных \(и не очень\) багов, возникавших во время игрового процесса\. Особенно это касается S\.T\.A\.L\.K\.E\.R\.: Clear Sky, в которую без патчей играть было вообще нереально \(до сих пор помню баг, 'убивавший' все сохранения\)\. Как показал анализ игрового движка \- баги в коде действительно есть, и их достаточно много\. Ниже представлен один из них\.

```cpp
BOOL CActor::net_Spawn(CSE_Abstract* DC)
{
  ....
  m_States.empty();
  ....
}
```

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

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

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

**Девятое место**

Источник: [Долгожданная проверка CryEngine V](https://pvs-studio.ru/ru/blog/posts/cpp/0417/)

Продолжаем раскрывать тему ошибок в игровых движках\. На девятом месте \- код из CryEngine V\. В играх на этом движке я не замечал столько багов, сколько в играх на X\-Ray Engine, но 'вскрытие' показало, что подозрительных фрагментов кода движок содержит немало\.

```cpp
void CCryDXGLDeviceContext::
OMGetBlendState(...., FLOAT BlendFactor[4], ....)
{
  CCryDXGLBlendState::ToInterface(ppBlendState, m_spBlendState);
  if ((*ppBlendState) != NULL)
    (*ppBlendState)->AddRef();
  BlendFactor[0] = m_auBlendFactor[0];
  BlendFactor[1] = m_auBlendFactor[1];
  BlendFactor[2] = m_auBlendFactor[2];
  BlendFactor[2] = m_auBlendFactor[3];
  *pSampleMask = m_uSampleMask;
}
```

**Предупреждение PVS\-Studio**: [V519](https://pvs-studio.ru/ru/docs/warnings/v519/) The 'BlendFactor\[2\]' variable is assigned values twice successively\. Perhaps this is a mistake\.

Как неоднократно упоминалось в наших статьях, никто не застрахован от опечаток\. На практике также неоднократно подтверждалось, что статический анализ отлично справляется с обнаружением copy\-paste и опечаток\. Переписывали значения из массива _m\_auBlendFactor_ в _BlendFactor_, но случайно опечатались и два раза написали _BlendFactor\[2\]_\. Из\-за этой путаницы в _BlendFactor\[2\]_ будет записано значение из _m\_auBlendFactor\[3\]_, а в _BlendFactor\[3\]_ значение не поменяется\.

**Восьмое место**

Источник: [Единорог в космосе: проверяем исходный код 'Space Engineers'](https://pvs-studio.ru/ru/blog/posts/csharp/0376/)

Немного изменим курс и перейдём от C\+\+ к C\#\. Перед нами \- код из проекта Space Engineers \- игры\-'песочницы' о строительстве и обслуживании космических структур\. Сам я в игру не играл, но как сказал один из комментаторов статьи: "_Я не сильно удивлён результатами :\)_"\. Что ж, интересные ошибки действительно удалось найти, ниже \- две из них\.

```cpp
public void Init(string cueName)
{
  ....
  if (m_arcade.Hash    == MyStringHash.NullOrEmpty && 
      m_realistic.Hash == MyStringHash.NullOrEmpty)
    MySandboxGame.Log.WriteLine(string.Format(
      "Could not find any sound for '{0}'", cueName));
  else
  {
    if (m_arcade.IsNull)
      string.Format(
        "Could not find arcade sound for '{0}'", cueName);
    if (m_realistic.IsNull)
      string.Format(
        "Could not find realistic sound for '{0}'", cueName);
  }
}
```

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

* [V3010](https://pvs-studio.ru/ru/docs/warnings/v3010/) The return value of function 'Format' is required to be utilized\. 
* [V3010](https://pvs-studio.ru/ru/docs/warnings/v3010/) The return value of function 'Format' is required to be utilized\. 

Как видите, ошибки игнорирования возвращаемых значений методов встречаются не только в коде на C\+\+, но и на C\#\. Метод _String\.Format_ составляет результирующую строку на основе строки формата и объектов для форматирования, после чего возвращает результат как возвращаемое значение метода\. В коде выше в _else_\-ветви есть два вызова _string\.Format_, но их возвращаемые значения не используются\. Скорее всего, необходимо было залогировать эти сообщения по аналогии с тем, как это сделано в _then_\-ветви оператора _if_, используя метод _MySandboxGame\.Log\.WriteLine_\.

**Седьмое место**

Источник: [Проверка проекта Quake III Arena GPL](https://pvs-studio.ru/ru/blog/posts/cpp/0130/)

Я уже говорил, что статический анализ хорошо обнаруживает опечатки? Пример ниже \- ещё одно тому подтверждение\.

```cpp
void Terrain_AddMovePoint(....) {
  ....
  x = ( v[ 0 ] - p->origin[ 0 ] ) / p->scale_x;
  y = ( v[ 1 ] - p->origin[ 1 ] ) / p->scale_x;
  ....
}
```

**Предупреждение PVS\-Studio**: [V537](https://pvs-studio.ru/ru/docs/warnings/v537/) Consider reviewing the correctness of 'scale\_x' item's usage\.

Происходит запись в переменные _x_ и _y_, но в обоих присвоениях в выражении вычисления значения используется подвыражение _p\-\>scale\_x_\. Выглядит очень подозрительно и похоже на то, что во втором случае необходимо заменить выражение _p\-\>scale\_x_ на _p\-\>scale\_y_\.

**Шестое место**

Источник: [Проверяем исходный C\#\-код Unity](https://pvs-studio.ru/ru/blog/posts/csharp/0568/)

Компания Unity Technologies не так давно открыла исходный код собственного игрового движка \- Unity\. Мы не могли пройти мимо такого события и при анализе нашли немало интересных мест\. Ниже \- одно из них\. 

```cpp
public override bool IsValid()
{
  ....
  return base.IsValid()
    && (pageSize >= 1 || pageSize <= 1000)
    && totalFilters <= 10;
}
```

**Предупреждение PVS\-Studio**: [V3063](https://pvs-studio.ru/ru/docs/warnings/v3063/) A part of conditional expression is always true if it is evaluated: pageSize <\= 1000\.

Имеем неверную проверку диапазона для _pageSize_\. Скорее всего планировалось проверить, что значение _pageSize_ лежит в пределах \[1; 1000\]\. Однако допущена досадная опечатка: вместо оператора '&&' программист случайно написал '\|\|'\. Получается, что подвыражение на самом деле ничего не проверяет\.

**Пятое место**

Источник: [Анализируем ошибки в открытых компонентах Unity3D](https://pvs-studio.ru/ru/blog/posts/csharp/0423/)

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

```cpp
public static CrawledMemorySnapshot Unpack(....)
{
  ....
  var result = new CrawledMemorySnapshot
  {
    ....
    staticFields = packedSnapshot.typeDescriptions
                                 .Where(t =>
                                        t.staticFieldBytes != null &
                                        t.staticFieldBytes.Length > 0)
                                 .Select(t => UnpackStaticFields(t))
                                 .ToArray()
    ....
  };
  ....
}
```

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

Обратите внимание на лямбда\-выражение, переданное в качестве аргумента методу _Where_\. Судя по коду, коллекция _typeDescriptions_ может содержать такие элементы, у которых член _staticFieldBytes_ может иметь значение _null_\. Именно поэтому, прежде чем обратиться к свойству _Length_, выполняется проверка, что _staticFieldBytes \!\= null_\. Вот только были перепутаны операторы, и вместо '&&' используется '&'\. Это значит, что независимо от результата вычисления левого выражения \(_true_ \\ _false_\) правое выражение также будет вычисляться, из\-за чего, если _staticFieldBytes \=\= null_, при обращении к свойству _Length_ будет сгенерировано исключение типа _NullReferenceException_\. При использовании '&&' такой ситуации не возникло бы, так как, если _staticFieldBytes \=\= null_, правая часть выражения не вычислялась бы\.

Несмотря на то, что Unity – единственный движок, дважды попавший в данный "Топ\-10", энтузиастам это не мешает создавать на нём много замечательных игр\. В том числе – про борьбу с 'багами'\.

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

**Четвертое место**

Источник: [Анализ исходного кода движка Godot](https://pvs-studio.ru/ru/blog/posts/cpp/0321/)

Иногда встречаются интересные ошибки, связанные с пропущенными ключевыми словами\. Пример: создаётся объект исключения, но никак не используется, так как разработчик забывает указать ключевое слово _throw_\. Встречаются такие [ошибки и в C\# проектах](https://pvs-studio.ru/ru/blog/examples/v3006/), и в [C\+\+ проектах](https://pvs-studio.ru/ru/blog/examples/v596/)\. Пропущенное ключевое слово встретилось и в игровом движке Godot Engine\.

```cpp
Variant Variant::get(const Variant& p_index, bool *r_valid) const 
{
  ....
  if (ie.type == InputEvent::ACTION) 
  {
    if (str =="action") 
    {
      valid=true;
      return ie.action.action;
    }
    else if (str == "pressed") 
    {
      valid=true;
      ie.action.pressed;
    }
  }
  ....
}
```

**Предупреждение PVS\-Studio**: [V607](https://pvs-studio.ru/ru/docs/warnings/v607/) Ownerless expression 'ie\.action\.pressed'\.

В приведённом фрагменте кода видно, что в зависимости от значений _ie\.type_ и _str_ хотели вернуть определённое значение типа _Variant_\. Вот только один раз выражение возврата написали нормально \- _return ie\.action\.action;_, а во второй раз забыли оператор _return_, из\-за чего нужное значение не будет возвращено, а метод продолжит своё выполнение\.

**Третье место**

Источник: [PVS\-Studio: анализируем код Doom 3](https://pvs-studio.ru/ru/blog/posts/cpp/0120/)

Вот мы и подобрались к "Топ\-3"\. Замыкает его небольшой фрагмент исходного кода из игры Doom 3\. Как я говорил выше, то, что со стороны ошибка выглядит просто и может быть непонятно, как можно так ошибиться, ещё ничего не значит \- на практике каких типов ошибок только не встретишь\.\.\.

```cpp
void Sys_GetCurrentMemoryStatus( sysMemoryStats_t &stats ) {
  ....
  memset( &statex, sizeof( statex ), 0 );
  ....
}
```

**Предупреждение PVS\-Studio**: [V575](https://pvs-studio.ru/ru/docs/warnings/v575/) The 'memset' function processes '0' elements\. Inspect the third argument\.

Чтобы лучше понять проблему, стоит вспомнить сигнатуру функции _memset_:

```cpp
void* memset(void* dest, int ch, size_t count);
```

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

**Второе место**

Второе место заняла ошибка, найденная в C\# коде игрового движка Xenko\.

Источник: [Ищем ошибки в игровом движке Xenko](https://pvs-studio.ru/ru/blog/posts/csharp/0379/)

```cpp
private static ImageDescription 
CreateDescription(TextureDimension dimension, 
                  int width, int height, int depth, ....) { .... }

public static Image New3D(int width, int height, int depth, ....)
{
    return new Image(CreateDescription(TextureDimension.Texture3D,  
                                       width, width, depth,  
                                       mipMapCount, format, 1), 
                     dataPointer, 0, null, false);
}
```

**Предупреждение PVS\-Studio**: [V3065](https://pvs-studio.ru/ru/docs/warnings/v3065/) Parameter 'height' is not utilized inside method's body\.

Ошибку допустили, передавая аргументы методу _CreateDescription_\. Если посмотреть на его сигнатуру, можно увидеть, что второй, третий и четвёртый параметры имеют названия _width_, _height_ и _depth_ соответственно\. При вызове же передаются аргументы _width_, _width_ и _depth_\. Подозрительно? Вот и анализатор тоже счёл это место достаточно подозрительным, чтобы выдать предупреждение\.

**Первое место**

Источник: [Долгожданная проверка Unreal Engine 4](https://pvs-studio.ru/ru/blog/posts/cpp/0249/)

Возглавляет наш сегодняшний "Топ\-10" ошибка из игрового движка Unreal Engine\. Как и в случае со статьёй "[Toп 10 ошибок в C\+\+ проектах за 2017 год](https://pvs-studio.ru/ru/blog/posts/cpp/0565/)", увидев приведённую ниже ошибку, я сразу понял, кто должен возглавить список самых интересных ошибок\.

```cpp
bool VertInfluencedByActiveBone(
  FParticleEmitterInstance* Owner,
  USkeletalMeshComponent* InSkelMeshComponent,
  int32 InVertexIndex,
  int32* OutBoneIndex = NULL);

void UParticleModuleLocationSkelVertSurface::Spawn(....)
{
  ....
  int32 BoneIndex1, BoneIndex2, BoneIndex3;
  BoneIndex1 = BoneIndex2 = BoneIndex3 = INDEX_NONE;

  if(!VertInfluencedByActiveBone(
        Owner, SourceComponent, VertIndex[0], &BoneIndex1) &&
     !VertInfluencedByActiveBone(
        Owner, SourceComponent, VertIndex[1], &BoneIndex2) && 
     !VertInfluencedByActiveBone(
        Owner, SourceComponent, VertIndex[2]) &BoneIndex3)
  {
  ....
}
```

**Предупреждение PVS\-Studio**: [V564](https://pvs-studio.ru/ru/docs/warnings/v564/) The '&' operator is applied to bool type value\. You've probably forgotten to include parentheses or intended to use the '&&' operator\.

Не удивлюсь, если, сопоставляя предупреждение анализатора и приведённый код, вы задались вопросом: "А где здесь, собственно, использование '&' вместо '&&'?"\. Но, если обратить внимание, что последний параметр функции _VertInfluencedByActiveBone_ имеет значение по умолчанию, и упростить условное выражение оператора _if_, всё станет более понятно:

```cpp
if (!foo(....) && !foo(....) && !foo(....) & arg)
```

Обратите особое внимание на последнее подвыражение:

```cpp
!VertInfluencedByActiveBone(Owner, SourceComponent, VertIndex[2])  
&BoneIndex3
```

Параметр со значением по умолчанию сыграл злую шутку \- если бы значения по умолчанию не было, данный код бы просто не скомпилировался\. Но так как оно есть, всё успешно компилируется, а ошибка не менее успешно маскируется\. Вот это подозрительное место и заметил анализатор \- инфиксная операция '&', в которой левый операнд имеет тип _bool_, а правый \- _int32_\.

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

![0570_Static_Analysis_And_Gamedev_ru/image8.png](https://import.viva64.com/docx/blog/0570_Static_Analysis_And_Gamedev_ru/image8.png)

Надеюсь, мне удалось показать, что статический анализ будет полезен при разработке игр и игровых движков и является ещё одним ответом на вопрос о повышении качества кода \(и конечного продукта, как следствие\)\. Если вы занимаетесь разработкой в сфере видеоигровой индустрии, то просто обязаны рассказать коллегам о статическом анализе и показать эту статью\. Думаете с чего начать? Начните с [PVS\-Studio](https://pvs-studio.ru/ru/pvs-studio/download/)\.