Главная » Хабрахабр » Статический анализ в видеоигровой индустрии: топ-10 программных ошибок

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

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

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

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

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

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

  • Стоимость и сложность исправления ошибки возрастает со временем. Одно из основных преимуществ статического анализа — обнаружение ошибок на ранних этапах разработки (можно находить ошибку в момент написания кода). Следовательно, используя статический анализатор, можно упростить жизнь себе и коллегам, обнаруживая и исправляя многие ошибки до того, как они станут причиной головной боли.
  • С помощью статического анализа можно выявлять множество различных паттернов ошибок (copy-paste, опечатки, неправильное использование функций и т.д.).
  • Как правило, статический анализ хорош в выявлении тех проблем, которые плохо выявляются с помощью динамического анализа. Впрочем, справедливо и обратное утверждение.
  • Негативные моменты использования (например, ложные срабатывания), как правило, 'сглаживаются' командами-разработчиками серьёзных статических анализаторов за счёт различных функций подавления предупреждений (единичное, по паттерну и т.п.), отключения неактуальных диагностик, исключения из анализа файлов и директорий. Благодаря этому можно значительно уменьшить процент 'шума', правильно настроив анализатор. Мой коллега Андрей Карпов продемонстрировал в статье про проверку EFL Core Libraries, что в случае настройки анализатора количество ложных срабатываний составит всего 10-15%.

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

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

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

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

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

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

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

Джон Кармак, один из самых известных разработчиков в сфере видеоигровой индустрии, в своё время признал активное использование методики статического анализа одним из главных своих достижений как программиста: "The most important thing I have done as a programmer in recent years is to aggressively pursue static code analysis." В следующий раз, когда кто-то из ваших знакомых скажет, что статический анализ – инструмент для новичков, покажите ему эту цитату. Своё знакомство со статическим анализом Кармак описал в соответствующей статье, с которой я настоятельно рекомендую ознакомиться: как для мотивации, так и для общего развития.

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

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

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

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

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

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

Пока кто-то размышляет о том, стоит ли внедрять статический анализ в свой процесс разработки, некоторые команды уже вовсю его используют и извлекают пользу! Например, Rocksteady, Epic Games, ZeniMax Media, Oculus, Codemasters, Wargaming и другие (источник).

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

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

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

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

Источник: Ищем аномалии в X-Ray Engine

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

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

Предупреждение PVS-Studio: V530 The return value of function 'empty' is required to be utilized.

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

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

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

Источник: Долгожданная проверка CryEngine V

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

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 The 'BlendFactor[2]' variable is assigned values twice successively. Perhaps this is a mistake.

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

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

Источник: Единорог в космосе: проверяем исходный код 'Space Engineers'

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

public void Init(string cueName)
'", 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 The return value of function 'Format' is required to be utilized.
  • 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

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

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

Предупреждение PVS-Studio: V537 Consider reviewing the correctness of 'scale_x' item's usage.

Выглядит очень подозрительно и похоже на то, что во втором случае необходимо заменить выражение p->scale_x на p->scale_y. Происходит запись в переменные x и y, но в обоих присвоениях в выражении вычисления значения используется подвыражение p->scale_x.

Шестое место

Источник: Проверяем исходный C#-код Unity

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

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

Предупреждение PVS-Studio: V3063 A part of conditional expression is always true if it is evaluated: pageSize

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

Пятое место

Источник: Анализируем ошибки в открытых компонентах Unity3D

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

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 Possible null dereference. Consider inspecting 't.staticFieldBytes'.

Судя по коду, коллекция typeDescriptions может содержать такие элементы, у которых член staticFieldBytes может иметь значение null. Обратите внимание на лямбда-выражение, переданное в качестве аргумента методу Where. Вот только были перепутаны операторы, и вместо '&&' используется '&'. Именно поэтому, прежде чем обратиться к свойству Length, выполняется проверка, что staticFieldBytes != null. При использовании '&&' такой ситуации не возникло бы, так как, если staticFieldBytes == null, правая часть выражения не вычислялась бы. Это значит, что независимо от результата вычисления левого выражения (true \ false) правое выражение также будет вычисляться, из-за чего, если staticFieldBytes == null, при обращении к свойству Length будет сгенерировано исключение типа NullReferenceException.

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

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

Источник: Анализ исходного кода движка Godot

Пример: создаётся объект исключения, но никак не используется, так как разработчик забывает указать ключевое слово throw. Иногда встречаются интересные ошибки, связанные с пропущенными ключевыми словами. Пропущенное ключевое слово встретилось и в игровом движке Godot Engine. Встречаются такие ошибки и в C# проектах, и в C++ проектах.

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 Ownerless expression 'ie.action.pressed'.

Вот только один раз выражение возврата написали нормально — return ie.action.action;, а во второй раз забыли оператор return, из-за чего нужное значение не будет возвращено, а метод продолжит своё выполнение. В приведённом фрагменте кода видно, что в зависимости от значений ie.type и str хотели вернуть определённое значение типа Variant.

Третье место

Источник: PVS-Studio: анализируем код Doom 3

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

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

Предупреждение PVS-Studio: V575 The 'memset' function processes '0' elements. Inspect the third argument.

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

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

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

Второе место

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

Источник: Ищем ошибки в игровом движке Xenko

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 Parameter 'height' is not utilized inside method's body.

Если посмотреть на его сигнатуру, можно увидеть, что второй, третий и четвёртый параметры имеют названия width, height и depth соответственно. Ошибку допустили, передавая аргументы методу CreateDescription. Подозрительно? При вызове же передаются аргументы width, width и depth. Вот и анализатор тоже счёл это место достаточно подозрительным, чтобы выдать предупреждение.

Первое место

Источник: Долгожданная проверка Unreal Engine 4

Как и в случае со статьёй "Toп 10 ошибок в C++ проектах за 2017 год", увидев приведённую ниже ошибку, я сразу понял, кто должен возглавить список самых интересных ошибок. Возглавляет наш сегодняшний «Топ-10» ошибка из игрового движка Unreal Engine.

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 The '&' operator is applied to bool type value. You've probably forgotten to include parentheses or intended to use the '&&' operator.

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

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

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

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

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

Заключение

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

Static Analysis in Video Game Development: Top 10 Software Bugs Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Sergey Vasiliev.


Оставить комментарий

Ваш email нигде не будет показан
Обязательные для заполнения поля помечены *

*

x

Ещё Hi-Tech Интересное!

Вышла Oracle Database 18c XE

Можно открывать шампанское и закатывать вечеринку — спустя более, чем 7 лет с момента выпуска предыдущего релиза, для скачивания наконец доступна свежайшая Oracle Database 18c XE. Свершилось! Пока только для Linux x64, но версии для других платформ, также как и ...

Palm возрождается из пепла с новым гаджетом

Ее разработки служили примером для других компаний, в свое время поклонников продукции Palm было не счесть. Компания Palm навсегда вписана в историю современного «железа». Но устройства компании и ее программное обеспечение до сих пор популярны у гиков. Как и многим ...