Коротко
Движение по сетке переносит героя в соседнюю клетку раз в такт таймера, свободное движение каждый кадр прибавляет к координатам v × dt, а гибрид, как в Pac-Man, сочетает плавные пиксели с решениями по клеткам. Сетка проста и предсказуема, свободное движение точно до пикселя, гибрид даёт плавность и честные повороты в коридорах. В статье — код всех трёх вариантов на JavaScript с проверкой в Node.js, окно и буфер поворота, доворот до центра полосы и проверка обмена клетками против прохода сквозь врага.
Содержание
- Три способа двигать героя: таблица отличий
- Пересчёт скорости: клетки, пиксели и сдвиг за кадр
- Движение по сетке: таймер шага и плавная отрисовка
- Свободное движение: v × dt и диагональ
- Гибрид из Pac-Man: доворот, окно и буфер поворота
- Проход сквозь врага: проверка обмена клетками
- Практический сценарий: лабиринт с коридорами в одну клетку
- Когда вам это не подойдёт
Три способа двигать героя: таблица отличий
Главное отличие — где живёт «правда» о положении: в номере клетки, в пикселях или в пикселях с решениями по клетке. От этого зависят столкновения, повороты и ощущение от управления.
| Свойство | По сетке | Свободное (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 пикселя, герой движется плавно, повороты не теряются, а враг не проходит сквозь героя.
- Карта. Храните лабиринт строками (
#— стена,.— проход) и функциейisFree(cx, cy); поиск пути для врагов — в статье об A*. - Скорость. Задайте скорость в клетках в секунду и пересчитайте в пиксели:
7,14 × 32 ≈ 228,6 пикс/с. - Герой.
createHybridMover(map, 1, 1, 228.6, 4, 0.2); нажатие стрелки —hero.press("down"), без проверки, возможен ли поворот прямо сейчас. - Враги. Тот же гибрид с окном
0: поворот строго в центре, как у привидений. - Столкновения. Каждый кадр сохраняйте прошлую клетку центра и вызывайте
caught()для героя и каждого врага. - Проверка. Запустите игру с принудительными
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 с и пересчёт скорости — наши.
Читайте также: