С годами стоимость создания игр стала больше, вырос их масштаб, а следовательно, и их кодовая база. Разработчикам становится всё сложнее уследить за ошибками. А забагованная игра влечёт финансовые и репутационные убытки. Как же с этим может помочь статический анализ?
Мы часто пишем статьи про проверку проектов с разбором ошибок из них. Сегодняшняя статья также будет включать обзор ошибок в коде, однако главная тема — обсуждение насущных проблем в сфере разработки игр. В основном мы будем говорить про разработку под Unity, но многие моменты актуальны и для программистов из других сфер. Главными темами будут возрастающая стоимость игр, репутация компаний-игроделов и статический анализ.
Стоимость разработки игр с каждым годом растёт. Как следствие – дороже становятся и сами игры. Давайте разбираться, почему же так происходит.
В 2020 году произошло увеличение цены за AAA-тайтл на $10. Ранее цена была на уровне $60 и была сформирована ещё в 2000-x. Естественно, игровые студии понимают, что во многих странах покупательская способность людей не растёт, а в некоторых даже падает. Если ещё поднять стоимость игр, то их просто не будут покупать. В итоге возможны два варианта развития событий:
Так почему же из года в год бюджеты игр растут? Чтобы ответить на этот вопрос, нужно спросить себя: "А стал бы я покупать игру, которая ничем не отличается от прошлогодней?". Думаю, многие ответили бы "нет". Разработчики вынуждены делать игры всё масштабнее, чтобы удивлять игроков.
Вы можете задать логичный вопрос: "А как сильно возросла стоимость создания игр?". Для ответа на него предлагаю обратиться к статистике. Мне удалось найти график бюджетов игр до 2017 года. В этих данных исключены расходы на маркетинг, то есть отображена только стоимость разработки.
Из графика выше видно, что примерно каждые 10 лет стоимость разработки растёт в 10 раз. Причём в настоящее время цифры весьма внушительные. Кстати, есть интересный свежий пример.
Возьмём знаменитый Cyberpunk 2077, который имел примерный бюджет в $313 млн. Данная цифра содержит в себе расходы на продвижение игры. Обычно маркетинговые расходы меньше или равны расходам на разработку. Но даже если мы разделим общий бюджет пополам, то всё равно получим огромную сумму.
Стоит отметить, что игра вышла довольно сырой и имела много багов. Думаю, что её бюджет вырос уже довольно сильно, учитывая то, что игру продолжают доделывать уже 2 года и готовят к ней дополнения. Подобная деятельность называется пост-релизной поддержкой. Она необходима почти для любого крупного проекта, ведь какой смысл в игре, если после старта из неё уйдёт большинство игроков. Данный этап включает в себя создание нового контента и исправление багов, которые не получилось исправить на основном этапе разработки.
Кстати, о багах...
Вы можете вспомнить хотя бы одну игру с багами? Да во всех играх есть ошибки.
Конечно, эта фраза касается не только игр, но и любых других проектов. Невозможно создать большой и сложный продукт, который не содержит в себе ошибок. Над игрой, да и вообще над любой серьёзной программой, могут работать сотни людей. И все они не безупречные боги, им свойственно ошибаться.
Современные игры огромны, из-за этого искать ошибки в них в очень тяжело и дорого. Разработчикам становится всё тяжелее работать — приходится перерабатывать, а также жертвовать различными аспектами игры, чтобы закончить работу в срок.
Также несвоевременное исправление ошибок влияет на репутацию. Представьте, что ваша компания выпустила игру, в которой уйма багов. Люди так устроены, что в большей мере запоминают плохое. Не стоит растрачивать свою репутацию, которая так упорно зарабатывается, на ошибках, которых можно было легко избежать.
У современных игровых студий есть целый арсенал технологий для упрощения разработки. Одной из них является статический анализ. Данная методология поиска потенциальных ошибок в исходном коде способна помочь студиям и их разработчикам найти различные проблемы и дефекты безопасности в их программах. Зачем ждать, пока баг всплывёт у игрока, если его можно исправить ещё при написании кода?
Кто-то может спросить: "А как это коррелирует с бюджетом, ценами и прочим?" Естественно, что поиск ошибок происходит в рабочее время, которое нужно оплачивать. При использовании статического анализа поиск дефектов происходит гораздо быстрее. Следовательно, освободившееся время можно использовать для наполнения мира, создания новых механик и прочего.
Стоит заметить, что большинство ошибок, совершаемых разработчиками игр, не отличаются от ошибок разработчиков из других сфер деятельности. Это можно понять, например, по нашим проверкам Open Source игр. В этих проектах постоянно находятся подозрительные места. Поэтому статический анализ будет полезен всем программистам.
Одним из инструментов, выполняющих статический анализ, является PVS-Studio. Я, как человек, причастный к разработке данного продукта, расскажу, в чём его польза для проектов на Unity.
В PVS-Studio было несколько особых доработок анализа проектов, использующих этот игровой движок. К примеру, мы занимались добавлением информации о поведении методов популярных классов из Unity. Это помогает находить больше ошибок в коде. Кстати, этой теме посвящена отдельная статья: "Как анализатор PVS-Studio стал находить ещё больше ошибок в проектах на Unity".
Также PVS-Studio учитывает и другие особенности этого движка. Например, что Unity предоставляет необычную реализацию оператора '==' для типов, которые наследуются от класса UnityEngine.Object.
С помощью PVS-Studio удалось найти ряд странностей и в коде самого Unity. Данной теме посвящены 3 публикации: статья 2016 года, статья 2018 года и совсем свежая статья 2022 года.
Для начала у вас уже должен быть установлен анализатор и введена лицензия. Если же вы ещё этого не сделали, то в этом вам поможет данная страница.
Откройте ваш проект в Unity Hub. Далее нажмите: Assets -> Open C# Project. У вас откроется IDE, заданная в настройках редактора Unity. Она указывается здесь:
Я буду использовать Visual Studio 2022. Чтобы проанализировать проект в данной версии IDE, можно использовать пункт меню Extensions -> PVS-Studio -> Check -> Solution.
Более подробно почитать про использование анализатора вы можете здесь.
Было бы неправильно написать статью про то, что при разработке игр нужно использовать статический анализ, и не привести примеров найденных анализатором ошибок. К сожалению, многие большие и популярные игры имеют закрытый код. Таким образом, мы не сможем проверить качество кода таких игр, как Ori and the Blind Forest, Superhot, Beat Saber, Rust, Firewatch и других. Выход можно найти на просторах Open Source. Для анализа я взял одну простую игру на Unity — Card-Game-Simulator. Этого проекта будет более чем достаточно для демонстрации работы анализатора.
Пример 1
private void Update()
{
....
if (Inputs.IsSubmit && joinButton.interactable)
Join();
else if (Inputs.IsNew)
Host();
else if (Inputs.IsFocusBack)
roomIdIpInputField.ActivateInputField();
else if (Inputs.IsFocusNext)
passwordInputField.ActivateInputField();
else if (Inputs.IsPageVertical && !Inputs.IsPageVertical) // <=
ScrollPage(Inputs.IsPageDown);
else if (Inputs.IsPageHorizontal && !Inputs.WasPageHorizontal)
ToggleConnectionSource();
else if (Inputs.IsCancel)
Close();
}
V3022 Expression 'Inputs.IsPageVertical && !Inputs.IsPageVertical' is always false. Probably the '||' operator should be used here. LobbyMenu.cs 159
Анализатор сообщает о том, что выражение Inputs.IsPageVertical && !Inputs.IsPageVertical всегда ложно. IsPageVertical представляет из себя свойство, которое возвращает значение типа bool.
public static bool IsPageVertical =>
Math.Abs(Input.GetAxis(PageVertical)) > Tolerance;
Естественно, что не существует значения, при котором данное условие будет истинно. При этом чуть ниже имеется выражение с похожими свойствами. После взгляда на них становится понятно, что в указанном анализатором месте опечатка.
Скорее всего, условие должно выглядеть следующим образом:
Inputs.IsPageVertical && !Inputs.WasPageVertical
Пример 2
private bool DecodeDict()
{
while (neededBits > 0)
{
int dictByte = input.PeekBits(8);
if (dictByte < 0)
{
return false;
}
input.DropBits(8);
readAdler = (readAdler << 8) | dictByte;
neededBits -= 8;
}
return false;
}
V3009 It's odd that this method always returns one and the same value of 'false'. Inflater.cs 288
PVS-Studio указывает на то, что данный метод всегда возвращает false. Если мы взглянем на код, то увидим, что имеется только два return и оба возвращают false. Стоит отметить, что перед объявлением метода имеется комментарий:
/// <summary>
/// Decodes the dictionary checksum after the deflate header.
/// </summary>
/// <returns>
/// False if more input is needed.
/// </returns>
private bool DecodeDict()
Комментарий даёт понять, что метод DecodeDict возвращает false только в случае, если недостаточно входных данных. Фактически же метод всегда возвращает false :). Может быть и так, что метод действительно никогда не должен возвращать true, но выглядит это странно, а данный комментарий вводит в заблуждение.
Пример 3
public override int PixelWidth
{
get
{
#if (CORE_WITH_GDI || GDI) && !WPF
try
{
Lock.EnterGdiPlus();
return _gdiImage.Width;
}
finally { Lock.ExitGdiPlus(); }
#endif
#if GDI && WPF
int gdiWidth = _gdiImage.Width;
int wpfWidth = _wpfImage.PixelWidth;
Debug.Assert(gdiWidth == wpfWidth);
return wpfWidth;
#endif
#if WPF && !GDI
return _wpfImage.PixelWidth;
#endif
#if NETFX_CORE || UWP
return _wrtImage.PixelWidth;
#endif
#if __IOS__
return (int)_iosImage.CGImage.Width;
#endif
#if __ANDROID__
return _androidImage.Width;
#endif
#if PORTABLE
return PixelWidth; // <=
#endif
}
}
V3110 Possible infinite recursion inside 'PixelWidth' property. XBitmapSource.cs 97
Анализатор обнаружил потенциальную бесконечную рекурсию. Аксессор get содержит несколько директив препроцессора #if, и в последней из них свойство обращается само к себе. Если при сборке символ PORTABLE будет определён, а другие символы из директив — нет, возникнет бесконечная рекурсия.
Пример 4
public override bool Equals(object obj)
{
Selector selector = obj as Selector;
if (obj == null)
return false;
return _path == selector._path; ;
}
V3019 Possibly an incorrect variable is compared to null after type conversion using 'as' keyword. Check variables 'obj', 'selector'. PdfFormXObjectTable.cs 217
Анализатор обнаружил потенциальную ошибку, которая может привести к обращению по нулевой ссылке. Объект базового класса привели к производному с помощью as. В случае неудачного преобразования значению selector будет присвоен null. В данном случае на равенство null проверяется переменная базового типа — obj. Таким образом, при передаче в метод ссылки на объект типа Selector или нулевой ссылки, исключение выброшено не будет. В остальных случаях будет выбрасываться исключение. Обычно же ожидается, что метод в таком случае будет возвращать false. Скорее всего, стоит проверять на равенство null именно переменную selector.
Пример 5
internal PdfFormXObject(....): base(thisDocument)
{
....
XPdfForm pdfForm = form;
// Get import page
PdfPages importPages = importedObjectTable.ExternalDocument.Pages;
if ( pdfForm.PageNumber < 1
|| pdfForm.PageNumber > importPages.Count)
{
PSSR.ImportPageNumberOutOfRange(pdfForm.PageNumber,
importPages.Count,
form._path);
}
PdfPage importPage = importPages[pdfForm.PageNumber - 1];
....
}
V3010 The return value of function 'ImportPageNumberOutOfRange' is required to be utilized. PdfFormXObject.cs 99
Здесь возвращаемое значение метода ImportPageNumberOutOfRange не используется. Давайте взглянем на него:
public static string ImportPageNumberOutOfRange(....)
{
return String.Format("The page cannot be imported from document '{2}'," +
" because the page number is out of range. " +
"The specified page number is {0}," +
" but it must be in the range from 1 to {1}.", ....);
}
Видно, что метод возвращает сообщение об ошибке, которое никак не используется. Подобная оплошность может стать причиной затруднённого поиска причины неправильной работы программы. Скорее всего, при значениях pdfForm.PageNumber < 1 или pdfForm.PageNumber > importPages.Count выполнение кода не должно было идти дальше.
Резюмируя, можно сказать, что разрабатывать игры становится всё труднее и дороже. Поэтому разработчикам стоит использовать различные инструменты, которые помогут повысить качество кода и удешевить процесс поиска ошибок. Статический анализатор PVS-Studio может стать вашим верным товарищем при создании новой игры.
Мне было бы интересно узнать ваше мнение по данной теме. Быть может, вы являетесь разработчиком игры и уже используете статический анализ? В общем, приглашаю в комментарии для обсуждений.
Кстати, если у вас есть проекты на Unity, которые вы хотите, чтобы мы проверили, то для подобного у нас имеется страница на GitHub.
Вы также можете попробовать PVS-Studio на своём проекте. Возможно, это поможет обнаружить проблемы, которые укрылись от вашего взора во время ревью и тестирования.