- PVSM.RU - https://www.pvsm.ru -
Игры — одни из самых массовых продуктов среди программного обеспечения. Это огромная индустрия, в которой появился новый игровой движок — Amazon Lumberyard. Проект ещё находится в статусе беты и у него есть время, чтобы исправить ошибки, повысить качество кода. Разработчикам движка предстоит проделать много работы, чтобы в ближайшее время не разочаровать миллионы игроманов и разработчиков игр.
Amazon Lumberyard [1] — бесплатный кроссплатформенный игровой движок класса AAA, разработанный компанией Amazon и основанный на архитектуре движка CryEngine [2], который был лицензирован у компании Crytek в 2015 году. Кстати, анализ CryEngine уже дважды проводился мною в августе 2016 [3] и апреле 2017 [4]. При этом я вынужден отметить, что код по прошествии года стал только хуже. И вот на днях я решил посмотреть, что сделал Amazon на основе этого игрового движка. Над окружением они очень хорошо поработали. Документация для разработчиков и софт для развёртывания рабочего окружения сделаны очень круто и на высоком уровне. Но с кодом снова беда! Я надеюсь, что у Amazon намного больше ресурсов для работы с проектом, и они всё-таки уделят внимание качеству кода. Этим обзором я надеюсь обратить внимание разработчиков на качество кода и подтолкнуть к новому подходу в разработке этого игрового движка. Текущее состояние кода оказалось в таком плачевном состоянии, что я несколько раз менял название статьи и перерисовывал титульную картинку по мере просмотра отчёта с результатами анализа. Первая версия картинки была менее эмоциональной:
Анализировались исходники Amazon Lumberyard последней доступной версии 1.14.0.1. Исходный код взят из репозитория на Github [5]. Одной из первых игр на движке Lumberyard должна стать Star Citizen [6]. Потенциальных игроков, которые её ждут, тоже приглашаю взглянуть, что на данный момент находится «под капотом» игры.
В качестве статического анализатора кода использовался PVS-Studio [7]. Он доступен для Windows, Linux и macOS. Т.е. для анализа кроссплатформенного проекта есть даже из чего выбрать для более комфортной работы. Кроме C и C++ поддерживается анализ проектов на языке C#. В планах Java [8]. На перечисленных языках написано подавляющее большинство кода в мире (не без ошибок, конечно же), так что пробуйте анализатор PVS-Studio на своём проекте, узнаете много интересного ;-).
В качестве сборочной системы Lumberyard используется WAF, которая был и в CryEngine. Специального способа для интеграции с этой сборочной системой у анализатора нет. Я решил поработать с проектом на Windows и выбрал такой способ запуска анализа: система мониторинга компиляции [9]. Проектный файл для Visual Studio является автогенерируемым. Им можно пользоваться для сборки проекта и просмотра отчёта анализатора.
Список команд для анализа выглядит примерно так:
cd /path/to/lumberyard/dev
lmbr_waf.bat ...
CLMonitor.exe monitor
MSBuild.exe ... LumberyardSDK_vs15.sln ...
CLMonitor.exe analyze --log /path/to/report.plog
Отчёт, как я уже говорил, можно просматривать в Visual Studio.
Amazon Lumberyard позиционирует себя как кроссплатформенный игровой движок. Продвигать проект в массы с такой фичей легко, а вот поддерживать очень трудно. Одно из предупреждений PVS-Studio было выдано на фрагмент кода, где программист Игорь боролся с компилятором Qualcomm. Возможно, он решил свою задачу, но оставил крайне подозрительный код. Я решил оформить его картинкой.
V523 [10] The 'then' statement is equivalent to the 'else' statement. toglsloperand.c 700
Тут выполняется одинаковый код, независимо от вычисленного условия. На фоне оставленных комментариев такое решение выглядит подозрительно.
В целом по проекту это не единственное место, где условия нужно упростить для наглядности, либо исправить настоящую ошибку. Вот список таких мест:
Анализатор нашёл такой забавный код:
V709 [11] CWE-682 Suspicious comparison found: 'a == b == c'. Remember that 'a == b == c' is not equal to 'a == b && b == c'. toglslinstruction.c 564
void CallBinaryOp(....)
{
....
uint32_t src1SwizCount = GetNumSwizzleElements(....);
uint32_t src0SwizCount = GetNumSwizzleElements(....);
uint32_t dstSwizCount = GetNumSwizzleElements(....);
....
if (src1SwizCount == src0SwizCount == dstSwizCount) // <=
{
....
}
....
}
В C++ такой код, к сожалению, компилируется успешно, но выполняется он совсем не так, как может показаться. Выражения в C++ вычисляются в порядке приоритета, выполняя неявные касты, если это необходимо.
Такая проверка была бы правильной, например, в языке Python. Но в этой ситуации разработчик «выстрелил себе в ногу».
Ещё 3 контрольных выстрела:
Речь пойдёт о предупреждении V501 [12] — первой диагностике общего назначения в PVS-Studio. Ошибок, найденных только одной этой диагностикой, может быть достаточно для написания статьи. И проект Amazon Lumberyard отлично это демонстрирует.
К сожалению, долго просматривать однотипные ошибки скучно, поэтому в этом разделе прокомментирую только пару фрагментов кода, а остальное предупреждения приведу списком.
V501 [12] There are identical sub-expressions to the left and to the right of the '||' operator: hotX < 0 || hotX < 0 editorutils.cpp 166
QCursor CMFCUtils::LoadCursor(....)
{
....
if (!pm.isNull() && (hotX < 0 || hotX < 0))
{
QFile f(path);
f.open(QFile::ReadOnly);
QDataStream stream(&f);
stream.setByteOrder(QDataStream::LittleEndian);
f.read(10);
quint16 x;
stream >> x;
hotX = x;
stream >> x;
hotY = x;
}
....
}
В условии не хватает переменной hotY. Классическая опечатка.
V501 [12] There are identical sub-expressions 'sp.m_pTexture == m_pTexture' to the left and to the right of the '&&' operator. shadercomponents.h 487
V501 [12] There are identical sub-expressions 'sp.m_eCGTextureType == m_eCGTextureType' to the left and to the right of the '&&' operator. shadercomponents.h 487
bool operator != (const SCGTexture& sp) const
{
if (sp.m_RegisterOffset == m_RegisterOffset &&
sp.m_Name == m_Name &&
sp.m_pTexture == m_pTexture && // <= 1
sp.m_RegisterCount == m_RegisterCount &&
sp.m_eCGTextureType == m_eCGTextureType && // <= 2
sp.m_BindingSlot == m_BindingSlot &&
sp.m_Flags == m_Flags &&
sp.m_pAnimInfo == m_pAnimInfo &&
sp.m_pTexture == m_pTexture && // <= 1
sp.m_eCGTextureType == m_eCGTextureType && // <= 2
sp.m_bSRGBLookup == m_bSRGBLookup &&
sp.m_bGlobal == m_bGlobal)
{
return false;
}
return true;
}
В этом фрагменте найдено сразу две копипасты. Для наглядности подрисовал стрелочки.
V501 [12] There are identical sub-expressions to the left and to the right of the '==' operator: pSrc.GetLen() == pSrc.GetLen() fbxpropertytypes.h 978
inline bool FbxTypeCopy(FbxBlob& pDst, const FbxString& pSrc)
{
bool lCastable = pSrc.GetLen() == pSrc.GetLen();
FBX_ASSERT( lCastable );
if( lCastable )
pDst.Assign(pSrc.Buffer(), (int)pSrc.GetLen());
return lCastable;
}
Тут я хочу передать привет разработчикам из AUTODESK [13]. Эта ошибка из их библиотеки FBX SDK [14]. Перепутали переменные pSrc и pDst. Я думаю, кроме Lumberyard полно и других пользователей, чьи проекты зависят от кода с этой ошибкой.
V501 [12] There are identical sub-expressions to the left and to the right of the '&&' operator: pTS->pRT_ALD_1 && pTS->pRT_ALD_1 d3d_svo.cpp 857
void CSvoRenderer::ConeTracePass(SSvoTargetsSet* pTS)
{
....
if (pTS->pRT_ALD_1 && pTS->pRT_ALD_1)
{
static int nPrevWidth = 0;
if (....)
{
....
}
else
{
pTS->pRT_ALD_1->Apply(10, m_nTexStateLinear);
pTS->pRT_RGB_1->Apply(11, m_nTexStateLinear);
}
}
....
}
Вернёмся к коду Lumberyard. В условии проверяется один и тот же указатель pTS->pRT_ALD_1. Один из них должен был быть pTS->pRT_RGB_1. Возможно, даже после объяснения не сразу можно увидеть разницу, а она есть: разница в коротеньких подстроках ALD и RGB. Если вам скажут, что ручного Code Review достаточно, то покажите этот пример.
А если этого примера недостаточно, то есть ещё 5 похожих:
И как я обещал, вот вам список оставшихся предупреждений V501 [12] без примеров кода:
Есть вторая по крутости диагностика в PVS-Studio — V502 [15]. Эта диагностика старше некоторых новых языков программирования, в которых уже нельзя допустить такую ошибку. А для С++ эта ошибка будет актуальная, пожалуй, всегда.
Начнём с маленького простого примера.
V502 [15] Perhaps the '?:' operator works in a different way than it was expected. The '?:' operator has a lower priority than the '+' operator. zipencryptor.cpp 217
bool ZipEncryptor::ParseKey(....)
{
....
size_t pos = i * 2 + (v1 == 0xff) ? 1 : 2;
RCLogError("....", pos);
return false;
....
}
Операция сложения имеет более высокий приоритет, чем тернарный оператор. Следовательно, у этого выражения совсем другая логика вычисления, чем предполагал автор.
Исправить ошибку можно таким образом:
size_t pos = i * 2 + (v1 == 0xff ? 1 : 2);
V502 [15] Perhaps the '?:' operator works in a different way than it was expected. The '?:' operator has a lower priority than the '-' operator. 3dengine.cpp 1898
float C3DEngine::GetDistanceToSectorWithWater()
{
....
return (bCameraInTerrainBounds && (m_pTerrain &&
m_pTerrain->GetDistanceToSectorWithWater() > 0.1f)) ?
m_pTerrain->GetDistanceToSectorWithWater() :
max(camPostion.z - OceanToggle::IsActive() ?
OceanRequest::GetOceanLevel() : GetWaterLevel(), 0.1f);
}
А вот пример кода, где работают с позицией камеры. Пример сложный для восприятия глазами и в нём присутствует ошибка. Для публикации форматирование кода было изменено, но в исходном файле этот код ещё более нечитабелен.
Ошибка присутствует в этой подстроке:
camPostion.z - OceanToggle::IsActive() ? .... : ....
Если камера в Вашей игре вдруг начала вести себя неадекватно, то знайте, разработчики сэкономили на статическом анализе кода :D.
Другие примеры с похожими предупреждениями:
Amazon Lumberyard основан на коде CryEngine [2]. Причём не на самой лучшей его версии. Такой вывод я сделал, посмотрев отчёт анализатора. Некоторые найденные ошибки уже исправлены в последней версии CryEngine по двум моим обзорам кода, но до сих пор присутствуют в коде Lumberyard. Также за последний год анализатор был значительно улучшен и удалось найти дополнительные ошибки, которые теперь присутствуют в двух игровых движках. Но с Lumberyard всё же ситуация похуже. В наследство Amazon по сути достался весь технический долг CryEngine. Ну а свой собственный технический долг, само собой, в каждой компании появляется своими силами :).
В этом разделе я приведу всего парочку ошибок, которые были исправлены в последней версии CryEngine, и теперь остались только проблемой проекта Lumberyard.
V519 [16] The 'BlendFactor[2]' variable is assigned values twice successively. Perhaps this is a mistake. Check lines: 1283, 1284. ccrydxgldevicecontext.cpp 1284
Примерно такие эмоции будут испытывать разработчики Lumberyard, когда узнают, что эта ошибка осталась только у них.
Кстати таких ещё две:
Есть такая ошибка:
V546 [17] Member of a class is initialized by itself: 'eConfigMax(eConfigMax.VeryHigh)'. particleparams.h 1837
ParticleParams() :
....
fSphericalApproximation(1.f),
fVolumeThickness(1.0f),
fSoundFXParam(1.f),
eConfigMax(eConfigMax.VeryHigh), // <=
fFadeAtViewCosAngle(0.f)
{}
В CryEngine этот класс вообще переписали, а тут ошибка с инициализацией осталась.
V521 [18] Such expressions using the ',' operator are dangerous. Make sure the expression '!sWords[iWord].empty(), iWord ++' is correct. tacticalpointsystem.cpp 3376
bool CTacticalPointSystem::Parse(....) const
{
string sInput(sSpec);
const int MAXWORDS = 8;
string sWords[MAXWORDS];
int iC = 0, iWord = 0;
for (; iWord < MAXWORDS; !sWords[iWord].empty(), iWord++)
{
sWords[iWord] = sInput.Tokenize("_", iC);
}
....
}
Подозрительный цикл, который в CryEngine тоже уже переписали.
У пользователей, которые начинают использовать PVS-Studio впервые, возникает примерно одинаковая ситуация: находят ошибку, выясняют, что её добавили несколько месяцев назад и с радостью осознают, что чудом избегали проявления проблемы у своих пользователей. Многие наши клиенты пришли к регулярному использованию PVS-Studio именно после таких историй.
Иногда для запуска процессов контроля качества кода компания должна побывать в таких ситуациях несколько раз. Вот пример про CryEngine и Lumberyard:
V557 [19] CWE-119 Array overrun is possible. The 'id' index is pointing beyond array bound. gameobjectsystem.cpp 113
uint32 CGameObjectSystem::GetExtensionSerializationPriority(....)
{
if (id > m_extensionInfo.size())
{
return 0xffffffff; // minimum possible priority
}
else
{
return m_extensionInfo[id].serializationPriority;
}
}
Как известно, Amazon Lumberyard основан на не самой новой версии CryEngine. Тем не менее, с помощью анализатора PVS-Studio удалось найти ошибку, которая присутствует сейчас в двух игровых движках. Надо было с помощью оператора '>=' индекс проверять…
Ошибка с индексацией серьёзная. Более того, всего таких мест шесть! Вот ещё пример:
V557 [19] CWE-119 Array overrun is possible. The 'index' index is pointing beyond array bound. vehicleseatgroup.cpp 73
CVehicleSeat* CVehicleSeatGroup::GetSeatByIndex(unsigned index)
{
if (index >= 0 && index <= m_seats.size())
{
return m_seats[index];
}
return NULL;
}
Кто-то наделал однотипных ошибок, и они не были исправлены только потому, что когда-то не попали в обзоры ошибок CryEngine.
Оставшиеся предупреждения:
Долгое существование ошибок в коде можно объяснить только соответствующим уровнем тестирования проекта. Есть мнение, что статический анализ находит ошибки только в неиспользуемом коде. Так вот, это не так. Разработчики забывают, что большинство пользователей молча страдает от неочевидных багов в программах, а проявление этих самых редких багов часто пагубно сказывается на работе всей компании, репутации и её продажах, если таковые имеются.
Дойдя до этого раздела статьи, вы наверняка заметили, что программирование методом Copy-Paste — причина многих проблем. В PVS-Studio поиск таких ошибок реализован в разных диагностиках. В этом разделе будут приведены примеры копипасты, находимые с помощью V561 [20].
Вот пример подозрительного кода, когда объявляют переменные с одинаковым именем в пересекающихся областях видимости.
V561 [20] CWE-563 It's probably better to assign value to 'pLibrary' variable than to declare it anew. Previous declaration: entityobject.cpp, line 4703. entityobject.cpp 4706
void CEntityObject::OnMenuConvertToPrefab()
{
....
IDataBaseLibrary* pLibrary = GetIEditor()->Get....;
if (pLibrary == NULL)
{
IDataBaseLibrary* pLibrary = GetIEditor()->Get....;
}
if (pLibrary == NULL)
{
QString sError = tr(....);
CryMessageBox(....);
return;
}
....
}
Указатель 'pLibrary' не перезаписывается, как ожидалось. Инициализация этого указателя была полностью скопирована под условие вместе с объявлением типа.
Приведу списком все похожие места:
Большой список… некоторые из перечисленных мест являются даже полными копиями описанного примера.
В коде игрового движка найдено очень много мест, где переменная присваивается сама себе. Где-то этот код остался для отладки, где-то код просто красиво оформлен (тоже часто является источником ошибок), поэтому приведу один самый подозрительный для меня фрагмент кода.
V570 [21] The 'behaviorParams.ignoreOnVehicleDestroyed' variable is assigned to itself. vehiclecomponent.cpp 168
bool CVehicleComponent::Init(....)
{
....
if (!damageBehaviorTable.getAttr(....)
{
behaviorParams.ignoreOnVehicleDestroyed = false;
}
else
{
behaviorParams.ignoreOnVehicleDestroyed = // <=
behaviorParams.ignoreOnVehicleDestroyed; // <=
}
....
}
В текущем виде ветвь else вообще не нужна. Но, возможно, этот фрагмент кода содержит ошибку: хотели присвоить переменной противоположное значение:
bValue = !bValue
Но с результатами этой диагностики разработчиками лучше ознакомиться самим.
В этом разделе будет приведено много примеров, когда при обработке ошибок что-то пошло не так.
Пример 1.
V606 [22] Ownerless token 'nullptr'. dx12rootsignature.cpp 599
RootSignature* RootSignatureCache::AcquireRootSignature(....)
{
....
RootSignature* result = new RootSignature(m_pDevice);
if (!result->Init(params))
{
DX12_ERROR("Could not create root signature!");
nullptr;
}
m_RootSignatureMap[hash] = result;
return result;
}
}
Забыли написать return nullptr;. Теперь невалидное значение переменной result будет использовано в других местах кода.
Точь-в-точь такой же код скопировали ещё в одно место:
Пример 2.
V606 [22] Ownerless token 'false'. fillspacetool.cpp 191
bool FillSpaceTool::FillHoleBasedOnSelectedElements()
{
....
if (validEdgeList.size() == 2)
{
....
}
if (validEdgeList.empty())
{
....
for (int i = 0, iVertexSize(....); i < iVertexSize; ++i)
{
validEdgeList.push_back(....);
}
}
if (validEdgeList.empty()) // <=
{
false; // <= fail
}
std::vector<BrushEdge3D> linkedEdgeList;
std::set<int> usedEdgeSet;
linkedEdgeList.push_back(validEdgeList[0]); // <= fail
....
}
Очень интересный пример ошибки с пропущенным оператором return. Теперь возможна ситуация обращения по индексу к пустому контейнеру.
Пример 3.
V564 [23] CWE-480 The '&' operator is applied to bool type value. You've probably forgotten to include parentheses or intended to use the '&&' operator. toglslinstruction.c 2914
void SetDataTypes(....)
{
....
// Check assumption that both the values which MOVC might pick
// have the same basic data type.
if(!psContext->flags & HLSLCC_FLAG_AVOID_TEMP_REGISTER_ALIASING)
{
ASSERT(GetOperandDataType(psContext, &psInst->asOperands[2])
== GetOperandDataType(psContext, &psInst->asOperands[3]));
}
....
}
Неправильно проверили наличие битиков во флаге. Оператор отрицания применяется к значению флага, а не всего выражения. Правильно написать так:
if(!(psContext->flags & ....))
Ещё подобные предупреждения:
Пример 4.
V596 [24] CWE-390 The object was created but it is not being used. The 'throw' keyword could be missing: throw runtime_error(FOO); prefabobject.cpp 1491
static std::vector<std::string> PyGetPrefabLibrarys()
{
CPrefabManager* pPrefabManager = GetIEditor()->GetPrefabMa....;
if (!pPrefabManager)
{
std::runtime_error("Invalid Prefab Manager.");
}
....
}
Ошибка с генерацией исключения. Надо было писать так:
throw std::runtime_error("Invalid Prefab Manager.");
Весь список таких ошибок:
V549 [25] CWE-688 The first argument of 'memcmp' function is equal to the second argument. meshutils.h 894
struct VertexLess
{
....
bool operator()(int a, int b) const
{
....
if (m.m_links[a].links.size() != m.m_links[b].links.size())
{
res = (m.m_links[a].links.size() <
m.m_links[b].links.size()) ? -1 : +1;
}
else
{
res = memcmp(&m.m_links[a].links[0], &m.m_links[a].links[0],
sizeof(m.m_links[a].links[0]) * m.m_links[a].links.size());
}
....
}
....
};
В условии сравниваются размеры двух векторов. Если они равны, то в ветви else значения первых элементов векторов сравниваются с помощью функции memcmp(). Но первый и второй аргументы этой функции одинаковы! Доступ к элементам массива достаточно громозднкий. Там есть индексы a и b. Скорее всего, опечатка именно в них.
V611 [26] CWE-762 The memory was allocated using 'new T[]' operator but was released using the 'delete' operator. Consider inspecting this code. It's probably better to use 'delete [] data;'. vectorn.h 102
~vectorn_tpl()
{
if (!(flags & mtx_foreign_data))
{
delete[] data;
}
}
vectorn_tpl& operator=(const vectorn_tpl<ftype>& src)
{
if (src.len != len && !(flags & mtx_foreign_data))
{
delete data; // <=
data = new ftype[src.len];
}
....
}
Память по указателю datа освобождается с помощью неправильного оператора. Везде должен использоваться оператор delete[].
V779 [27] CWE-561 Unreachable code detected. It is possible that an error is present. fbxskinimporter.cpp 67
Events::ProcessingResult FbxSkinImporter::ImportSkin(....)
{
....
if (BuildSceneMeshFromFbxMesh(....)
{
context.m_createdData.push_back(std::move(createdData));
return Events::ProcessingResult::Success; // <=
}
else
{
return Events::ProcessingResult::Failure; // <=
}
context.m_createdData.push_back(); // <= fail
return Events::ProcessingResult::Success;
}
Все ветки условного оператора завершаются выходом из функции. При этом некий код не выполняется.
V779 [27] CWE-561 Unreachable code detected. It is possible that an error is present. dockablelibrarytreeview.cpp 153
bool DockableLibraryTreeView::Init(IDataBaseLibrary* lib)
{
....
if (m_treeView && m_titleBar && m_defaultView)
{
if (m_treeView->topLevelItemCount() > 0)
{
ShowTreeView();
}
else
{
ShowDefaultView();
}
return true; // <=
}
else
{
return false; // <=
}
emit SignalFocused(this); // <= fail
}
На этом фрагменте кода легко заметить ошибку. Но если долго писать код, то внимание резко снижается и подобные ошибки легко попадают в проект.
V622 [28] CWE-478 Consider inspecting the 'switch' statement. It's possible that the first 'case' operator is missing. datum.cpp 872
AZ_INLINE bool IsDataGreaterEqual(....)
{
switch (type.GetType())
{
AZ_Error("ScriptCanvas", false, "....");
return false;
case Data::eType::Number:
return IsDataGreaterEqual<Data::NumberType>(lhs, rhs);
....
case Data::eType::AABB:
AZ_Error("ScriptCanvas", false, "....",
Data::Traits<Data::AABBType>::GetName());
return false;
case Data::eType::OBB:
AZ_Error("ScriptCanvas", false, "....",
Data::Traits<Data::OBBType>::GetName());
return false;
....
}
Если в switch присутствует код вне оператора case/default, то он никогда не выполняется.
В статью вошли 95 предупреждений анализатора, из них 25 с примерами кода. Сколько это материала от всего отчёта анализатора? Я быстро прокрутил всего треть предупреждений уровня High. Есть ещё Medium и Low, группа диагностик для поиска оптимизаций и другие неосвоенные возможности анализатора — это ещё сотни очевидных ошибок и тысячи неисследованных предупреждений.
И тут читателю надо задать себе вопрос: «Возможно ли с таким подходом к проекту выпустить хороший игровой движок?». Контроля качества кода нет. За основу был взят код CryEngine со старыми ошибками, добавлены новые ошибки. Сам CryEngine дорабатывается только после очередного обзора кода. У компании Amazon есть все шансы с её ресурсами поработать в направлении качества кода и выпустить самый крутой игровой движок!
Не стоит сильно расстраиваться. Среди клиентов PVS-Studio есть более тридцати других компаний, которые занимаются играми. С ними и их продуктами вы можете познакомиться на странице нашего сайта "Клиенты [29]", выбрав фильтр «Разработка игр». Так что мы постепенно улучшаем мир. Возможно, мы сможем улучшить и Amazon Lumberyard :).
На тему качества игрового ПО коллега недавно написал статью, предлагаю заинтересованным ознакомиться: "Статический анализ в видеоигровой индустрии: топ-10 программных ошибок [30]".
Ссылка на загрузку анализатора PVS-Studio [7], как же без неё ;-)
Автор: SvyatoslavMC
Источник [31]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/programmirovanie/285480
Ссылки в тексте:
[1] Amazon Lumberyard: https://aws.amazon.com/ru/lumberyard/
[2] CryEngine: https://www.cryengine.com/
[3] августе 2016: https://www.viva64.com/ru/b/0417/
[4] апреле 2017: https://www.viva64.com/ru/b/0495/
[5] Github: https://github.com/aws/lumberyard/
[6] Star Citizen: https://robertsspaceindustries.com/
[7] PVS-Studio: https://www.viva64.com/ru/pvs-studio/
[8] В планах Java: https://www.viva64.com/ru/b/0572/
[9] система мониторинга компиляции: https://www.viva64.com/ru/m/0031/
[10] V523: https://www.viva64.com/ru/w/v523/
[11] V709: https://www.viva64.com/ru/w/v709/
[12] V501: https://www.viva64.com/ru/w/v501/
[13] AUTODESK: https://www.autodesk.com
[14] FBX SDK: https://www.autodesk.com/products/fbx/overview
[15] V502: https://www.viva64.com/ru/w/v502/
[16] V519: https://www.viva64.com/ru/w/v519/
[17] V546: https://www.viva64.com/ru/w/v546/
[18] V521: https://www.viva64.com/ru/w/v521/
[19] V557: https://www.viva64.com/ru/w/v557/
[20] V561: https://www.viva64.com/ru/w/v561/
[21] V570: https://www.viva64.com/ru/w/v570/
[22] V606: https://www.viva64.com/ru/w/v606/
[23] V564: https://www.viva64.com/ru/w/v564/
[24] V596: https://www.viva64.com/ru/w/v596/
[25] V549: https://www.viva64.com/ru/w/v549/
[26] V611: https://www.viva64.com/ru/w/v611/
[27] V779: https://www.viva64.com/ru/w/v779/
[28] V622: https://www.viva64.com/ru/w/v622/
[29] Клиенты: https://www.viva64.com/ru/customers/
[30] Статический анализ в видеоигровой индустрии: топ-10 программных ошибок: https://www.viva64.com/ru/b/0570/
[31] Источник: https://habr.com/post/416565/?utm_campaign=416565
Нажмите здесь для печати.