На момент выпуска в 1998 году Half-life получил тёплый приём за свой гейм-дизайн, который стал возможным благодаря искусственному интеллекту. Это влияние AI привело к тому. что HL назвали одной из самых важных игр в истории.
И даже двадцать лет спустя, изучив её код, можно многое узнать о создании простых, но эффективных систем AI. Вся логика AI жёстко закодирована на C++ и не слишком объектоориентирована, поэтому в ней гораздо легче разобраться, чем в более свежих движках (хотя и расширять её не так просто).
В этой статье мы рассмотрим открытый SDK для Half-Life 1, проанализируем различные аспекты AI, такие как система планировщика задач, её реализация, похожая на конечные автоматы, и сенсорная система. Прочитав статью, вы глубже поймёте принцип использования этих концепций и их реализации в играх.
Скриншот 1: охранник Барни сражается с одним из монстров
Загрузка Half-Life SDK
Установить SDK Valve для Half-Life очень просто (с отличие от инструментов F.E.A.R.) и если вы хотите разрабатывать моды, то для него требуется только оригинальная игра. Вот, что вам будет нужно:
- Скачайте версию 2.3 SDK Half-Life, или только исходники без ресурсов, или копию полного SDK с моделями.
- Распакуйте файл в любой каталог, лучше в папку с игрой, если вы хотите разрабатывать с помощью SDK моды. Это займёт несколько секунд, в результате у вас будет пачка каталогов с моделями и исходным кодом.
Скриншот 2: код игры на C++ в SDK Half-Life версии 2.3.
Разбираемся с кодом
Кодовая база не так хорошо структурирована, как в F.E.A.R. или даже в Quake 3. В ней есть несколько подкаталогов, но файлы имеют не очень понятные названия, а реализация классов C++ разбросана по нескольким файлам, из названий которых почти ничего нельзя понять.
- В полном SDK есть две папки, в которых содержится код:
Single-Player Source
иMultiplayer Source
. Обе они имеют схожую структуру каталогов. - Бо́льшая часть игровой логики находится в подкаталоге
/dll/
, в котором содержатся все файлы, необходимые для сборки hl.dll, который также является фреймворком для модов. Кроме того, в этом каталоге содержится код ИИ, разбросанный по множеству файлов, с названиями типа*monster*.[h,cpp]
,*ai*.[h,cpp]
и других файлах - В каталоге с исходным кодом есть и другие каталоги, например
engine
, в котором содержатся файлы заголовков, взаимодействующие с основным исполняемым файлом (как базовые сущности). В каталогеcommon
также содержатся похожие низкоуровневые файлы, используемые движком и кодом игры.
Если вы изучаете или модифицируете AI, то больше всего времени вы будете уделять каталогу /dll/
, потому что в нём содержится поведение различных акторов игры.
Скриншот 3: катсцена из игры с учёным.
Планировщик и система целей
В файлах schedule.[h,cpp]
находится очень простая система, управляемая целями. Она состоит из нескольких уровней задач, которые можно процедурно объединять.
Задачи
Задачи — это короткие атомизированные поведения, имеющие конкретное назначение. Например, большинство акторов Half-Life поддерживает следующие задачи: TASK_WALK_PATH
, TASK_CROUCH
, TASK_STAND
, TASK_GUARD
, TASK_STEP_FORWARD
, TASK_DODGE_RIGHT
, TASK_FIND_COVER_FROM_ENEMY
, TASK_EAT
, TASK_STOP_MOVING
, TASK_TURN_LEFT
, TASK_REMEMBER
. Они определяются как перечисления в файле заголовка и реализуются как методы C++.
Условия
Условия используются для выражения ситуации актора в мире. Поскольку логика задана жёстко, условия можно выразить очень компактно, как битовые поля, но в таком случае условий может быть не больше 32. Например, условиями являются COND_NO_AMMO_LOADED
, COND_SEE_HATE
, COND_SEE_FEAR
, COND_SEE_DISLIKE
, COND_ENEMY_OCCLUDED
, COND_ENEMY_TOOFAR
, COND_HEAVY_DAMAGE
, COND_CAN_MELEE_ATTACK2
, COND_ENEMY_FACING_ME
.
Планы
План состоит из серии задач (с произвольными параметрами) и учитывает битовое поле условий, чтобы определить, когда план неприменим. Для удобства отладки объекты планов имеют имена.
Цели
Цели находятся на более высоком уровне и состоят из планов. Логика цели может при необходимости выбирать план на основании проваленной задачи и текущего контекста. Примеры целей из Half-Life: GOAL_ATTACK_ENEMY
, GOAL_MOVE
, GOAL_TAKE_COVER
, GOAL_MOVE_TARGET
и GOAL_EAT
.
Использованный Valve код извлечён из движка Quake, и до сих пор достаточно очевиден, несмотря на то, что был преобразован в C++; файлы и struct имеют похожие названия.
Скриншот 4: десантники подняли тревогу в исследовательском центре.
Конечный автомат
На практике все эти планы и задачи соединены вместе в структуру, похожую на конечный автомат. На верхнем уровне для обновления ИИ вызывается функция в monsterstate.cpp
:
void CBaseMonster :: RunAI ( void );
Она, в свою очередь, вызывает перегруженные функции, отвечающие за проверку с помощью MaintainSchedule()
применимости текущего плана и выбор новых с помощью GetSchedule()
. Их можно изменять в зависимости от потребностей с помощью порождённых классов, см., например, barney.cpp
или scientist.cpp
.
На нижнем уровне функции StartTask()
и RunTask()
реализуют логику для каждого из идентификаторов задач, определённых в конструкции enum
. Они реализованы в классах, тоже унаследованных из CBaseMonster
. В результате это во многом выглядит как конечный автомат, реализованный как конструкция switch
.
void CScientist :: RunTask( Task_t *pTask )
{
switch ( pTask->iTask )
{
case TASK_RUN_PATH_SCARED:
if ( MovementIsComplete() )
TaskComplete();
if ( RANDOM_LONG(0,31) < 8 )
Scream();
break;
case TASK_MOVE_TO_TARGET_RANGE_SCARED:
/* ... */
break;
case TASK_HEAL:
if ( m_fSequenceFinished )
{
TaskComplete();
}
else
{
if ( TargetDistance() > 90 )
TaskComplete();
pev->ideal_yaw = UTIL_VecToYaw( /* ... */ );
ChangeYaw( pev->yaw_speed );
}
break;
default:
CTalkMonster::RunTask( pTask );
break;
}
}
Более типичным подходом была бы реализация каждого из этих блоков case
в их собственном классе, но при существующей реализации гораздо проще при необходимости использовать логику одного объекта в другом, хотя и ценой модульности.
Интересно также заметить, что AI хранит два состояния: одно идеальное и одно текущее. Таким образом коду игры проще создавать для акторов цели, и заставлять их находить наилучшие способы их достижения. Это интересное сочетание конечного автомата и целенаправленной системы.
Скриншот 5: игровая катсцена с учёным.
Реализация сенсорной системы
В базовом monster.[h,cpp]
есть код, дающий всем акторам зрение, обоняние и слух.
void CBaseMonster :: Look ( int iDistance );
Функция зрения проверяет различные флаги, такие как SF_MONSTER_PRISONER
и SF_MONSTER_WAIT_TILL_SEEN
, чтобы при необходимости обеспечивать дизайнерам возможность контроля. В уравнении также учитываются такие параметры, как область видимости и угол обзора.
CSound* CBaseMonster :: PBestSound ( void );
Код слуха и обоняния работает похожим образом, только использует события звука. Хранится список объектов, требующих внимания монстров, а сенсорная система выбирает для фокусировки лучший из них.
Итоги и дополнительное чтение
В целом, исходный код, стоящий за этой системой, хотя и прост, очень информативен. Если вы хотите подобрать лёгкую реализацию принятия решений искусственным интеллектом, то стоит выбрать этот подход. Однако, возможно, стоит реализовать каждую задачу в своём собственном объекте: в наши дни в коммерческих играх обычно используют такое решение.
Скриншот 6: стихийное поведение отрядов в Half-Life.
В коде AI Half-Life содержатся и другие интересные идеи.
- Код игры представляет навигационные точки в виде только 3D-вектора и типа локации! Они привязываются к нижестоящей навигационной системе, но их можно использовать и в олдскульной системе «хлебных крошек», по которым следуют монстры.
- Half-Life удивила многих поведением отрядов. Однако в игре нет никакого AI верхнего уровня, управляющего этими отрядами, то есть всё поведение проявляется стихийно.
Если вы хотите воссоздать что-то большее, чем просто монстр из Half-Life, лучше всего изучить фреймворк ботов. Он позволит создавать AI-ботов для многопользовательской игры, которых можно применять в сторонних модах Half-life. Их можно найти здесь:
Автор: PatientZero