Конечный автомат персонажа в платформере: от флагов isJumping к состояниям, иерархии и стеку

Коротко

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

Содержание

  1. Почему флаги ломаются
  2. Что такое конечный автомат
  3. Таблица переходов героя платформера
  4. Реализация через switch
  5. Объекты-состояния с enter и exit
  6. Параллельные автоматы: движение и оружие
  7. Иерархия: общее суперсостояние «на земле»
  8. Стек состояний: урон и возврат
  9. Чек-лист: как перевести героя на автомат
  10. Когда вам это не подойдёт

Почему флаги ломаются

Флаги ломаются, потому что они допускают комбинации, которых в игре быть не должно. Роберт Нистром в главе State книги Game Programming Patterns показывает это на герое с флагами прыжка, приседания и пикирования: без проверки флага герой прыгает повторно прямо в воздухе, а без ещё одной проверки приседает посреди прыжка и меняет графику (проверено: ноябрь 2026).

3 булевых флага: isJumping, isDucking, isDiving
комбинаций: 2 × 2 × 2 = 8
допустимых: стоит, прыгает, приседает, пикирует = 4
недопустимых: 4 — и каждую нужно запрещать отдельной проверкой

С каждым новым флагом комбинаций становится вдвое больше, а проверок — всё сложнее. Автомат решает это иначе: вместо набора флагов одно поле state.

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

Конечный автомат — это фиксированный набор состояний, одно текущее состояние и переходы между ними, которые срабатывают по входу. Нистром формулирует четыре свойства: набор состояний конечен; автомат в каждый момент находится ровно в одном; на вход подаются события (нажатия клавиш, касание земли); у каждого состояния есть переходы, связывающие вход с новым состоянием. Если для пары «состояние + вход» перехода нет, вход игнорируется — именно так и исчезает прыжок в воздухе.

Таблица переходов героя платформера

Таблица переходов — это главный артефакт автомата: её можно проверить глазами до того, как написан код.

Переходы героя платформера
СостояниеВходНовое состояние
СтоитНажата «влево/вправо»Бежит
Стоит, БежитНажат прыжокПрыжок
Стоит, БежитНажата «вниз»Присед
Стоит, БежитПод ногами нет землиПадение
БежитОтпущены направленияСтоит
ПрыжокВертикальная скорость стала направлена внизПадение
ПадениеКоснулся землиСтоит или Бежит
ПриседОтпущена «вниз»Стоит
Прыжок, ПадениеНажат прыжок— (игнорируется)

Последняя строка — та самая, которая во флаговой версии требовала отдельной проверки. В автомате её не нужно писать: у состояний «Прыжок» и «Падение» перехода по прыжку просто нет.

Реализация через switch

Самая короткая реализация — перечисление состояний и switch по текущему. Нистром начинает именно с неё: вся логика одного состояния собрана в одной ветке.

const State = { IDLE: 0, RUN: 1, JUMP: 2, FALL: 3, CROUCH: 4 };

function updateHero(hero, input) {
  switch (hero.state) {
    case State.IDLE:
    case State.RUN:
      if (input.jumpPressed) { hero.vy = -hero.jumpSpeed; hero.state = State.JUMP; }
      else if (input.down) { hero.state = State.CROUCH; }
      else if (!hero.onGround) { hero.state = State.FALL; }
      else { hero.state = input.dx !== 0 ? State.RUN : State.IDLE; }
      break;
    case State.JUMP:
      if (hero.vy >= 0) hero.state = State.FALL;   // прыжок здесь не обрабатывается
      break;
    case State.FALL:
      if (hero.onGround) hero.state = input.dx !== 0 ? State.RUN : State.IDLE;
      break;
    case State.CROUCH:
      if (!input.down) hero.state = State.IDLE;
      break;
  }
}

Минус switch — данные состояния. Если присед должен копить заряд для суперпрыжка, счётчик заряда придётся держать в общем объекте героя, хотя нужен он только одному состоянию.

Объекты-состояния с enter и exit

Объект-состояние — это отдельный объект на каждое состояние со своими данными и методами; герой лишь хранит ссылку на текущий. Это паттерн «Состояние» из книги «банды четырёх», и Нистром разбирает его как следующий шаг после switch. Два метода делают его особенно удобным: enter вызывается при входе в состояние, exit — при выходе. В enter удобно один раз поменять анимацию и хитбокс, вместо того чтобы делать это в каждом переходе, ведущем в состояние.

class CrouchState {
  enter(hero) { this.charge = 0; hero.setAnimation("crouch"); hero.setHitbox("low"); }
  exit(hero)  { hero.setHitbox("normal"); }
  update(hero, input, dt) {
    this.charge += dt;                         // данные живут в самом состоянии
    if (!input.down) return this.charge > 0.5 ? new SuperJumpState() : new IdleState();
    return null;                               // остаёмся
  }
}

function changeState(hero, next) {
  hero.state.exit(hero);
  hero.state = next;
  hero.state.enter(hero);
}

function tick(hero, input, dt) {
  const next = hero.state.update(hero, input, dt);
  if (next) changeState(hero, next);
}

Нистром отдельно обсуждает выбор между статическими состояниями (один общий объект, если у состояния нет данных) и создаваемыми на каждый переход (если данные есть, как заряд приседа). Для героя-одиночки создание объекта на переход ничего не стоит; для сотен врагов статические состояния экономят память и сборку мусора.

Параллельные автоматы: движение и оружие

Если герой может бегать, прыгать и при этом держать оружие, нужны два автомата, а не один большой. Нистром показывает: если склеить в одном автомате n состояний движения и m состояний оружия, получится n × m состояний; два параллельных автомата дают n + m.

движение: стоит, бежит, прыжок, падение, присед   n = 5
оружие:   без оружия, пистолет, перезарядка       m = 3

один автомат:  5 × 3 = 15 состояний
два автомата:  5 + 3 = 8 состояний

Работает это, пока автоматы почти не зависят друг от друга. Если «перезарядку нельзя начинать в прыжке», одному автомату придётся спросить состояние другого — это допустимо, но признак того, что зависимостей стало много.

Иерархия: общее суперсостояние «на земле»

Иерархический автомат убирает дублирование одинаковых переходов. В таблице выше «Стоит» и «Бежит» одинаково реагируют на прыжок, «вниз» и потерю опоры. Нистром предлагает суперсостояние OnGround: оно обрабатывает общие входы, а дочерние состояния — только свои. Если дочернее состояние вход не обработало, он передаётся родителю.

class OnGroundState {
  update(hero, input) {
    if (input.jumpPressed) { hero.vy = -hero.jumpSpeed; return new JumpState(); }
    if (input.down) return new CrouchState();
    if (!hero.onGround) return new FallState();
    return null;
  }
}

class RunState extends OnGroundState {
  update(hero, input, dt) {
    if (input.dx === 0) return new IdleState();
    return super.update(hero, input, dt);      // общие переходы — у родителя
  }
}

Стек состояний: урон и возврат

Стек состояний нужен, когда после временного состояния персонаж должен вернуться туда, где был. Нистром разбирает это как автомат с магазинной памятью на примере стрельбы: новое состояние кладётся поверх стека, а по окончании снимается, и герой продолжает прежнее. Для платформера тот же приём подходит к получению урона: отброс и мигание кладутся поверх «бежит» или «падение», а после снятия автомат продолжает с того состояния, которое лежало ниже.

const stack = [new IdleState()];
const current = () => stack[stack.length - 1];

function push(hero, s) { current().exit(hero); stack.push(s); s.enter(hero); }
function pop(hero)     { current().exit(hero); stack.pop(); current().enter(hero); }

// при попадании: push(hero, new HurtState(0.4));
// HurtState через 0,4 с вызывает pop(hero)

Подвох — устаревшее состояние: если за время урона герой упал с платформы, нижнее «бежит» уже неверно. Поэтому в enter нижнего состояния полезно заново проверить условия (есть ли земля) и сразу перейти дальше.

Чек-лист: как перевести героя на автомат

Задача: заменить флаги героя автоматом и убедиться, что исчезли недопустимые комбинации. Проверяемый результат — тест, который 1000 раз жмёт случайные клавиши, и ни разу не встречает «прыжок в воздухе» и «присед в прыжке».

  1. Выпишите все флаги героя и посчитайте комбинации: 2 в степени числа флагов.
  2. Отметьте допустимые — это и есть список состояний.
  3. Составьте таблицу переходов до кода; пустая клетка означает «вход игнорируется».
  4. Начните со switch, если состояний до шести.
  5. Перенесите смену анимации и хитбокса в enter, как только одно и то же присваивание встречается в трёх и более переходах.
  6. Выделите параллельный автомат, если появилось оружие или другой независимый аспект.
  7. Добавьте стек для урона и других временных состояний; в enter нижнего состояния перепроверяйте условия.
  8. Добавьте в отладочный вывод имя текущего состояния: половина багов поведения видна сразу.

Coyote time и буфер прыжка из статьи о прыжке хорошо ложатся на эту схему: окно coyote time — это время, которое состояние «Падение» после схода с края ещё принимает вход прыжка.

Когда вам это не подойдёт

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

  • Сложный ИИ с целями и приоритетами. Нистром прямо пишет, что автоматы не Тьюринг-полны, и для сложного ИИ советует деревья поведения и системы планирования.
  • Очень простой объект. Пуле или монетке с одним поведением автомат не нужен: хватает одного update.
  • Поведение задаётся данными дизайнера. Если дизайнеры правят логику без программиста, удобнее таблица переходов в файле данных или визуальный редактор логики, а код читает её.
  • Физика вместо логики. Если прыжок и падение полностью определяются физическим движком, состояние можно вычислять из скорости и контакта с землёй, а не хранить.

Мы разрабатываем J2DRU — браузерный 2D игровой движок с событийной логикой и путём к TypeScript; он в разработке и публично пока не открыт, заявку можно оставить на странице раннего доступа (подача заявки не гарантирует приглашение).

Источники (проверено: ноябрь 2026): Robert Nystrom, Game Programming Patterns, глава State (gameprogrammingpatterns.com/state.html) — флаги и их баги, определение автомата, enum и switch, паттерн «Состояние», enter и exit, параллельные, иерархические и магазинные автоматы, ограничения автоматов. Таблица переходов, подсчёт комбинаций и код — наш пример.

Читайте также:

Частые вопросы

Зачем конечный автомат, если можно обойтись флагами?

Три булевых флага дают 8 комбинаций, а допустимых состояний обычно меньше. Автомат хранит ровно одно текущее состояние, поэтому недопустимые сочетания вроде «прыгает и приседает» просто невозможно получить.

Что выбрать: switch или объекты-состояния?

switch проще и хорош для 4–6 состояний с малым количеством данных. Объекты-состояния удобнее, когда у состояний есть свои данные, действия входа и выхода и их становится больше десятка.

Что такое параллельные автоматы?

Это два независимых автомата на одном персонаже, например движение и оружие. По Роберту Нистрому, вместо n×m комбинированных состояний остаётся n+m.

Когда конечного автомата уже мало?

Когда поведение врага состоит из множества целей и приоритетов. Автоматы не Тьюринг-полны, и для сложного ИИ Нистром советует смотреть на деревья поведения и системы планирования.