A Wraparound Game Playfield in JavaScript

The JavaScript code shown below will render a game playfield in which bodies wrap around to the left when they exit to the right, and around to the top when they exit from the bottom, and vice versa. The camera will always focus on the midpoint between the two bodies. The midpoint calculation takes into account the possibility of wrapping on the borders of the playfield to make sure that the playfield is rendered in the most efficient manner.

Look, it’s a little hard to explain. Remember the combat in Star Control? It’s like that.

WraparoundPlayfield

<html>
<body>
<script type="text/javascript">

// main

function LayoutTest()
{
	this.main = function()
	{
		var venue0 = new Venue
		(
			"Venue0",
			new Coords(960, 960), // size
			new Camera(new Coords(320, 320), new Coords(0, 0)),
			[
				new Body
				(
					"Body0", 
					"#808080", // color
					new Coords(0, 0), // pos
					new Coords(0, 0) // vel
				),
				new Body
				(
					"Body1", 
					"#00ff00", // color
					new Coords(0, 0), // pos
					new Coords(4, 4) // vel
				),
				new Body
				(
					"Body2", 
					"#ff0000", // color
					new Coords(0, 0), // pos
					new Coords(4, 0) // vel
				),
			]
		);

		Globals.Instance.initialize
		(
			venue0,
			100 // millisecondsPerTick
		);
	}
}

// classes

function Body(name, color, pos, vel)
{
	this.name = name;
	this.color = color;
	this.pos = pos;
	this.vel = vel;
}

function Camera(viewSize, pos)
{
	this.viewSize = viewSize;
	this.pos = pos;

	this.viewSizeHalf = this.viewSize.clone().divideScalar(2);
}

function Coords(x, y)
{
	this.x = x;
	this.y = y;
}
{
	Coords.prototype.absolute = function()
	{
		this.x = Math.abs(this.x);
		this.y = Math.abs(this.y);

		return this;
	}

	Coords.prototype.add = function(other)
	{
		this.x += other.x;
		this.y += other.y;

		return this;
	}

	Coords.prototype.clone = function()
	{
		return new Coords(this.x, this.y);
	}

	Coords.prototype.divideScalar = function(scalar)
	{
		this.x /= scalar;
		this.y /= scalar;

		return this;
	}

	Coords.prototype.isWithinRange = function(rangeToWrapTo)
	{
		var returnValue = 
		(
			this.x >= 0 
			&& this.x <= rangeToWrapTo.x
			&& this.y >= 0
			&& this.y <= rangeToWrapTo.y
		);

		return returnValue;
	}

	Coords.prototype.modulo = function(other)
	{
		this.x = this.x % other.x;
		this.y = this.y % other.y;

		return this;
	}

	Coords.prototype.multiplyScalar = function(scalar)
	{
		this.x *= scalar;
		this.y *= scalar;

		return this;
	}

	Coords.prototype.overwriteWith = function(other)
	{
		this.x = other.x;
		this.y = other.y;

		return this;
	}

	Coords.prototype.subtract = function(other)
	{
		this.x -= other.x;
		this.y -= other.y;

		return this;
	}

	Coords.prototype.toString = function()
	{
		return "x" + this.x + "y" + this.y;
	}

	Coords.prototype.wrapToRange = function(rangeToWrapTo)
	{
		while (this.x < 0)
		{
			this.x += rangeToWrapTo.x;
		}

		while (this.x > rangeToWrapTo.x)
		{
			this.x -= rangeToWrapTo.x;
		}

		while (this.y < 0)
		{
			this.y += rangeToWrapTo.y;
		}

		while (this.y > rangeToWrapTo.y)
		{
			this.y -= rangeToWrapTo.y;
		}

		return this;		
	}
}

function Globals()
{}
{
	Globals.Instance = new Globals();

	Globals.processTick = function()
	{
		Globals.Instance.venue.update();
	}

	Globals.prototype.initialize = function(venue, millisecondsPerTick)
	{
		this.venue = venue;

		document.body.appendChild(this.venue.toHTMLElement());

		setInterval(Globals.processTick, millisecondsPerTick);
	}

}

function Venue(name, size, camera, bodies)
{
	this.name = name;
	this.size = size;
	this.camera = camera;
	this.bodies = bodies;
}
{
	Venue.prototype.update = function()
	{
		for (var i = 0; i < this.bodies.length; i++)
		{
			var body = this.bodies[i];

			body.pos.add
			(
				body.vel
			).wrapToRange
			(
				this.size
			);
		}

		this.updateHTMLElement();
	}

	// html

	Venue.prototype.toHTMLElement = function()
	{
		var canvas = document.createElement("canvas");
		canvas.width = this.size.x;
		canvas.height = this.size.y;
		canvas.graphics = canvas.getContext("2d");

		this.htmlElement = canvas;

		return canvas;
	}

	Venue.prototype.updateHTMLElement = function()
	{
		var graphics = this.htmlElement.graphics;

		var cameraViewSize = this.camera.viewSize;		

		graphics.fillStyle = "#000040";
		graphics.fillRect(0, 0, cameraViewSize.x, cameraViewSize.y);

		var body1 = this.bodies[1];
		var body2 = this.bodies[2];

		var midpointBetweenBodies1And2 = 
			body1.pos.clone().add(body2.pos).divideScalar(2);

		var displacementFromBody1To2 = 
			body2.pos.clone().subtract(body1.pos);

		var displacementFromBody1To2Absolute = 
			displacementFromBody1To2.clone().absolute();

		var displacementFromBody1To2AbsoluteNoWrap = 
			displacementFromBody1To2Absolute;

		var displacementFromBody1To2AbsoluteWithWrap = 
			this.size.clone().subtract(displacementFromBody1To2Absolute);

		var differenceOfWrapAndNoWrap = 
			displacementFromBody1To2AbsoluteWithWrap.clone().subtract
			(
				displacementFromBody1To2AbsoluteNoWrap
			);

		if (differenceOfWrapAndNoWrap.x < 0)
		{
			midpointBetweenBodies1And2.x = 
				(body1.pos.x + body2.pos.x + this.size.x) / 2;
		}

		if (differenceOfWrapAndNoWrap.y < 0)
		{
			midpointBetweenBodies1And2.y = 
				(body1.pos.y + body2.pos.y + this.size.y) / 2;		
		}

		midpointBetweenBodies1And2.wrapToRange(this.size);

		this.camera.pos = midpointBetweenBodies1And2;
		var cameraViewCornerNW = this.camera.pos.clone().subtract
		(
			this.camera.viewSizeHalf
		);

		var drawPos = new Coords(0, 0);

		for (var i = 0; i < this.bodies.length; i++)
		{
			var body = this.bodies[i];

			drawPos.overwriteWith
			(
				body.pos
			).subtract
			(
				cameraViewCornerNW
			).wrapToRange
			(
				this.size
			);

			if (drawPos.isWithinRange(cameraViewSize) == true)
			{
				graphics.fillStyle = body.color;
				graphics.strokeStyle= body.color;
				graphics.fillText
				(
					body.pos.toString(),
					drawPos.x,
					drawPos.y	
				);

				graphics.beginPath();
				graphics.arc
				(
					drawPos.x, drawPos.y, 
					5, // radius
					// start and end angles
					0, 2 * Math.PI 
				)
				graphics.stroke();

				graphics.beginPath();
				graphics.moveTo(drawPos.x, drawPos.y);
				drawPos.add
				(
					body.vel.clone().multiplyScalar(5)
				);
				graphics.lineTo(drawPos.x, drawPos.y);
				graphics.stroke();
			}
		}
	}	
}

// run

new LayoutTest().main();

</script>
</body>
</html>

This entry was posted in Uncategorized and tagged , , , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s