I'm developing a game where the player is a circle and tiles are squares. The user moves the avatar (circle) with the keyboard, and should not be able to collide with tiles (squares).
Also, I would like the circle to slide along the square if they hit the corner, such that if the player keeps pressing the key to move in the same direction, they will slide along the square instead of getting stuck on it.
I've developed a full reproduction of the problem I'm facing here:
let canvas = document.querySelector("canvas");
let ctx = canvas.getContext("2d");
class Vec2 {
constructor(x, y) {
this.x = x || 0;
this.y = y || 0;
}
distance(v) {
let x = v.x - this.x;
let y = v.y - this.y;
return Math.sqrt(x * x + y * y);
}
magnitude() {
return Math.sqrt(this.x * this.x + this.y * this.y);
}
dot(v) {
return this.x * v.x + this.y * v.y;
}
normalize() {
let magnitude = this.magnitude();
return new Vec2(this.x / magnitude, this.y / magnitude);
}
multiply(val) {
return typeof val === "number" ? new Vec2(this.x * val, this.y * val) : new Vec2(this.x * val.x, this.y * val.y);
}
subtract(val) {
return typeof val === "number" ? new Vec2(this.x - val, this.y - val) : new Vec2(this.x - val.x, this.y - val.y);
}
add(val) {
return typeof val === "number" ? new Vec2(this.x + val, this.y + val) : new Vec2(this.x + val.x, this.y + val.y);
}
}
function clamp(value, min, max) {
return Math.min(Math.max(value, min), max);
}
function drawCircle(xCenter, yCenter, radius) {
ctx.beginPath();
ctx.arc(xCenter, yCenter, radius, 0, 2 * Math.PI);
ctx.fill();
}
function drawSquare(x, y, w, h) {
ctx.beginPath();
ctx.rect(x, y, w, h);
ctx.stroke();
}
function circleRectangleCollision(cX, cY, cR, rX, rY, rW, rH) {
let x = clamp(cX, rX, rX + rW);
let y = clamp(cY, rY, rY + rH);
let cPos = new Vec2(cX, cY);
return cPos.distance(new Vec2(x, y)) < cR;
}
function getCircleRectangleDisplacement(rX, rY, rW, rH, cX, cY, cR, cVel) {
let circle = new Vec2(cX, cY);
let nearestX = Math.max(rX, Math.min(cX, rX + rW));
let nearestY = Math.max(rY, Math.min(cY, rY + rH));
let dist = new Vec2(cX - nearestX, cY - nearestY);
let tangentVel = dist.normalize().dot(cVel);
// The original answer had `cVel.subtract(tangentVel * 2);` here
// but that was giving me issues as well
return cVel.add(tangentVel);
}
let circlePos = new Vec2(150, 80);
let squarePos = new Vec2(240, 110);
let circleR = 50;
let squareW = 100;
let squareH = 100;
let circleVel = new Vec2(5, 0);
draw = () => {
ctx.fillStyle = "#b2c7ef";
ctx.fillRect(0, 0, 800, 800);
ctx.fillStyle = "#ffffff";
drawCircle(circlePos.x, circlePos.y, circleR);
drawSquare(squarePos.x, squarePos.y, squareW, squareH);
}
update = () => {
draw();
if (circleRectangleCollision(circlePos.x, circlePos.y, circleR, squarePos.x, squarePos.y, squareW, squareH)) {
circleVel = getCircleRectangleDisplacement(squarePos.x, squarePos.y, squareW, squareH, circlePos.x, circlePos.y, circleR, circleVel);
}
circlePos = circlePos.add(circleVel);
}
setInterval(update, 30);
canvas { display: flex; margin: 0 auto; }
<canvas width="800" height="800"></canvas>
If you run the snippet you'll see that the circle correctly moves around the square, but afterwards it moves down and to the right. I'm not sure why this is happening. It should just keep moving completely straight and to the right afterwards.
Unfortunately I'm not very good at math, so I'm having trouble figuring out why this is happening. I learned of the main algorithm through this answer, but also used the following answers as a reference: One Two Three
Another issue I noticed is that if you change the y position of circlePos from 80 to 240, then it still slides along the top of the square, instead of taking the more natural path of sliding along the bottom of the square. I'd like to fix this as well if possible.
In addition, ideally there shouldn't really be any sliding at all if the circle hits the tile straight on, if that makes sense. It should get stuck against the square in that case.