---
title: "Движение по сетке против свободного движения в 2D-игре: таймер шага, v × dt и гибрид из Pac-Man с доворотом и буфером поворота"
description: "Три способа двигать героя в 2D-игре на JavaScript: шаг по клеткам по таймеру с плавной отрисовкой, свободное движение со скоростью v × dt и гибрид, как в Pac-Man, — плавные пиксели, решения по клетке центра, доворот до центра полосы, окно и буфер поворота, проверка обмена клетками. Пересчёт скорости из клеток в пиксели, код проверен в Node.js."
slug: dvizhenie-po-setke-protiv-svobodnogo-dvizheniya-v-2d-igre
date: 2026-11-03
updated: 2026-11-03
scheduled_at: "2026-11-03T19:00:00+03:00"
rubric: praktika-i-uroki
tags: ["dvizhenie", "setka", "javascript", "novichkam", "gejmdizajn"]
reading_time: 9
draft: false
summary: "Движение по сетке переносит героя из клетки в клетку раз в такт таймера, и столкновения сводятся к сравнению номеров клеток. Свободное движение прибавляет к координатам скорость, умноженную на время кадра, и даёт точность до пикселя, но требует геометрии столкновений. Гибрид, как в Pac-Man (1980), хранит плавные пиксели, а решения принимает по клетке, где находится центр: поворот разрешён в небольшом окне около центра, нажатие запоминается в буфере, а поперечная координата сама доворачивается к центру коридора. Столкновение на сетке проверяют как «одна клетка или обмен клетками»."
---

## Коротко

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

## Содержание

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

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

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

Таблица: Сетка, свободное движение и гибрид

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

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

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

```text
клеток в секунду   = 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` — тот же приём, что фиксированный шаг из статьи об игровом цикле; здесь важно другое: картинку можно рисовать плавно, интерполируя между прошлой и текущей клеткой.

```js
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` раза выше.

```js
// Свободное движение: скорость в пикселях в секунду, умноженная на 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` — сколько секунд помнить нажатие, пока поворот невозможен.
- **Доворот** — поперечная координата сама подтягивается к центру полосы, поэтому после раннего поворота герой идёт по центру коридора, а не по стене.

```js
// Гибрид: плавные пиксели, решения по клетке центра, доворот и буфер поворота.
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).

```js
// Столкновение на сетке: одна клетка ИЛИ обмен клетками за один шаг.
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 игровой движок; он в разработке и публично пока не открыт, заявку можно оставить на [странице раннего доступа](/early-access?utm_source=blog&utm_medium=article&utm_campaign=dvizhenie-po-setke-protiv-svobodnogo-dvizheniya-v-2d-igre) (подача заявки не гарантирует приглашение).

**Источники (проверено: ноябрь 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 с и пересчёт скорости — наши.

**Читайте также:**
- [Игровой цикл и физика столкновений AABB](/blog/igrovoj-cikl-i-fizika-stolknovenij-aabb)
- [Поиск пути A* на тайловой сетке](/blog/algoritm-a-star-poiska-puti-na-tajlovoj-setke)
- [Действия ввода вместо клавиш: KeyboardEvent.code, Gamepad API, мёртвая зона](/blog/vvod-klaviatura-gejmpad-kasanie-v-2d-igre)
