В прошлом году я достиг точки во время разработки «Протектората», в которой мне нужно было реализовать базовый ИИ.
Как инженер-программист, я работаю в основном в области автоматизации. Автоматизация приносит с собой определенную потребность в интеллекте, даже если он очень прост. В наши дни горячие темы ИИ — машинное и глубокое обучение. Ближе всего к этому меня подвела моя работа в странной области анализа настроений.
Итак, столкнувшись с новорождёнными пустыми игровыми персонажами, праздно стоящими на палубе своей совершенно новой космической станции, я подумал, что лучше всего провести небольшое исследование лучших техник реализации и управления их поведением.
Мне нужен был простой способ дать каждому персонажу состояние и заставить его выполнять определенную задачу в зависимости от состояния, в котором он находился. Это поднимало такие вопросы, как
- Как я буду переводить каждого NPC между состояниями?
- Как заставить каждого отдельного NPC выполнять определенную задачу в зависимости от состояния, в котором они находятся?
- Как каждое поведение содержится в каждом состоянии?
Я прочитал много недоступного, многословного и страшного на вид текста об управлении игровым ИИ, пока не наткнулся на решение, которое, казалось, соответствовало всем требованиям: конечный автомат.
Что такое конечный автомат?
Проще говоря, конечный автомат — это то, что может существовать в одном из многих состояний в любое время. Каждое состояние несет в себе поведение, которое разыгрывается в зависимости от состояния. Важным моментом в этом объяснении является то, что машина находится в одном состоянии; многие состояния конечны. Полезно думать о машине как об объекте, который имеет xколичество потенциальных состояний и текущее состояние которого может измениться в любое время. Более того, при изменении состояния разыгрываются ограничивающие его действия.
Итак, как мы применим это к персонажам нашей игры?
Конечный автомат для каждого символа
Представьте сцену:
Вы смотрите на свою космическую станцию. Он только что получил несколько новобранцев, которые стоят без дела. Ты их Бог. Они ждут ваших приказов. Я нажимаю на одного из своих персонажей, чтобы выбрать их, а затем нажимаю на свой новый Дендрарий. Этот персонаж собирается пойти и ухаживать за растениями в гигантской оранжерее.
Мне нужно вызвать поведение, и для этого мне нужно изменить состояние персонажа. Их нынешнее состояние, ну, это ничего. Они бездействуют. Никакое поведение не связано с этим состоянием, кроме как стоять и смотреть в пространство. Однако, когда я выбираю их, а затем выбираю Дендрарий, я меняю состояние персонажа.
Новое состояние персонажа — в пути к системе. Обратите внимание, что штат не собирается заниматься садоводством или чем-то подобным. На данный момент нас не особо волнует конечный результат, когда персонаж начинает управлять Дендрарием, потому что это новое состояние, на которое мы изменим позже. А пока мы просто хотим отправиться туда. Как только мы туда доберемся, мы можем изменить состояние на manningarboretum, что приведет к другому поведению.
Мы можем изменить состояние в любое время. Так что, если бы я хотел, чтобы персонаж не ходил, пока он шел в Дендрарий, я мог бы изменить состояние обратно на Idle. Какое поведение имеет состояние простоя? Ничего такого. Так что персонаж теперь просто стоит на месте. Мы изменили состояние и вызвали изменение поведения.
Всякий раз, когда мы меняем состояние, мы переходим от одного набора поведения к другому. Мы говорим: «когда ты в этом состоянии, сделай это», и я могу изменить одно состояние в любое время, но я должен всегда останавливать текущее состояние. Это самое важное, что нужно помнить о переходе между действиями таким образом. Типы состояний, которые у меня есть, конечны, и я могу занимать только одно состояние за раз.
Программирование конечного автомата
Конечные автоматы могут быть как простыми, так и сложными, как вы хотите. Для Protectorate я использую игровой движок Unity, и, следовательно, большая часть логики и поведения, которые я хочу запускать в течение длительного периода времени, инкапсулированы в сопрограммы. Каждая сопрограмма воплощает в себе набор действий, которые может выполнять персонаж. Кроме того, курутина запускается, когда я вхожу в состояние, и останавливается, когда я выхожу из него. Поток на базовом уровне выглядит следующим образом:
- Введите состояние
- Запустите связанную сопрограмму.
- Выйти из вышеуказанного состояния
- Остановите вышеуказанную сопрограмму
- Повторите для соответствующего требуемого состояния.
Псевдокод для вышеуказанного может выглядеть так:
ENTER IDLE STATE
START COROUTINE(DO NOTHING)
...........time passes
EXIT IDLE STATE
STOP COROUTINE(DO NOTHING
ENTER ENROUTETODESTINATION STATE
START COROUTINE(WALK TO DESTINATION)
...........time passes.......character arrives which triggers:
EXIT ENROUTETODESTINATION STATE
STOP COROUTINE(WALK TO DESTINATION)
ENTER MAN SYSTEM STATE
START COROUTINE(MAN SYSTEM) etc
В Unity мы пишем код C#. Для этой цели мы можем легко представить состояния как набор констант внутри перечисления.
public enum State
{
Idle,
EnRouteToSystem,
ManningSystem,
EnRouteToQuarters,
InQuarters,
EnRouteToGenericLocation,
Sleeping
}
Затем я создаю экземпляр перечисления State с помощью get; и установить; properties, благодаря чему мы можем запускать или останавливать соответствующие поведенческие сопрограммы, просто устанавливая состояние в одно из указанных выше значений.
private State _currentState;
public State state { get { return _currentState; } set { ExitState(_currentState); _currentState = value; EnterState(_currentState); } }
Ниже вы можете увидеть, как мы выходим из текущего состояния и входим в новое, вызывая запуск и остановку новых сопрограмм, что позволяет мгновенно изменить поведение.
void ExitState(State state) { switch (state) { case State.Idle: StopCoroutine("Idle"); break; case State.EnRouteToSystem: StopCoroutine("EnRouteToSystem"); break; case State.ManningSystem: StopCoroutine("ManningSystem"); break; case State.EnRouteToQuarters: StopCoroutine("EnRouteToQuarters"); break; case State.InQuarters: StopCoroutine("InQuarters"); break; case State.EnRouteToGenericLocation: StopCoroutine("EnRouteToGenericLocation"); break; case State.Sleeping: StopCoroutine("Sleeping"); break; default: break; }
}
void EnterState(State state) { switch (state) { case State.Idle: StartCoroutine("Idle"); break; case State.EnRouteToSystem: StartCoroutine("EnRouteToSystem"); break; case State.ManningSystem: StartCoroutine("ManningSystem"); break; case State.EnRouteToQuarters: StartCoroutine("EnRouteToQuarters"); break; case State.InQuarters: StartCoroutine("InQuarters"); break; case State.EnRouteToGenericLocation: StartCoroutine("EnRouteToGenericLocation"); break; case State.Sleeping: StartCoroutine("Sleeping"); break; default: break; } }
Это не SkyNet, но, по крайней мере, это автоматизированный способ изменения и выполнения действий по сценарию. Прелесть этого способа реализации конечного автомата в том, что он расширяем. Теоретически вам нужно только добавить новые состояния в список. Ваши сопрограммы могут быть в любом классе, в котором они должны быть.
Что касается того, действительно ли это чистый код… давайте не будем касаться этого в этом посте, а?