Могу рассказать об AI в
Faded.
Там тупняка покамест много конечно с ботами, но все части интеллекта хорошо изолированы, и баги являются частью конкретных стейтов/их недостатка для конкретных ситуаций, а не всей системы в целом.
Я прошёл через 3 итерации полной переписки системы, и нынешняя версия мне нравится.
Первое, на что стоит обратить внимание, это, как подметил Taugeshtu, стейт-машины. Они простые и нормально работают для простого ИИ (турелям хватит).
Обычно стейт это какая-то последовательность действий, которую повторяет бот, пока не перейдёт в другой. В стайте про HL ими можно назвать shedule'ы по сути. В идеале стейты должны иметь чёткую задачу, чтоб если не понимаешь, что бот творит, достаточно было глянуть в каком он стейте.
Херня начинает происходить при попытке написать для сложного бота при каких условиях бот должен быть в каком стейте, т.к. событий в игре много и стейтов тоже становится много.
Распространённый вариант -
State transition table, когда у нас есть таблица в какой стейт перейти при получении ботом каждого эвента в зависимости от того, в каком он стейте сейчас, но с ней случается комбинаторный взрыв, и невозможно за всем уследить.
Далее (уже не про стейт-машины), если
Behaviour trees.
Вкратце, суть такова (насколько я её понял из разных презентаций): есть дерево, ноды которого - условия, а конечные листья - собственно, действия или их последовательности. Условия могут возвращать успех, тогда мы идём дальше вниз по нему, а если возвращают неудачу, то мы возвращаемся на родительскую ноду и пробуем следующую её ветвь, если же следующей нет, то он тоже возвращает к своему паренту.
Тут одновременно 2 удобства, на мой взгляд - иерархически удобно разбитая логика + непрямое наличие приоритетов у действий.
Там они много типов нод напридумывали ещё, но судя по докам, всё это мотивировалось желанием создать юзер-френдли систему, в которой нубы-дизайнеры без знания кода смогли бы городить поведения.
Мои хотелки и бекграунд требовали немного иного:
1. Хочу задавать весь AI в коде.
2. Долгое время я прототипировал AI в движке первой Мафии, а там все скрипты по дефолту были coroutine'ами, и для меня было естественным описывать растянутое во времени поведение ими, наподобие:
персонаж_идисюда точка1
персонаж_посмотрина точка2
персонаж_анимация "чешет репу"
персонаж_идисюда точка3
3. Не хочу переписывать с нуля огромную FSM, которая у меня уже была.
Из этих условий у меня вышел странный code-driven гибрид стейтмашины и BT:
У бота по-прежнему есть стейты. и он может находится только в одном. Стейт представляет собой корутину, идущую циклом.
Каждый тик AI (0.5 сек у меня) сначала заполняются переменные бота тем, что он видит/слышит, а затем на основе перменных проверяется дерево, схожее с BT, но цель которого - выбрать стейт.
Начало дерева выглядит так:
if (checkSlip()) {}
else if (checkGetHit()) {}
else if (checkCuffed()) {}
else if (checkNoPath()) {}
else if (checkDoorOnTheWay()) {}
else if (checkRecentHit()) {} // autoclose off
//else if (checkShouldUseElevator()) {}
else if (checkRecentBulletHit()) {}
else if (checkHandsUp()) {}
else if (checkAir()) {}
else if (checkShooterVisible()) {}
else if (checkRecentGunshot()) {}
else if (checkShouldReload()) {}
else if (checkPickupsVisible()) {}
else if (checkManiac()) {}
else if (checkKillerVisible()) {}
else if (checkDealWithBulletHit()) {}
else if (checkArrestVillainVisible()) {}
else if (checkThiefVisible()) {}
else if (checkMDVisible()) {}
else if (checkIntruder()) {}
else if (checkDealWithVillain()) {}
else if (checkRecentScream()) {}
else if (checkPhysHitLight()) {}
else if (checkEvacuate()) {}
else if (checkPhysThrowerVisible()) {}
else if (checkMDSound()) {}
else if (checkUncertainVillainVisible()) {} // autoclose on
else if (checkVoice()) {}
else if (checkConversation()) {}
else if (checkSusp()) {}
else if (checkCrouchedVisible()) {}
else if (checkSemafor()) {}
else if (checkRecentFootstep()) {}
else if (checkBody()) {}
else if (checkBloodVisible()) {}
else if (checkInspectLoudSounds()) {}
else if (checkDealWithBody()) {}
else if (checkCamShooting()) {}
else if (checkSuspPickup()) {}
else if (checkInspectFootsteps()) {}
else if (checkSecure()) {}
else if (checkEscortVillain()) {}
else if (checkPhone()) {}
Это его часть. Проверки написаны по приоритетам - от важных до неважных. Это проверки базового AI, ниже там могут быть ещё проверки каждого уникального бота свои.
Внутри каждого чека ветвление более частное, например:
bool checkThiefVisible()
{
if (ignoreVillains) return false;
if ((villainVisible >= 0) && (!villainUncertain[villainVisible]))
{
if (guardVisible>=0)
{
if (!villainReported[villainVisible])
{
if ((behaviour<=behType.civil) || (!HasAnyLoadedWeapon()))
{
if (villainDanger[villainVisible]>VILLAIN_SUSP)
{
if (!ignorePointToVillain)
{
changeState(State.pointToVillain);
return true;
}
}
}
}
}
if (behaviour!=behType.civil)
{
if (villainDanger[villainVisible]==VILLAIN_THIEF)
{
if (Vector3.Distance(villain[villainVisible].transform.position, transform.position) < 2.5f)
{
changeState(State.dealWithThief);
return true;
}
else
{
intruderVisible = villainVisible;
changeState(State.catchIntruderRun);
return true;
}
}
}
}
return false;
}
Если функция по частным проверкам не позволяет уйти в стейт - она возвращает false, и мы идём дальше вниз по списку первому.
Если она выбирает стейт и возвращает true, дерево заканчивается.
changeState проверяет, в каком мы стейте, если в том же самом - то ничего не делается, если в другом - то корутина прошлого резко дропается, и начинается новая.
Тут можно порассуждать, лучше ли делать так, или лучше иметь некую exitState функцию для каждого стейта (типа курил сигарету в стейте, а на выходе надо её выкинуть), сначала я делал второе, но это было мутно, т.к. нам, к примеру, не надо прятать пистолет при переходе из стрельбы в перезарядку, но надо при переходе в спокойное состояние, так что подумал что лучше заканчивать действия в начале самих стейтов.