Столкновения
Без столкновений нет игры
Пуля попала во врага, герой встал на платформу, мяч отбился от ракетки, змейка съела яблоко — всё это проверка столкновений: пересекаются ли две фигуры прямо сейчас. Холст этого не знает (на нём только пиксели), поэтому столкновения считают по числам из состояния игры.
В большинстве 2D-игр объекты для столкновений упрощают до двух фигур: прямоугольника и круга. Даже если на экране нарисован сложный персонаж, его «хитбокс» — простой прямоугольник.
Прямоугольник с прямоугольником (AABB)
Прямоугольники, стороны которых параллельны краям экрана, называют AABB (axis-aligned bounding box). Проверка — от обратного: прямоугольники не пересекаются, если один целиком левее, правее, выше или ниже другого. Во всех остальных случаях — пересекаются.
function rectsOverlap(a, b) {
return (
a.x < b.x + b.w && // левый край a левее правого края b
a.x + a.w > b.x && // правый край a правее левого края b
a.y < b.y + b.h && // верх a выше низа b
a.y + a.h > b.y // низ a ниже верха b
);
}
Строгие < и > означают, что касание краями — ещё не столкновение. Если в игре нужно иначе,
меняют на <= и >=.
Круг с кругом
Круги пересекаются, когда расстояние между центрами меньше суммы радиусов:
function circlesOverlap(a, b) {
const dist = Math.hypot(a.x - b.x, a.y - b.y); // √(dx² + dy²)
return dist < a.r + b.r;
}
Math.hypot(dx, dy) — длина гипотенузы, то есть расстояние по теореме Пифагора. Когда объектов
очень много, корень не считают и сравнивают квадраты: dx * dx + dy * dy < (a.r + b.r) ** 2.
Круг с прямоугольником
Мяч и кирпич, монета и платформа. Точный способ — найти точку прямоугольника, ближайшую к центру круга, и проверить расстояние до неё:
function circleRect(c, r) {
const nearX = Math.max(r.x, Math.min(c.x, r.x + r.w));
const nearY = Math.max(r.y, Math.min(c.y, r.y + r.h));
return Math.hypot(c.x - nearX, c.y - nearY) < c.r;
}
В простых аркадах часто делают ещё проще: считают круг квадратом со стороной 2r и используют
rectsOverlap. Ошибка заметна только на углах.
Отскок от стен
Мяч не должен вылетать за поле. Проверяем каждую стену: если мяч в неё залез — возвращаем его на границу и разворачиваем скорость:
if (ball.x - ball.r < 0) { // левая стена
ball.x = ball.r;
ball.vx = Math.abs(ball.vx); // теперь точно летит вправо
}
if (ball.x + ball.r > canvas.width) { // правая стена
ball.x = canvas.width - ball.r;
ball.vx = -Math.abs(ball.vx); // теперь точно летит влево
}
Почему не ball.vx = -ball.vx? За один кадр мяч может залезть в стену глубоко. Если только
развернуть скорость, на следующем кадре он может ещё не выбраться — и скорость развернётся снова, мяч
«прилипнет» и задрожит. Возврат на границу плюс Math.abs делают отскок надёжным.
Что делать после столкновения
Столкновение — это только «да / нет». Дальше решает игра:
for (const enemy of enemies) {
if (rectsOverlap(bullet, enemy)) {
enemy.alive = false;
bullet.alive = false;
score += 10;
}
}
enemies = enemies.filter((e) => e.alive);
Удалять из массива прямо во время перебора опасно (индексы сдвигаются), поэтому сначала помечают
alive = false, а потом убирают через filter.
Слишком быстрые объекты
Если пуля за кадр пролетает 50 пикселей, а стена толщиной 10, на одном кадре пуля ещё перед стеной, а на следующем — уже за ней, и пересечения не случится. Это называется «туннелирование». Простые средства: ограничить скорость, сделать объекты толще или двигать быстрые объекты в несколько мелких шагов за кадр.
Итог
- Хитбоксы — прямоугольники и круги, проверяем числа из состояния, а не пиксели.
- AABB: четыре сравнения краёв; круги:
Math.hypot(dx, dy) < r1 + r2. - Отскок: вернуть на границу и задать направление через
Math.abs. - После столкновения помечаем объекты и убираем
filter-ом.