I'm trying to implement collision detection with raycasting for my 2D javascript game. The game uses equally-sized tiles as its level structure so to optimize collision detecton I'm trying to use a LOS algorithm very similar to the one at the bottom of this this article.
However, I've noticed that at certain angles, tiles that should be detected are not. I'm also noticing an overall "L" shape pattern where tiles that are beyond the ray's endpoint are being detected when they don't need to be. I don't think these are intended behaviors of the algorithm but I can't quite figure out what I could be doing wrong.
I've assembled a small jsfiddle here that includes the most relevant code and an additional visualization. You can click anywhere on the canvas to set a new ray origin.
Would appreciate any help. Thanks.
const GRID_HEIGHT = 10
const GRID_WIDTH = 10
const CELLSIZE = 32
const OUTLINE_COLOUR = 'green';
const FILL_COLOUR = 'red'
const RAY_COLOUR = 'blue';
const canvas = document.getElementById('demo');
const ctx = canvas.getContext('2d');
var mouseVector = new Vector2D([96, 0], [1, 0]);
canvas.height = GRID_HEIGHT * CELLSIZE;
canvas.width = GRID_WIDTH * CELLSIZE
canvas.addEventListener("mousemove", updateMouseVector);
canvas.addEventListener("click", setMouseVectorOrigin);
function Vector2D(point1, point2) {
if (point2 != undefined) {
this.p1 = point1;
this.p2 = point2;
} else {
this.p1 = [0, 0];
this.p2 = point1;
};
};
function drawCellOutline(x, y) {
ctx.fillStyle = OUTLINE_COLOUR;
ctx.strokeRect(x, y, CELLSIZE, CELLSIZE);
};
function fillCell(x, y) {
ctx.fillStyle = FILL_COLOUR;
ctx.fillRect(x, y, CELLSIZE, CELLSIZE);
};
function drawGrid() {
for (let y = 0; y < GRID_HEIGHT; y++) {
for (let x = 0; x < GRID_WIDTH; x++) {
drawCellOutline(x * CELLSIZE, y * CELLSIZE);
};
};
};
function clearCanvas() {
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height)
};
function getMouseCoordinates(e) {
let mouseX, mouseY;
if (e.offsetX) {
mouseX = e.offsetX;
mouseY = e.offsetY;
} else if (e.layerX) {
mouseX = e.layerX;
mouseY = e.layerY;
}
return [mouseX, mouseY]
}
function updateMouseVector(e) {
let mousePos = getMouseCoordinates(e);
mouseVector.p2[0] = mousePos[0];
mouseVector.p2[1] = mousePos[1];
}
function setMouseVectorOrigin(e) {
let mousePos = getMouseCoordinates(e);
mouseVector.p1[0] = mousePos[0];
mouseVector.p1[1] = mousePos[1];
};
function drawRay(rayVector) {
ctx.fillStyle = RAY_COLOUR;
ctx.beginPath();
ctx.moveTo(rayVector.p1[0], rayVector.p1[1]);
ctx.lineTo(rayVector.p2[0], rayVector.p2[1]);
ctx.stroke();
}
function raycastCollision(rayVector) {
let dx = Math.ceil(Math.abs(rayVector.p2[0] - rayVector.p1[0]) / CELLSIZE);
let dy = Math.ceil(Math.abs(rayVector.p2[1] - rayVector.p1[1]) / CELLSIZE);
let x_inc = (rayVector.p2[0] > rayVector.p1[0]) ? 1 : -1;
let y_inc = (rayVector.p2[1] > rayVector.p1[1]) ? 1 : -1;
x_inc *= CELLSIZE;
y_inc *= CELLSIZE;
// "error" is The difference between the next horizontal cell vs the next vertical cell.
// if error is positive then horizontal is closer, otherwise vertical.
let error = dx - dy;
let pos = [rayVector.p1[0], rayVector.p1[1]]; // Create copy because we're going to be mutating it.
let n = 1 + dx + dy; // 1 represents thw tile we're starting from.
for (; n > 0; --n) {
fillCell(Math.floor(pos[0] / CELLSIZE) * 32, Math.floor(pos[1] / CELLSIZE) * 32);
if (error > 0) {
pos[0] += x_inc;
error -= dy;
} else {
pos[1] += y_inc;
error += dx;
};
};
};
function main() {
clearCanvas();
drawGrid();
raycastCollision(mouseVector);
drawRay(mouseVector);
requestAnimationFrame(main);
};
requestAnimationFrame(main);
<canvas id="demo"></canvas>