В прошлом году я достиг точки во время разработки «Протектората», в которой мне нужно было реализовать базовый ИИ.

Как инженер-программист, я работаю в основном в области автоматизации. Автоматизация приносит с собой определенную потребность в интеллекте, даже если он очень прост. В наши дни горячие темы ИИ — машинное и глубокое обучение. Ближе всего к этому меня подвела моя работа в странной области анализа настроений.

Итак, столкнувшись с новорождёнными пустыми игровыми персонажами, праздно стоящими на палубе своей совершенно новой космической станции, я подумал, что лучше всего провести небольшое исследование лучших техник реализации и управления их поведением.

Мне нужен был простой способ дать каждому персонажу состояние и заставить его выполнять определенную задачу в зависимости от состояния, в котором он находился. Это поднимало такие вопросы, как

  • Как я буду переводить каждого NPC между состояниями?
  • Как заставить каждого отдельного NPC выполнять определенную задачу в зависимости от состояния, в котором они находятся?
  • Как каждое поведение содержится в каждом состоянии?

Я прочитал много недоступного, многословного и страшного на вид текста об управлении игровым ИИ, пока не наткнулся на решение, которое, казалось, соответствовало всем требованиям: конечный автомат.

Что такое конечный автомат?

Проще говоря, конечный автомат — это то, что может существовать в одном из многих состояний в любое время. Каждое состояние несет в себе поведение, которое разыгрывается в зависимости от состояния. Важным моментом в этом объяснении является то, что машина находится в одном состоянии; многие состояния конечны. Полезно думать о машине как об объекте, который имеет xколичество потенциальных состояний и текущее состояние которого может измениться в любое время. Более того, при изменении состояния разыгрываются ограничивающие его действия.

Итак, как мы применим это к персонажам нашей игры?

Конечный автомат для каждого символа

Представьте сцену:

Вы смотрите на свою космическую станцию. Он только что получил несколько новобранцев, которые стоят без дела. Ты их Бог. Они ждут ваших приказов. Я нажимаю на одного из своих персонажей, чтобы выбрать их, а затем нажимаю на свой новый Дендрарий. Этот персонаж собирается пойти и ухаживать за растениями в гигантской оранжерее.

Мне нужно вызвать поведение, и для этого мне нужно изменить состояние персонажа. Их нынешнее состояние, ну, это ничего. Они бездействуют. Никакое поведение не связано с этим состоянием, кроме как стоять и смотреть в пространство. Однако, когда я выбираю их, а затем выбираю Дендрарий, я меняю состояние персонажа.

Новое состояние персонажа — в пути к системе. Обратите внимание, что штат не собирается заниматься садоводством или чем-то подобным. На данный момент нас не особо волнует конечный результат, когда персонаж начинает управлять Дендрарием, потому что это новое состояние, на которое мы изменим позже. А пока мы просто хотим отправиться туда. Как только мы туда доберемся, мы можем изменить состояние на manningarboretum, что приведет к другому поведению.

Мы можем изменить состояние в любое время. Так что, если бы я хотел, чтобы персонаж не ходил, пока он шел в Дендрарий, я мог бы изменить состояние обратно на Idle. Какое поведение имеет состояние простоя? Ничего такого. Так что персонаж теперь просто стоит на месте. Мы изменили состояние и вызвали изменение поведения.

Всякий раз, когда мы меняем состояние, мы переходим от одного набора поведения к другому. Мы говорим: «когда ты в этом состоянии, сделай это», и я могу изменить одно состояние в любое время, но я должен всегда останавливать текущее состояние. Это самое важное, что нужно помнить о переходе между действиями таким образом. Типы состояний, которые у меня есть, конечны, и я могу занимать только одно состояние за раз.

Программирование конечного автомата

Конечные автоматы могут быть как простыми, так и сложными, как вы хотите. Для Protectorate я использую игровой движок Unity, и, следовательно, большая часть логики и поведения, которые я хочу запускать в течение длительного периода времени, инкапсулированы в сопрограммы. Каждая сопрограмма воплощает в себе набор действий, которые может выполнять персонаж. Кроме того, курутина запускается, когда я вхожу в состояние, и останавливается, когда я выхожу из него. Поток на базовом уровне выглядит следующим образом:

  1. Введите состояние
  2. Запустите связанную сопрограмму.
  3. Выйти из вышеуказанного состояния
  4. Остановите вышеуказанную сопрограмму
  5. Повторите для соответствующего требуемого состояния.

Псевдокод для вышеуказанного может выглядеть так:

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, но, по крайней мере, это автоматизированный способ изменения и выполнения действий по сценарию. Прелесть этого способа реализации конечного автомата в том, что он расширяем. Теоретически вам нужно только добавить новые состояния в список. Ваши сопрограммы могут быть в любом классе, в котором они должны быть.

Что касается того, действительно ли это чистый код… давайте не будем касаться этого в этом посте, а?