Столкновение круга с прямоугольником
Необходима небольшая помощь в коде. У меня имеется формула определения столкновения круга и прямоугольника, другими словами проверка коллизии:
rect.x < ball.x + ball.radius &&
rect.x + rect.weight > ball.x - ball.radius &&
rect.y < ball.y + ball.radius &&
rect.height + rect.y > ball.y - ball.radius
Тут неважно какие размеры фигуры, столкновение определяется успешно что по оси Х, что по оси Y. Столкновение так же определяется и при столкновении круга с углом прямоугольника, но, к сожалению, оно происходит слишком рано, т.е. когда круг еще даже не дотронулся до угла, он считает, что столкновение есть. Каким образом можно улучшить этот код, чтобы он идеально определял точное столкновение круга и угла прямоугольника? Благодарю за помощь.
Ответы (1 шт):
Метод взят из MDN
Суть алгоритма в том, чтобы найти ближайшую к центру круга точку прямоугольника и посмотреть насколько она далеко от центра круга. Если расстояние окажется меньше чем радиус круга, то значит они пересекаются

Основная функция:
function checkCollisionCircleWithRect(circle, rect) {
const x = Math.max(rect.minX, Math.min(circle.x, rect.maxX));
const y = Math.max(rect.minY, Math.min(circle.y, rect.maxY));
// Измеряем расстояние от найденной точки до центра круга
const distance = Math.sqrt(
(x - circle.x) ** 2 +
(y - circle.y) ** 2
);
const isCollision = distance < circle.radius;
if (isCollision) console.log('Collision');
}
Рабочий код (круг нужно/можно двигать мышкой и лучше открыть на всю страницу):
const canvas = document.querySelector('#canvas');
const ctx = canvas.getContext('2d');
class Circle {
x;
y;
radius;
color;
path2D;
constructor(x, y, radius, color) {
this.x = x;
this.y = y;
this.radius = radius;
this.color = color;
}
move(x, y) {
this.x = x;
this.y = y;
}
draw() {
this.path2D = new Path2D();
this.path2D.arc(this.x, this.y, this.radius, 0, 2 * Math.PI);
ctx.fillStyle = this.color;
ctx.fill(this.path2D);
}
}
class Rect {
x;
y;
height;
width;
maxX;
minX;
maxY;
minY;
color;
path2D;
constructor(x, y, height, width, color) {
this.x = x;
this.y = y;
this.height = height;
this.width = width;
this.maxX = x + width;
this.minX = x;
this.maxY = y + height;
this.minY = y;
this.color = color;
}
draw() {
this.path2D = new Path2D();
this.path2D.rect(this.x, this.y, this.width, this.height);
ctx.fillStyle = this.color;
ctx.fill(this.path2D);
}
}
const figures = [
new Circle(320, 200, 50, 'red'),
new Rect(100, 100, 100, 150, 'blue')
];
function updateFrame() {
// ОЧИСТКА КАНВАСА
ctx.clearRect(0, 0, canvas.width, canvas.height);
// РИСУЕМ ОБЪЕКТЫ
figures.forEach(figure => {
figure.draw();
});
checkCollisionCircleWithRect(figures[0], figures[1]);
}
function checkCollisionCircleWithRect(circle, rect) {
const x = Math.max(rect.minX, Math.min(circle.x, rect.maxX));
const y = Math.max(rect.minY, Math.min(circle.y, rect.maxY));
// Измеряем расстояние от найденной точки до центра круга
const distance = Math.sqrt(
(x - circle.x) ** 2 +
(y - circle.y) ** 2
);
const isCollision = distance < circle.radius;
if (isCollision) console.log('Collision');
}
let startX = null;
let startY = null;
let obj = null;
canvas.addEventListener('mousedown', (e) => {
const x = e.offsetX;
const y = e.offsetY;
obj = figures[0];
startX = x;
startY = y;
});
canvas.addEventListener('mousemove', (e) => {
if (obj === null) return;
const x = e.offsetX;
const y = e.offsetY;
const diffX = x - startX;
const diffY = y - startY;
obj.move(obj.x + diffX, obj.y + diffY);
startX = x;
startY = y;
});
canvas.addEventListener('mouseup', () => {
obj = null;
});
// НЕ ОБРАЩАЙТЕ ВНИМАНИЕ НА ТО ЧТО НИЖЕ
// ЭТО ПРОСТО НАСТРОЙКА ИГРОВОГО ЦИКЛА
const targetFPS = 60;
const timeInterval = Math.floor(1000 / 60 * (60 / targetFPS));
let previousFrameTime = performance.now();
(function gameLoop(currentFrameTime = performance.now()) {
requestAnimationFrame((timeStamp) => gameLoop(timeStamp));
if (currentFrameTime - previousFrameTime < timeInterval) return;
previousFrameTime = currentFrameTime;
updateFrame();
})();
canvas {
background-color: gray;
}
<canvas id="canvas" width="480" height="320"></canvas>
Спасибо MBo за хорошее замечание
Действительно, у нас код для обнаружения столкновения вызывается в каждои кадре и чем меньше у нас будет вычислений, тем лучше. Предлагается вместо того чтобы каждый раз вычислять квадратный корень для вычисления расстояния, заранее хранить значение радиуса круга в квадрате. Но эта оптимизация будет иметь смысл, если радиус круга не будет меняться. Если же радиус будет меняться, то предлагается вычислять квадрат радиуса вместо квадратного корня, т.к. это быстрее
Значит при инициализации круга, сразу в конструкторе можно это запомнить в отдельном поле и потом использовать где нужно. Тогда общий основная функция будет выглядеть так:
function checkCollisionCircleWithRect(circle, rect) {
const x = Math.max(rect.minX, Math.min(circle.x, rect.maxX));
const y = Math.max(rect.minY, Math.min(circle.y, rect.maxY));
// Измеряем расстояние от найденной точки до центра круга
const distanceSquare = (x - circle.x) ** 2 + (y - circle.y) ** 2;
const isCollision = distanceSquare < circle.radiusSquare;
if (isCollision) console.log('Collision');
}
Рабочий код (круг нужно/можно двигать мышкой и лучше открыть на всю страницу):
const canvas = document.querySelector('#canvas');
const ctx = canvas.getContext('2d');
class Circle {
x;
y;
radius;
radiusSquare;
color;
path2D;
constructor(x, y, radius, color) {
this.x = x;
this.y = y;
this.radius = radius;
this.radiusSquare = radius * radius;
this.color = color;
}
move(x, y) {
this.x = x;
this.y = y;
}
draw() {
this.path2D = new Path2D();
this.path2D.arc(this.x, this.y, this.radius, 0, 2 * Math.PI);
ctx.fillStyle = this.color;
ctx.fill(this.path2D);
}
}
class Rect {
x;
y;
height;
width;
maxX;
minX;
maxY;
minY;
color;
path2D;
constructor(x, y, height, width, color) {
this.x = x;
this.y = y;
this.height = height;
this.width = width;
this.maxX = x + width;
this.minX = x;
this.maxY = y + height;
this.minY = y;
this.color = color;
}
draw() {
this.path2D = new Path2D();
this.path2D.rect(this.x, this.y, this.width, this.height);
ctx.fillStyle = this.color;
ctx.fill(this.path2D);
}
}
const figures = [
new Circle(320, 200, 50, 'red'),
new Rect(100, 100, 100, 150, 'blue')
];
function updateFrame() {
// ОЧИСТКА КАНВАСА
ctx.clearRect(0, 0, canvas.width, canvas.height);
// РИСУЕМ ОБЪЕКТЫ
figures.forEach(figure => {
figure.draw();
});
checkCollisionCircleWithRect(figures[0], figures[1]);
}
function checkCollisionCircleWithRect(circle, rect) {
const x = Math.max(rect.minX, Math.min(circle.x, rect.maxX));
const y = Math.max(rect.minY, Math.min(circle.y, rect.maxY));
const distanceSquare = (x - circle.x) ** 2 + (y - circle.y) ** 2;
const isCollision = distanceSquare < circle.radiusSquare;
if (isCollision) console.log('Collision');
}
let startX = null;
let startY = null;
let obj = null;
canvas.addEventListener('mousedown', (e) => {
const x = e.offsetX;
const y = e.offsetY;
obj = figures[0];
startX = x;
startY = y;
});
canvas.addEventListener('mousemove', (e) => {
if (obj === null) return;
const x = e.offsetX;
const y = e.offsetY;
const diffX = x - startX;
const diffY = y - startY;
obj.move(obj.x + diffX, obj.y + diffY);
startX = x;
startY = y;
});
canvas.addEventListener('mouseup', () => {
obj = null;
});
// НЕ ОБРАЩАЙТЕ ВНИМАНИЕ НА ТО ЧТО НИЖЕ
// ЭТО ПРОСТО НАСТРОЙКА ИГРОВОГО ЦИКЛА
const targetFPS = 60;
const timeInterval = Math.floor(1000 / 60 * (60 / targetFPS));
let previousFrameTime = performance.now();
(function gameLoop(currentFrameTime = performance.now()) {
requestAnimationFrame((timeStamp) => gameLoop(timeStamp));
if (currentFrameTime - previousFrameTime < timeInterval) return;
previousFrameTime = currentFrameTime;
updateFrame();
})();
canvas {
background-color: gray;
}
<canvas id="canvas" width="480" height="320"></canvas>
Алгоритм выше написан для общего случая, если вам надо всё самим вычислять через формулы. Учитвая что вы работаете с canvas-ом в JS, то в основной функции вместо подсчёта расстояния можно использовать isPointInPath для того чтобы выяснить находится ли нужная нам точка внутри круга или нет
Рабочий код (круг нужно/можно двигать мышкой и лучше открыть на всю страницу):
const canvas = document.querySelector('#canvas');
const ctx = canvas.getContext('2d');
class Circle {
x;
y;
radius;
color;
path2D;
constructor(x, y, radius, color) {
this.x = x;
this.y = y;
this.radius = radius;
this.color = color;
}
move(x, y) {
this.x = x;
this.y = y;
}
draw() {
this.path2D = new Path2D();
this.path2D.arc(this.x, this.y, this.radius, 0, 2 * Math.PI);
ctx.fillStyle = this.color;
ctx.fill(this.path2D);
}
}
class Rect {
x;
y;
height;
width;
maxX;
minX;
maxY;
minY;
color;
path2D;
constructor(x, y, height, width, color) {
this.x = x;
this.y = y;
this.height = height;
this.width = width;
this.maxX = x + width;
this.minX = x;
this.maxY = y + height;
this.minY = y;
this.color = color;
}
draw() {
this.path2D = new Path2D();
this.path2D.rect(this.x, this.y, this.width, this.height);
ctx.fillStyle = this.color;
ctx.fill(this.path2D);
}
}
const figures = [
new Circle(320, 200, 50, 'red'),
new Rect(100, 100, 100, 150, 'blue')
];
function updateFrame() {
// ОЧИСТКА КАНВАСА
ctx.clearRect(0, 0, canvas.width, canvas.height);
// РИСУЕМ ОБЪЕКТЫ
figures.forEach(figure => {
figure.draw();
});
checkCollisionCircleWithRect(figures[0], figures[1]);
}
function checkCollisionCircleWithRect(circle, rect) {
const x = Math.max(rect.minX, Math.min(circle.x, rect.maxX));
const y = Math.max(rect.minY, Math.min(circle.y, rect.maxY));
const isCollision = ctx.isPointInPath(circle.path2D, x, y);
if (isCollision) console.log('Collision');
}
let startX = null;
let startY = null;
let obj = null;
canvas.addEventListener('mousedown', (e) => {
const x = e.offsetX;
const y = e.offsetY;
obj = figures[0];
startX = x;
startY = y;
});
canvas.addEventListener('mousemove', (e) => {
if (obj === null) return;
const x = e.offsetX;
const y = e.offsetY;
const diffX = x - startX;
const diffY = y - startY;
obj.move(obj.x + diffX, obj.y + diffY);
startX = x;
startY = y;
});
canvas.addEventListener('mouseup', () => {
obj = null;
});
// НЕ ОБРАЩАЙТЕ ВНИМАНИЕ НА ТО ЧТО НИЖЕ
// ЭТО ПРОСТО НАСТРОЙКА ИГРОВОГО ЦИКЛА
const targetFPS = 60;
const timeInterval = Math.floor(1000 / 60 * (60 / targetFPS));
let previousFrameTime = performance.now();
(function gameLoop(currentFrameTime = performance.now()) {
requestAnimationFrame((timeStamp) => gameLoop(timeStamp));
if (currentFrameTime - previousFrameTime < timeInterval) return;
previousFrameTime = currentFrameTime;
updateFrame();
})();
canvas {
background-color: gray;
}
<canvas id="canvas" width="480" height="320"></canvas>
P.S.
Картинка выше немного неправильно отображает, то как работает алгоритм в JS. Проблема в том, что в canvas-е в JS чем больше y, тем он ниже, а по картинке кажется, что чем больше y, тем выше y. Поэтому в коде у меня maxY = y + height, а не просто y