Патрулирование врагов и читаемые опасности: как настроить честные столкновения и окно предупреждения

Коротко

Честное испытание в 2D-игре отличается от нечестного тремя измеримыми параметрами: у каждой атаки врага есть фаза заметного замаха (телеграфа) длительностью 250–400 мс, у патрульного маршрута между точками A и B есть короткая пауза перед разворотом (400–600 мс), а физическая зона поражения шипов и ловушек уменьшена на 20–30% относительно видимого спрайта. Такой запас учитывает время зрительно-моторной реакции человека и погрешность прямоугольных коллайдеров. В статье разобраны формулы патруля и усечённых хитбоксов, таблица фаз удара в миллисекундах и проверенный в Node.js код врага с фазами замаха, удара и восстановления.

Содержание

  1. Три фазы честной атаки: почему замах должен длиться минимум 250–400 мс
  2. Математика патрулирования между точками A и B с паузой на краю
  3. Прощающий хитбокс: почему зону шипов уменьшают на 20–30%
  4. Контраст и форма: как отличить опасность от декорации за долю секунды
  5. Практический сценарий: патрульный страж с предупреждением перед выпадом на JavaScript
  6. Когда вам это не подойдёт

Три фазы честной атаки: почему замах должен длиться минимум 250–400 мс

Любая читаемая атака врага или срабатывание ловушки в 2D-игре делится на три последовательные фазы: замах (предупреждение / телеграф), активный удар и восстановление (откат), причём фаза замаха не может быть короче 250 мс.

Нижняя граница длительности замаха прямо следует из физиологии человека и задержки ввода:

  • Медианное время простой зрительно-моторной реакции человека на смену цвета экрана в онлайн-выборке Human Benchmark составляет 273 мс (около 16 кадров при 60 к/с; источник: статистика humanbenchmark.com/tests/reactiontime/statistics, проверено: октябрь 2026);
  • В реальной игре человеку нужно не просто нажать единственную кнопку, а распознать позу врага и выбрать действие (прыжок, шаг назад или удар) — по закону Хика—Хаймана (У. Э. Хик, «On the rate of gain of information», Quarterly Journal of Experimental Psychology, 1952 год, проверено: октябрь 2026) время реакции с выбором из 2–3 вариантов возрастает до 300–450 мс.

В классической книге по диснеевской анимации Фрэнка Томаса и Олли Джонстона «The Illusion of Life: Disney Animation» (1981 год, проверено: октябрь 2026) эта подготовительная фаза описана как принцип Anticipation (упреждение / отказное движение): перед рывком вперёд фигура сначала отводится назад, предупреждая зрителя о направлении и силе действия.

Тайминги трёх фаз атаки врага в 2D-экшене и платформере (при 60 к/с, шаг 16,67 мс)
Фаза атакиДлительность в мсЧисло кадров при 60 к/сАктивен ли хитбокс урона?Что видит и слышит игрок
1. Замах (telegraph)300–450 мс18–27 кадровНетВраг останавливается, отводит оружие назад, звучит предупреждающий сигнал
2. Активный удар (strike)100–150 мс6–9 кадровДаБыстрый выпад вперёд, чёткий след удара
3. Восстановление (recovery)400–600 мс24–36 кадровНетВраг открыт для ответного прыжка сверху или обхода

Если включить хитбокс урона сразу в кадре №1 фазы замаха, игрок получит урон от занесённой назад руки врага и воспримет столкновение как ошибку игры.

Математика патрулирования между точками A и B с паузой на краю

Патрулирование — это циклическое движение врага между левой границей minX (точка A) и правой границей maxX (точка B) с фиксированной скоростью speed и обязательной паузой waitTime при достижении края.

Без паузы на развороте координата врага просто отражается в один кадр, из-за чего школьнику или начинающему игроку трудно подгадать момент для перепрыгивания. С паузой поведение описывается переменной направления dir (-1 влево, +1 вправо) и таймером ожидания waitTimer:

если waitTimer > 0:
  waitTimer = waitTimer - dt
  Vx = 0
иначе:
  Vx = dir * speed
  X  = X + Vx * dt
  если X >= maxX:
    X         = maxX
    dir       = -1
    waitTimer = 0.5   // пауза 500 мс на правом краю
  если X <= minX:
    X         = minX
    dir       = +1
    waitTimer = 0.5   // пауза 500 мс на левом краю

Конкретный расчёт цикла патруля: пусть платформа имеет ширину 256 пикселей (8 клеток по 32), враг ходит по отрезку от minX = 224 до maxX = 448 (длина пути L = 224 пикселя) со скоростью speed = 112 пикс/с и останавливается на краях на waitTime = 0,5 с. Тогда:

Время прохода в одну сторону = 224 / 112 = 2,0 с
Полный цикл патруля (туда + пауза + обратно + пауза) = 2,0 + 0,5 + 2,0 + 0,5 = 5,0 с

Каждые 2,5 секунды враг замирает на полсекунды в предсказуемой точке, давая игроку чёткое окно для манёвра.

Прощающий хитбокс: почему зону шипов уменьшают на 20–30%

Прощающий хитбокс (forgiving hitbox) — это намеренное несовпадение размера видимого спрайта и его физического прямоугольника столкновения в пользу игрока: опасные объекты делают физически меньше картинки, а полезные (монеты, аптечки, платформы) — чуть больше.

Когда на клетке 32×32 пикселя нарисованы треугольные шипы, верхняя треть клетки состоит из пустых пикселей между остриями. Если задать шипам полный квадратный хитбокс 32×32, герой погибнет, задев пяткой пустой угол над шипом.

Правило расчёта усечённого прямоугольника опасности внутри клетки шириной W и высотой H со сжатием на 25% по бокам и на 30% сверху:

hitboxW = W * 0.50              // для 32 пикс → 16 пикс (по 8 пикс отступа слева и справа)
hitboxH = H * 0.70              // для 32 пикс → 22 пикс (10 пикс запаса сверху у острия)
offsetX = (W - hitboxW) / 2     // +8 пикс от левого края спрайта
offsetY = H - hitboxH           // +10 пикс от верхнего края (прижат к основанию пола)
Рекомендуемые размеры зон столкновения относительно видимого спрайта <code>32×32</code> пикселя
Тип объекта на сценеРазмер видимого спрайтаРазмер зоны столкновения (W × H)Смещение относительно спрайтаЗачем нужен такой допуск
Хёртбокс игрока (получение урона)32 × 48 пикс22 × 40 пикс (-31% по ширине)По центру, прижат к стопамКасание врага краем плаща или волос не отнимает жизнь
Напольные шипы (опасность)32 × 32 пикс16 × 20 пикс (-50% ширины, -37% высоты)Отступ 8 пикс с боков и 12 пикс сверхуСмерть наступает только при реальном наступании на остриё
Летящий снаряд врага16 × 16 пикс10 × 10 пикс (-37%)По центру (0,5; 0,5)Позволяет увернуться в плотном потоке снарядов
Монета или бонус (награда)16 × 16 пикс24 × 24 пикс (+50%)По центру (0,5; 0,5)Предмет подбирается, даже если герой пролетел в 4 пикселях рядом

Контраст и форма: как отличить опасность от декорации за долю секунды

В динамичной 2D-игре взгляд игрока сфокусирован на персонаже и точке в 100–150 пикселях впереди него, поэтому ловушки должны считываться периферическим зрением по трём визуальным признакам без чтения подписей:

  1. Язык геометрических форм: скруглённые и прямоугольные силуэты мозг считывает как безопасные опоры (платформы, кнопки), а острые треугольные и диагональные зубцы — как угрозу (шипы, пилы, челюсти);
  2. Цветовой код сцены: выберите один сигнальный цвет опасности на весь проект (например, ярко-алый или кислотно-фиолетовый) и никогда не используйте этот оттенок в безобидных фоновых цветах или декоративных кустах на слое параллакса;
  3. Разделение по слоям отрисовки: все объекты, способные нанести урон или остановить движение, размещаются строго на основном игровом слое с коэффициентом параллакса 1,0 и чётким контуром.

Практический сценарий: патрульный страж с предупреждением перед выпадом на JavaScript

Страж, который патрулирует отрезок X = 224..448 с паузой 0,5 с на краях, а при приближении героя ближе 96 пикселей (3 клетки) делает замах 0,35 с и только потом бьёт, описывается конечным автоматом из четырёх состояний и одного таймера. Код ниже проверен в Node.js 24; комментарии показывают его фактический вывод.

// Страж: патруль X = 224..448 со скоростью 112 пикс/с и паузой 0,5 с,
// при игроке ближе 96 пикс — замах 0,35 с, удар 0,15 с, восстановление 0,5 с
const MIN_X = 224, MAX_X = 448, SPEED = 112, WAIT = 0.5, SIGHT = 96;
const PHASES = { windup: 0.35, strike: 0.15, recovery: 0.5 };

function createGuard() { return { x: MIN_X, dir: 1, wait: 0, state: "patrol", timer: 0 }; }

function updateGuard(g, dt, playerX) {
  if (g.state === "patrol") {
    if (Math.abs(playerX - g.x) < SIGHT) { g.state = "windup"; g.timer = PHASES.windup; return; }
    if (g.wait > 0) { g.wait -= dt; return; }
    g.x += g.dir * SPEED * dt;
    if (g.x >= MAX_X) { g.x = MAX_X; g.dir = -1; g.wait = WAIT; }
    if (g.x <= MIN_X) { g.x = MIN_X; g.dir = 1; g.wait = WAIT; }
    return;
  }
  g.timer -= dt;
  if (g.timer > 0) return;
  if (g.state === "windup") { g.state = "strike"; g.timer = PHASES.strike; }
  else if (g.state === "strike") { g.state = "recovery"; g.timer = PHASES.recovery; }
  else { g.state = "patrol"; g.wait = WAIT; }
}
// Урон наносится только в фазе удара: касание во время замаха безопасно
function hurts(g, touching) { return touching && g.state === "strike"; }

// Проверка 1: полный цикл патруля без игрока поблизости
const dt = 1 / 60;
const g = createGuard();
let t = 0, turns = 0, lastDir = g.dir;
while (turns < 2) {
  updateGuard(g, dt, 10000); t += dt;
  if (g.dir !== lastDir) { turns++; lastDir = g.dir; }
}
while (g.wait > 0) { updateGuard(g, dt, 10000); t += dt; }
console.log("цикл патруля, с:", t.toFixed(2));          // 5.03 — 5 с с точностью до кадра 16,67 мс

// Проверка 2: сколько длится безопасный замах перед ударом
const h = createGuard();
let ms = 0;
updateGuard(h, dt, MIN_X + 50);                          // игрок подошёл на 50 пикс
while (h.state === "windup") { updateGuard(h, dt, MIN_X + 50); ms += dt * 1000; }
console.log("замах, мс:", Math.round(ms), "урон при касании в замахе:", hurts({ state: "windup" }, true)); // 367 false — 350 мс с точностью до кадра

Измеренные 5,03 с и 367 мс вместо 5,0 с и 350 мс — цена шага 1/60 с (16,67 мс): таймер переключает состояние в первом кадре, где он стал не больше нуля, поэтому фаза никогда не бывает короче заданной. Зону поражения шипов в той же игре задавайте по формуле усечённого хитбокса из раздела выше, а не по размеру спрайта.

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

Описанная схема конечного автомата из нескольких состояний и прощающих прямоугольных зон имеет свои границы:

  • Стелс-игры и тактики со сложным поиском пути по лабиринту: если 30 врагов должны обходить динамические преграды по сетке (алгоритм A*), слышать шаги сквозь двери и координировать обход с флангов, простого патруля между minX и maxX не хватит — потребуются граф навигации и поиск пути.
  • Файтинги киберспортивного уровня с проверкой попиксельных полигонов: для турнирных дуэлей, где у каждого удара задаются отдельные прямоугольники головы, корпуса и ног на каждый кадр, одного уменьшенного хитбокса мало.
  • Мирные игры для самых маленьких: если цель — исследование без поражения, фазы атаки и зоны урона лишние; достаточно безопасных препятствий и подсказок.

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

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

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

Почему атака врага без предупреждения раздражает игрока?

Медианное время простой зрительной реакции человека составляет около 273 мс (16 кадров при 60 к/с), а реакция с выбором действия занимает 300–400 мс. Если удар происходит мгновенно в первый же кадр сближения, человек физически не может среагировать на него зрением.

Зачем делать хитбокс шипов меньше нарисованного спрайта?

Треугольный шип или пламя имеют узкую верхушку и полупрозрачные края, но прямоугольный хитбокс AABB накрывает всю клетку целиком. Если уменьшить хитбокс опасности на 20–30% с краёв и сверху, игрок не погибнет от касания пустого пикселя воздуха рядом с остриём.

Зачем патрульному врагу пауза на концах маршрута?

Мгновенный разворот без остановки выглядит механически и не даёт игроку предсказуемого окна для прыжка сверху или пробега мимо. Пауза 0,4–0,6 с в точках A и B создаёт читаемый ритм.

Как хранить состояния врага в коде?

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