Движение по сетке против свободного движения в 2D-игре: таймер шага, v × dt и гибрид из Pac-Man с доворотом и буфером поворота

Коротко

Движение по сетке переносит героя в соседнюю клетку раз в такт таймера, свободное движение каждый кадр прибавляет к координатам v × dt, а гибрид, как в Pac-Man, сочетает плавные пиксели с решениями по клеткам. Сетка проста и предсказуема, свободное движение точно до пикселя, гибрид даёт плавность и честные повороты в коридорах. В статье — код всех трёх вариантов на JavaScript с проверкой в Node.js, окно и буфер поворота, доворот до центра полосы и проверка обмена клетками против прохода сквозь врага.

Содержание

  1. Три способа двигать героя: таблица отличий
  2. Пересчёт скорости: клетки, пиксели и сдвиг за кадр
  3. Движение по сетке: таймер шага и плавная отрисовка
  4. Свободное движение: v × dt и диагональ
  5. Гибрид из Pac-Man: доворот, окно и буфер поворота
  6. Проход сквозь врага: проверка обмена клетками
  7. Практический сценарий: лабиринт с коридорами в одну клетку
  8. Когда вам это не подойдёт

Три способа двигать героя: таблица отличий

Главное отличие — где живёт «правда» о положении: в номере клетки, в пикселях или в пикселях с решениями по клетке. От этого зависят столкновения, повороты и ощущение от управления.

Сетка, свободное движение и гибрид
СвойствоПо сеткеСвободное (v × dt)Гибрид
Где хранится положениеНомер клеткиПикселиПиксели + клетка центра
Как выглядитРывки или плавно с интерполяциейПлавноПлавно
Столкновения со стенамиПроверка одной клеткиГеометрия прямоугольниковПроверка клетки впереди
ПоворотыТолько в клеткеВ любой точкеВ окне около центра клетки
Типичные игры«Змейка», Sokoban, пошаговые игрыПлатформеры, шутеры сверхуЛабиринты в духе Pac-Man
Чем рискуемОтклик «раз в такт»Застревание на углах в узких коридорахНужно настроить окно и буфер

Пересчёт скорости: клетки, пиксели и сдвиг за кадр

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

клеток в секунду   = 1 ÷ шаг таймера
пикселей в секунду = клеток в секунду × размер клетки
сдвиг за кадр      = пикселей в секунду ÷ частота кадров

шаг 0,14 с, клетка 32 пикс:
1 ÷ 0,14  ≈ 7,14 клетки/с
7,14 × 32 ≈ 228,6 пикс/с
228,6 ÷ 60 ≈ 3,81 пикс за кадр при 60 Гц   (7,62 пикс при 30 Гц)

Для сравнения: в Pac-Man экран 224 × 288 пикселей делится на сетку 28 × 36 клеток по 8 × 8 пикселей (Jamey Pittman, The Pac-Man Dossier, проверено: ноябрь 2026).

Движение по сетке: таймер шага и плавная отрисовка

При движении по сетке герой стоит в целой клетке и перескакивает в соседнюю раз в stepTime секунд. Накопление времени по кадрам и цикл while — тот же приём, что фиксированный шаг из статьи об игровом цикле; здесь важно другое: картинку можно рисовать плавно, интерполируя между прошлой и текущей клеткой.

const TILE = 32;
const DIRS = { left: [-1, 0], right: [1, 0], up: [0, -1], down: [0, 1], none: [0, 0] };

// Карта из строк: '#' — стена, '.' — проход.
function makeMap(rows) {
  return {
    isFree: function (cx, cy) {
      return cy >= 0 && cy < rows.length && cx >= 0 && cx < rows[cy].length && rows[cy][cx] !== "#";
    },
  };
}

// Движение по сетке: шаг раз в stepTime секунд, между шагами — только отрисовка.
function createGridMover(map, cx, cy, stepTime) {
  const m = { cx: cx, cy: cy, prevX: cx, prevY: cy, dir: "none", t: 0 };
  m.update = function (dt) {
    m.t += dt;
    while (m.t >= stepTime) {           // за длинный кадр может набраться несколько шагов
      m.t -= stepTime;
      m.prevX = m.cx; m.prevY = m.cy;
      const d = DIRS[m.dir];
      if (map.isFree(m.cx + d[0], m.cy + d[1])) { m.cx += d[0]; m.cy += d[1]; }
    }
  };
  // Плавная картинка: рисуем между прошлой и текущей клеткой (отставание до одного шага).
  m.drawPos = function () {
    const k = m.t / stepTime;
    return {
      x: (m.prevX + (m.cx - m.prevX) * k + 0.5) * TILE,
      y: (m.prevY + (m.cy - m.prevY) * k + 0.5) * TILE,
    };
  };
  return m;
}

Проверка в Node.js: при шаге 0,14 с за 0,5 с герой проходит 3 клетки и при 60, и при 144 кадрах в секунду, а позиция отрисовки в обоих случаях одинакова — 130,3 пикселя. Цена плавности — отставание картинки от логики до одного шага (0,14 с); для «Змейки» и головоломок это незаметно, для динамичной игры — заметно.

Свободное движение: v × dt и диагональ

При свободном движении координаты каждый кадр получают прибавку «скорость × время кадра», поэтому путь за секунду не зависит от частоты экрана. Вторая ловушка — диагональ: без нормализации вектор (1, 1) даёт скорость в √2 ≈ 1,41 раза выше.

// Свободное движение: скорость в пикселях в секунду, умноженная на dt.
function moveFree(obj, input, speed, dt) {
  let vx = input.x, vy = input.y;
  const len = Math.hypot(vx, vy);
  if (len > 1) { vx /= len; vy /= len; }   // по диагонали не быстрее, чем по прямой
  obj.x += vx * speed * dt;
  obj.y += vy * speed * dt;
}

Проверка в Node.js: за 1 с по диагонали герой проходит 228,6 пикселя и при 60, и при 144 кадрах в секунду; без нормализации было бы 323,3 пикселя. Столкновения прямоугольников и проход быстрых объектов сквозь стены разобраны в статье об игровом цикле и AABB — здесь их не повторяем.

Гибрид из Pac-Man: доворот, окно и буфер поворота

Гибрид хранит плавные пиксели, а решения принимает по клетке, в которой находится центр героя. Именно так описан Pac-Man (Namco, вышел в Японии 22 мая 1980 года): у каждого персонажа одна клетка — та, где его центральная точка, хотя спрайт перекрывает соседние (The Pac-Man Dossier).

В Pac-Man привидения поворачивают только в центре клетки, а Пакман может начать поворот за несколько пикселей до центра и закончить после него: при заходе слева — 3 пикселя до центра и 4 после, при заходе справа — наоборот; пока поворот не завершён, он идёт по диагонали, а за каждый пиксель опоздания с поворотом отстаёт на кадр (The Pac-Man Dossier, раздел Cornering). Отсюда три механизма гибрида:

  • Окно поворота turnWindow — на сколько пикселей от центра клетки разрешён поворот.
  • Буфер поворота bufferTime — сколько секунд помнить нажатие, пока поворот невозможен.
  • Доворот — поперечная координата сама подтягивается к центру полосы, поэтому после раннего поворота герой идёт по центру коридора, а не по стене.
// Гибрид: плавные пиксели, решения по клетке центра, доворот и буфер поворота.
function createHybridMover(map, cx, cy, speed, turnWindow, bufferTime) {
  const m = { pos: [(cx + 0.5) * TILE, (cy + 0.5) * TILE], dir: "none", wanted: "none", wantedAge: 0 };
  const cellOf = function (v) { return Math.floor(v / TILE); };   // клетка = где центр
  const center = function (c) { return (c + 0.5) * TILE; };

  m.cell = function () { return [cellOf(m.pos[0]), cellOf(m.pos[1])]; };
  m.press = function (dir) { m.wanted = dir; m.wantedAge = 0; };

  m.update = function (dt) {
    const c = m.cell();
    const off = [m.pos[0] - center(c[0]), m.pos[1] - center(c[1])];

    // 1. Буфер поворота: нажатие живёт bufferTime секунд и ждёт, пока поворот станет возможен.
    if (m.wanted !== "none") {
      m.wantedAge += dt;
      const w = DIRS[m.wanted];
      const across = w[0] !== 0 ? 1 : 0;              // ось, на которой нужно быть у центра
      const free = map.isFree(c[0] + w[0], c[1] + w[1]);
      if (Math.abs(off[across]) <= turnWindow && free) { m.dir = m.wanted; m.wanted = "none"; }
      else if (m.wantedAge > bufferTime) m.wanted = "none";
    }
    if (m.dir === "none") return;

    const d = DIRS[m.dir];
    const axis = d[0] !== 0 ? 0 : 1, other = 1 - axis;
    const step = speed * dt;

    // 2. Доворот: поперечная координата подтягивается к центру своей полосы.
    m.pos[other] -= Math.sign(off[other]) * Math.min(Math.abs(off[other]), step);

    // 3. Движение вдоль оси; если впереди стена — остановка в центре клетки.
    const next = m.pos[axis] + d[axis] * step;
    const blocked = !map.isFree(c[0] + d[0], c[1] + d[1]);
    if (blocked && (next - center(c[axis])) * d[axis] >= 0) {
      m.pos[axis] = center(c[axis]);
      m.dir = "none";
    } else {
      m.pos[axis] = next;
    }
  };
  return m;
}

Проверка в Node.js на карте с развилкой (клетка 32 пикселя, 228,6 пикс/с, 60 Гц):

Результаты проверки гибрида
ОкноБуферНажатие «вниз»Результат
4 пикс0,25 сЗа ≈ 41 пиксель до центра развилкиПоворот за 3,04 пикселя до центра, остановка в центре нижней клетки (3, 3)
4 пикс0,05 сТо жеНажатие забыто, герой проехал развилку и встал у стены в (5, 1)
1 пикс0,25 с, но 30 ГцТо жеГерой проскочил окно: сдвиг 7,62 пикселя за кадр перепрыгнул зону ±1 пикс
4 пикс0,25 с, 30 ГцТо жеПоворот за 3,04 пикселя до центра

Правило из последних двух строк: окно поворота должно быть не меньше половины сдвига за кадр, иначе при низкой частоте кадров поворот будет теряться. Для 228,6 пикс/с это ≥ 1,9 пикселя при 60 Гц и ≥ 3,81 пикселя при 30 Гц; запас «окно 4 пикселя, буфер 0,2 с» — наш стартовый выбор.

Проход сквозь врага: проверка обмена клетками

Если столкновение на сетке проверять только как «оба в одной клетке», два объекта, которые в один шаг меняются клетками навстречу друг другу, проходят друг сквозь друга. В Pac-Man столкновение с привидением — это нахождение в одной клетке, и этого хватает более чем в 99% случаев, но если центры Пакмана и привидения за одну 1/60 с одновременно переходят в клетки друг друга, столкновения не происходит (The Pac-Man Dossier, раздел Just Passing Through).

// Столкновение на сетке: одна клетка ИЛИ обмен клетками за один шаг.
function caught(a, b) {
  const same = a.cx === b.cx && a.cy === b.cy;
  const swapped = a.cx === b.prevX && a.cy === b.prevY && b.cx === a.prevX && b.cy === a.prevY;
  return same || swapped;
}

Проверка в Node.js: для героя, перешедшего из (3, 1) в (4, 1), и врага, перешедшего из (4, 1) в (3, 1), проверка «одна клетка» даёт false, а caught() — true. Для гибрида храните клетку центра на прошлом и текущем кадре и проверяйте так же.

Практический сценарий: лабиринт с коридорами в одну клетку

Задача: лабиринт с коридорами шириной 32 пикселя, герой движется плавно, повороты не теряются, а враг не проходит сквозь героя.

  1. Карта. Храните лабиринт строками (# — стена, . — проход) и функцией isFree(cx, cy); поиск пути для врагов — в статье об A*.
  2. Скорость. Задайте скорость в клетках в секунду и пересчитайте в пиксели: 7,14 × 32 ≈ 228,6 пикс/с.
  3. Герой. createHybridMover(map, 1, 1, 228.6, 4, 0.2); нажатие стрелки — hero.press("down"), без проверки, возможен ли поворот прямо сейчас.
  4. Враги. Тот же гибрид с окном 0: поворот строго в центре, как у привидений.
  5. Столкновения. Каждый кадр сохраняйте прошлую клетку центра и вызывайте caught() для героя и каждого врага.
  6. Проверка. Запустите игру с принудительными 30 кадрами в секунду и убедитесь, что повороты не теряются.
Стартовые параметры
ПараметрЗначениеПочему
Клетка32 пиксУдобно для спрайтов 32 × 32
Скорость228,6 пикс/с7,14 клетки/с, как при шаге 0,14 с
Окно поворота4 пиксБольше половины сдвига за кадр и при 30 Гц
Буфер поворота0,2 сНажатие чуть заранее не теряется
Окно врага0 пиксПовороты врагов предсказуемы для игрока

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

Гибрид хорош для лабиринтов, но есть случаи, где он лишний или мешает:

  • Платформер с прыжками и инерцией. Гравитация, ускорение и наклонные поверхности требуют свободного движения с физикой; сетка нужна разве что для уровня.
  • Пошаговая игра или головоломка. Если ход — это действие игрока, а не время, таймер шага и доворот не нужны: двигайте на клетку по нажатию.
  • Коридоры шире одной клетки или открытые пространства. Доворот к центру полосы будет мешать движению вдоль стены; нужна обычная геометрия столкновений.
  • Очень быстрые объекты. Если сдвиг за кадр больше половины клетки, проверка «клетка впереди» начинает пропускать стены; уменьшайте скорость или дробите шаг.

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

Источники (проверено: ноябрь 2026): Jamey Pittman, The Pac-Man Dossier (pacman.holenet.info): дата выхода 22 мая 1980 года, экран 224 × 288 и сетка 28 × 36 клеток по 8 пикселей, клетка по центральной точке, повороты Пакмана до и после центра (3 и 4 пикселя), столкновение по одной клетке и проход сквозь привидение при обмене клетками за 1/60 с. Код, результаты проверки в Node.js, окно 4 пикселя, буфер 0,2 с и пересчёт скорости — наши.

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

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

Что выбрать для лабиринта: сетку или свободное движение?

Для лабиринта с коридорами шириной в клетку удобнее гибрид: герой движется плавно, но всегда идёт по центру коридора, а повороты и столкновения решаются по клеткам. Чистая сетка подходит пошаговым играм и головоломкам, свободное движение — платформерам и играм с физикой.

Как пересчитать скорость из клеток в пиксели?

Клеток в секунду = 1 ÷ шаг таймера, пикселей в секунду = клеток в секунду × размер клетки. При шаге 0,14 с и клетке 32 пикселя это около 7,14 клетки/с, или 228,6 пикселя/с, то есть около 3,81 пикселя за кадр при 60 Гц.

Зачем нужен буфер поворота?

Игрок нажимает «вниз» чуть раньше, чем герой дошёл до развилки. Без буфера нажатие теряется, и герой проскакивает поворот. Буфер хранит нажатие 0,15–0,25 с и выполняет поворот, как только он становится возможен.

Почему герой проходит сквозь врага на сетке?

Если проверять только «оба в одной клетке», два объекта, которые за один шаг меняются клетками навстречу друг другу, ни разу не окажутся в одной клетке. Так возникал известный проход сквозь привидение в Pac-Man. Проверяйте ещё и обмен клетками: моя новая клетка — его прошлая, и наоборот.