Raytracing vision cones on a 2D tilemap

Example source code


<!DOCTYPE html>
<html>
<head>

<script type="text/javascript">
var ctx = null;
var tileW = 20, tileH = 20;
var gridW = 30, gridH = 20;
var circleX = 15, circleY = 10, radius = 8;
var mapData = new Array();
var rayData = {};
var visionDirection = 0, visionRadius = (Math.PI/3);
var delayRotation = 4000, delayUpdate = 100;
var bluePos = [3, 3], blueVisible = false;

var currentSecond = 0, frameCount = 0, framesLastSecond = 0;
var gameTime = 0, lastFrame = 0, lastUpdate = 0;

window.onload = function() {
	ctx = document.getElementById('game').getContext('2d');
	ctx.font = "bold 10pt sans-serif";
	
	// Create our "map" and set random 10 tiles as impassable (0)
	for(var i = 0; i < (gridW*gridH); i++) { mapData.push(1); }
	for(var i = 0; i < 10; i++)
	{
		// Choose a random point on the map
		var randPos = (Math.floor(Math.random()*20000)%(gridW*gridH));
		
		// Check it's not the centre of the circle
		if(randPos==((circleY*gridW)+circleX)) { continue; }
		
		// Set the value of this position to 0
		mapData[randPos] = 0;
	}
	
	game.addEventListener('mouseup', function(e) {
		// Get the position of the mouse click on the page
		var mouseX = e.pageX;
		var mouseY = e.pageY;

		// Find the offset of the Canvas relative to the document top, left,
		// and modify the mouse position to account for this
		var p = game;
		do
		{
			mouseX-= p.offsetLeft;
			mouseY-= p.offsetTop;

			p = p.offsetParent;
		} while(p!=null);

		// fit the real mouse position to our grid
		mouseX = Math.floor(mouseX / tileW);
		mouseY = Math.floor(mouseY / tileH);
		
		// Set this blocking point to on or off, UNLESS this is the central point...
		if(mouseX!=circleX || mouseY!=circleY)
		{
			bluePos = [mouseX, mouseY];
		}
	});
	
	requestAnimationFrame(drawGame);
};

function updateRayData(cx, cy, cr, arcStart, arcEnd)
{
	var edges = pointsInArc(cx, cy, cr, arcStart, arcEnd);
	var points = {};
	
	for(x in edges)
	{
		var line = pointsOnLine(cx, cy, edges[x][0], edges[x][1]);
		
		for(l in line)
		{
			points[((line[l][1]*gridW)+line[l][0])] = [line[l][0], line[l][1]];
			if(mapData[((line[l][1]*gridW)+line[l][0])]==0) { break; }
		}
	}
	
	return points;
}

function pointsInArc(cx, cy, cr, angleStart, angleEnd)
{
	var list	= pointsOnCircumference(cx, cy, cr);
	var arc		= new Array();
	
	for(i in list)
	{
		var a = getAngle(cx, cy, list[i][0], list[i][1]);

		if(angleStart < 0 && (a >= (Math.PI*2 + angleStart))) { arc.push(list[i]); }
		else if(angleEnd > (Math.PI*2) && a <= (angleEnd - (Math.PI*2))) { arc.push(list[i]); }
		else if(a>=angleStart && a<=angleEnd) { arc.push(list[i]); }
	}
	
	return arc;
}

function pointsOnLine(x1, y1, x2, y2)
{
	line = new Array();

	var dx = Math.abs(x2 - x1);
	var dy = Math.abs(y2 - y1);
	var x = x1;
	var y = y1;
	var n = 1 + dx + dy;
	var xInc = (x1 < x2 ? 1 : -1);
	var yInc = (y1 < y2 ? 1 : -1);
	var error = dx - dy;

	dx *= 2;
	dy *= 2;

	while(n>0)
	{
		line.push([x, y]);

		if(error>0)
		{
			x+= xInc;
			error-= dy;
		}
		else
		{
			y+= yInc;
			error+= dx;
		}

		n-= 1;
	}

	return line;
}

function pointsOnCircumference(cx, cy, cr)
{
	var list = new Array();
	
	var x = cr;
	var y = 0;
	var o2 = Math.floor(1 - x);

	while(y <= x)
	{
		list.push([ x + cx,  y + cy]);
		list.push([ y + cx,  x + cy]);
		list.push([-x + cx,  y + cy]);
		list.push([-y + cx,  x + cy]);
		list.push([-x + cx, -y + cy]);
		list.push([-y + cx, -x + cy]);
		list.push([ x + cx, -y + cy]);
		list.push([ y + cx, -x + cy]);

		y+= 1;

		if(o2 <= 0) { o2+= (2 * y) + 1; }
		else
		{
			x-= 1;
			o2+= (2 * (y - x)) + 1;
		}
	}

	return list;
}

function getAngle(x, y, x2, y2)
{
	var a = Math.atan2(y2 - y, x2 - x);
	return a < 0 ? a + (Math.PI * 2) : a;
}

function drawGame()
{
	if(ctx==null) { return; }
	
	// Framerate & game time calculations
	var sec = Math.floor(Date.now()/1000);
	if(sec!=currentSecond)
	{
		currentSecond = sec;
		framesLastSecond = frameCount;
		frameCount = 1;
	}
	else { frameCount++; }
	
	var now = Date.now();
	gameTime+= (now-lastFrame);
	lastFrame = now;
	
	// Update the vision
	if(delayUpdate<(now-lastUpdate))
	{
		visionDirection = ((Math.PI*2) / delayRotation) * (gameTime % delayRotation);
		
		rayData = updateRayData(circleX, circleY, radius,
			(visionDirection-(visionRadius/2)), (visionDirection+(visionRadius/2)));

		lastUpdate = now;
		
		// Is the blue target visible?
		blueVisible = (typeof rayData[((bluePos[1]*gridW)+bluePos[0])]!='undefined');
	}

	// Clear the Canvas
	ctx.fillStyle = "#ffffff";
	ctx.fillRect(0, 0, 600, 400);
	
	// Draw rays
	ctx.fillStyle = "#dddd00";
	for(r in rayData)
	{
		ctx.fillRect(rayData[r][0]*tileW, rayData[r][1]*tileH, tileW, tileH);
	}

	// Draw the grid
	ctx.fillStyle = "#000000";
	ctx.strokeStyle = "#999999";
	ctx.beginPath();
	for(y = 0; y < gridH; ++y)
	{
		for(x = 0; x < gridW; ++x)
		{
			// Draw a blocking point here?
			if(mapData[((y*gridW)+x)]==0) { ctx.fillRect((x*tileW), (y*tileH), tileW, tileH); }
			
			// Draw the grid lines
			ctx.rect((x*tileW), (y*tileH), tileW, tileH);
		}
	}
	ctx.closePath();
	ctx.stroke();
	
	// Draw circle centre
	ctx.fillStyle = "#ff0000";
	ctx.fillRect(circleX*tileW, circleY*tileH, tileW, tileH);
	
	ctx.fillText("Framerate: " + framesLastSecond, 10, 20);
	
	// Draw the blue square target
	ctx.fillStyle = "#0000cc";
	ctx.fillRect(bluePos[0]*tileW, bluePos[1]*tileH, tileW, tileH);
	ctx.fillText("Blue target visible? " + (blueVisible ? "YES!" : "no"), 10, 40);
	
	// Ask for the next animation frame
	requestAnimationFrame(drawGame);
}
</script>

</head>
<body>

<p>Click on a grid square to move the blue target.  If it is visible at its current position and depending on the current vision cone direction, you will see the "Blue target visible?" text change.  If you do not see a grid and the raycasting example below, please ensure you have Javascript enabled and that your browser supports the Canvas element.</p>
<canvas id="game" width="600" height="400"></canvas>

</body>
</html>

Page loaded in 0.01 second(s).