A Networked Multiplayer Game in JavaScript with IO.js

The JavaScript code below implements a very simple multiplayer “Spacewar” game in Javascript, using IO.js and the Socket.IO library. To see it in action, follow the steps below.

It’s not much of a game at this point. There’s no planet in the middle of the screen and you can’t do hyperspace jumps as in most versions of Spacewar. But you can start a server, connect multiple web browsers to it, fly around with the W, A, S and D keys, and shoot with the Enter key.

NetworkedMultiplayerSpacewar

1. Make sure that IO.js (or, if preferred, its predecessor Node.js) is installed, following the steps in a previous post.

2. In any convenient location, create a new directory named “MultiplayerGame”.

3. Open a command prompt, navigate to the newly created “MultiplayerGame” directory, and run “npm install socket.io” to install the Socket.IO library for IO.js. Note that I had to do this even though I had already installed Socket.IO somehwere else on the filesystem. No doubt there’s some way to avoid the need to install it again.

4. In the newly created “MultiplayerGame” directory, create new text files named “Local.html”, “Server.js”, “Server-Run.bat”, “Client.html”, and “Common.js”. Copy the text given in the code listings below these steps into each respective file.

5. Run “Server-Run.bat”. A console window will appear, and the game server will start running in it.

6. Open “Client.html” in a web browser that runs JavaScript. The game field will appear, and a colored square representing the player will appear in it.

7. Open a second copy of “Client.html” in another web browser window. The game field containing two players will appear in both web browsers.

8. A self-hosted version of the game can be run by opening the “Local.html” file in a web browser. That version is going to be even less fun, though, since running locally means there will only ever be one active player in the game at a time.

Local.html:

<html>
<body>

<script type="text/javascript" src="Common.js"></script>

<script type="text/javascript">

function Local()
{
	// do nothing
}
{
	Local.prototype.initialize = function()
	{
		this.clientID = "Client" + IDHelper.IDNext();

		this.inputHelper = new InputHelper();
		this.inputHelper.initialize(document);

		this.displayHelper = new DisplayHelper(this.world.size);
		this.displayHelper.initialize(document);

		this.updatesIncoming = [];

		var bodyDefnForUser = BodyDefn.player(this.world, this.clientID);
		var update = new Update_BodyDefnRegister(bodyDefnForUser);
		this.world.updatesOutgoing.push(update);

		var bodyForUser = new Body
		(
			this.clientID,
			this.clientID,
			bodyDefnForUser.name,
			new Coords(10, 10),
			new Coords(1, 0)
		);
		
		update = new Update_BodyCreate(bodyForUser);
		this.world.updatesOutgoing.push(update);

		setInterval
		(
			this.updateForTick.bind(this),
			this.world.millisecondsPerTick()
		);	
	}

	Local.prototype.start = function()
	{
		this.clientID = IDHelper.IDNext();
		this.document = document;

		this.world = World.demo();

		this.initialize();
	}

	Local.prototype.updateForTick = function()
	{
		var world = this.world;

		world.updateForTick_UpdatesApply(this.updatesIncoming);

		world.updateForTick_Remove();

		world.updateForTick_Spawn();

		this.updateForTick_Client();

		this.updateForTick_Server();

		this.updateForTick_UpdatesOutgoingSend();
	}

	Local.prototype.updateForTick_Client = function()
	{
		var world = this.world;

		var bodyForUser = world.bodies[this.clientID];

		if (bodyForUser != null)
		{
			bodyForUser.updateForTick_UserInput
			(
				world, this.inputHelper
			);
		}

		this.displayHelper.drawWorld(world);
	}

	Local.prototype.updateForTick_Server = function()
	{
		var world = this.world;
		var bodies = world.bodies;

		for (var i = 0; i < bodies.length; i++)
		{
			var body = bodies[i];
			body.updateForTick_Integrity(world);
			body.updateForTick_Actions(world);
			body.updateForTick_Physics(world);
		}

		for (var i = 0; i < bodies.length; i++)
		{
			var body = bodies[i];
			body.updateForTick_Collisions(world, i);
		}
	}

	Local.prototype.updateForTick_UpdatesOutgoingSend = function()
	{
		var world = this.world;

		for (var i = 0; i < world.updatesOutgoing.length; i++)
		{
			var update = world.updatesOutgoing[i];
			this.updatesIncoming.push(update);
		}
		world.updatesOutgoing.length = 0;
	}
}

new Local().start();

</script>

</body>
</html>

Server.js:

var fs = require("fs");
eval(fs.readFileSync("./Common.js").toString());

function Server(portToListenOn)
{
	this.portToListenOn = portToListenOn;
	this.socketsToClients = [];
}
{
	Server.prototype.initialize = function()
	{
		Log.IsEnabled = true;

		this.serializer = Serializer.default();

		this.clientID = IDHelper.IDNext();

		this.updatesIncoming = [];

		var socketIO = require("socket.io");
		var io = socketIO.listen
		(
			this.portToListenOn,
			{ log: false }
		);

		io.sockets.on
		(
			"connection", 
			this.handleEvent_Connection.bind(this)
		);

		console.log("Server started at " + new Date().toLocaleTimeString());
		console.log("Listening on port " + this.portToListenOn + "...");

		setInterval
		(
			this.updateForTick.bind(this),
			this.world.millisecondsPerTick()
		);	
	}

	Server.prototype.start = function()
	{
		this.world = World.demo();

		this.initialize();
	}

	Server.prototype.updateForTick = function()
	{
		var world = this.world;

		world.updateForTick_UpdatesApply(this.updatesIncoming);

		world.updateForTick_Spawn();

		this.updateForTick_Server();

		world.updateForTick_UpdatesApply(world.updatesImmediate);

		world.updateForTick_Remove();

		this.updateForTick_UpdatesOutgoingSend();
	}

	Server.prototype.updateForTick_Server = function()
	{
		var world = this.world;
		var bodies = world.bodies;

		for (var i = 0; i < bodies.length; i++)
		{
			var body = bodies[i];
			body.updateForTick_Integrity(world);
			body.updateForTick_Actions(world);
			body.updateForTick_Physics(world);
		}

		for (var i = 0; i < bodies.length; i++)
		{
			var body = bodies[i];
			body.updateForTick_Collisions(world, i);
		}
	}

	Server.prototype.updateForTick_UpdatesOutgoingSend = function()
	{
		var world = this.world;

		for (var i = 0; i < world.updatesOutgoing.length; i++)
		{
			var update = world.updatesOutgoing[i];
			var updateSerialized = this.serializer.serialize(update);

			for (var c = 0; c < this.socketsToClients.length; c++)
			{
				var socketToClient = this.socketsToClients[c];
				socketToClient.emit("update", updateSerialized);
			}
		}
		world.updatesOutgoing.length = 0;
	}

	// events

	Server.prototype.handleEvent_UpdateReceived = function(data)
	{
		var bodyUpdateSerialized = data;
		var bodyUpdate = this.serializer.deserialize
		(
			bodyUpdateSerialized
		);
		bodyUpdate.updateWorld(this.world, this);
	}

	Server.prototype.handleEvent_Connection = function(socketToClient)
	{	
		var clientIndex = this.socketsToClients.length;
		var clientID = "Client" + clientIndex;

		socketToClient.on
		(
			"update",
			this.handleEvent_UpdateReceived.bind(this)
		);

		this.socketsToClients.push(socketToClient);

		var bodyDefnForClient = BodyDefn.player(this.world, clientID);

		var updateBodyDefnRegister = new Update_BodyDefnRegister
		(
			bodyDefnForClient
		);
		updateBodyDefnRegister.updateWorld(this.world);
		this.world.updatesOutgoing.push(updateBodyDefnRegister);

		var bodyForClient = new Body
		(
			clientID,
			clientID,
			bodyDefnForClient.name,
			new Coords().randomize().multiply(this.world.size),
			new Coords().randomize().normalize()
		);

		this.world.bodiesToSpawn.push(bodyForClient);

		var updateBodyCreate = new Update_BodyCreate(bodyForClient);
		this.world.updatesOutgoing.push(updateBodyCreate);

		var updateClientJoin = new Update_ClientJoin(clientID);
		var updateSerialized = this.serializer.serialize
		(
			updateClientJoin
		);
		socketToClient.emit
		(
			"update", updateSerialized
		);

		for (var i = 0; i < this.world.bodies.length; i++)
		{
			var body = this.world.bodies[i];
			
			var bodyDefn = body.defn(this.world);
			updateBodyDefnRegister = new Update_BodyDefnRegister(bodyDefn);
			updateSerialized = this.serializer.serialize(updateBodyDefnRegister);
			socketToClient.emit("update", updateSerialized);

			var updateBodyCreate = new Update_BodyCreate(body);
			updateSerialized = this.serializer.serialize(updateBodyCreate);
			socketToClient.emit("update", updateSerialized);
		}

		console.log(clientID + " joined the server.")
	}
}

new Server(8089).start();

Server-Run.bat:

iojs Server.js
pause

Client.html:

<html>
<body>

<script type="text/javascript" src="Common.js"></script>

<script type="text/javascript" src="http://localhost:8089/socket.io/socket.io.js"></script>

<script type="text/javascript">

function Client(serviceURL)
{
	this.serviceURL = serviceURL;
}
{
	Client.prototype.initialize = function()
	{
		this.serializer = Serializer.default();

		this.inputHelper = new InputHelper();
		this.inputHelper.initialize(document);

		this.displayHelper = new DisplayHelper(this.world.size);
		this.displayHelper.initialize(document);

		this.updatesIncoming = [];

		this.socketToServer = io.connect(this.serviceURL);
		this.socketToServer.on("update", this.handleEvent_UpdateReceived.bind(this));

		this.timer = setInterval
		(
			this.updateForTick.bind(this),
			this.world.millisecondsPerTick()
		);
	}

	Client.prototype.start = function()
	{
		this.clientID = IDHelper.IDNext();

		this.world = World.demo();

		this.initialize();
	}

	Client.prototype.updateForTick = function()
	{
		var world = this.world;

		world.updateForTick_UpdatesApply(this.updatesIncoming);

		world.updateForTick_Remove();

		world.updateForTick_Spawn();

		this.updateForTick_Client();

		this.updateForTick_UpdatesOutgoingSend();
	}

	Client.prototype.updateForTick_Client = function()
	{
		var world = this.world;

		var bodyForUser = world.bodies[world.idOfBodyControlledByUser];

		if (bodyForUser != null)
		{
			bodyForUser.updateForTick_UserInput
			(
				world, this.inputHelper
			);
		}

		this.displayHelper.drawWorld(world);
	}

	Client.prototype.updateForTick_UpdatesOutgoingSend = function()
	{
		var world = this.world;

		for (var i = 0; i < world.updatesOutgoing.length; i++)
		{
			var update = world.updatesOutgoing[i];
			var updateSerialized = this.serializer.serialize(update);
				
			this.socketToServer.emit("update", updateSerialized);
		}
		world.updatesOutgoing.length = 0;
	}

	// events

	Client.prototype.handleEvent_UpdateReceived = function(updateSerialized)
	{
		var update = this.serializer.deserialize(updateSerialized);
		this.updatesIncoming.push(update);
	}
}

new Client("http://localhost:8089").start();

</script>

</body>
</html>

Common.js:

// extensions

function ArrayExtensions()
{
	// extension class
}
{
	Array.prototype.addLookups = function(keyName)
	{
		for (var i = 0; i < this.length; i++)
		{
			var item = this[i];
			var key = item[keyName];
			this[key] = item;
		}
	}

	Array.prototype.append = function(other)
	{
		for (var i = 0; i < other.length; i++)
		{
			this.push(other[i]);		
		}
	}

	Array.prototype.contains = function(itemToFind)
	{
		return (this.indexOf(itemToFind) >= 0);
	}

	Array.prototype.members = function(memberName)
	{
		var returnValues = [];

		for (var i = 0; i < this.length; i++)
		{
			var item = this[i];
			var member = item[memberName];
			returnValues.push(member);
		}

		return returnValues;	
	}

	Array.prototype.removeAt = function(indexToRemoveAt)
	{
		this.splice(indexToRemoveAt, 1);
	}

	Array.prototype.remove = function(itemToRemove)
	{
		if (this.contains(itemToRemove) == true)
		{
			this.removeAt(this.indexOf(itemToRemove));
		}
	}
}

// classes

function Action(name, keyCode, perform)
{
	this.name = name;
	this.keyCode = keyCode;
	this.perform = perform;
}

function Body(id, name, defnName, pos, orientation)
{
	this.id = id;
	this.name = name;
	this.defnName = defnName;
	this.pos = pos;
	this.orientation = orientation;

	this.actionNames = [];
	this.vel = new Coords(0, 0);
	this.accel = new Coords(0, 0);
	this.right = this.orientation.clone().right();
}
{
	// instance methods

	Body.prototype.boundsRecalculate = function(world)
	{
		var bodyDefn = this.defn(world);
		var sizeHalf = bodyDefn.sizeHalf;
		this.bounds.min.overwriteWith(this.pos).subtract(sizeHalf);
		this.bounds.max.overwriteWith(this.pos).add(sizeHalf);
	}

	Body.prototype.collideWith = function(world, other)
	{
		var bodyDefnThis = this.defn(world);
		var bodyDefnOther = other.defn(world);

		if (bodyDefnThis.collide != null)
		{
			bodyDefnThis.collide(world, this, other);
		}
		if (bodyDefnOther.collide != null)
		{
			bodyDefnOther.collide(world, other, this);
		}

	}

	Body.prototype.defn = function(world)
	{
		var returnValue = world.bodyDefns[this.defnName];
		return returnValue;
	}

	Body.prototype.initializeForWorld = function(world)
	{
		var bodyDefn = this.defn(world);
		this.integrity = bodyDefn.integrityMax;
		this.ticksToLive = bodyDefn.ticksToLive;
		this.energy = 0;
		this.bounds = new Bounds(new Coords(0, 0), new Coords(0, 0));
		this.boundsRecalculate(world);
		this.ticksSinceActionPerformed = 0;
	}

	Body.prototype.overwriteWith = function(other)
	{
		this.defnName = other.defnName;
		this.pos.overwriteWith(other.pos);
		this.orientation.overwriteWith(other.orientation);
		this.vel.overwriteWith(other.vel);
		this.accel.overwriteWith(other.accel);
		this.right.overwriteWith(other.right);
	}

	Body.prototype.updateForTick_Actions = function(world)
	{
		var bodyDefn = this.defn(world);

		for (var a = 0; a < this.actionNames.length; a++)
		{
			var actionName = this.actionNames[a];
			var action = world.actions[actionName];
			action.perform(world, this);
		}

		this.energy += bodyDefn.energyPerTick;
		if (this.energy > bodyDefn.energyMax)
		{
			this.energy = bodyDefn.energyMax;
		}
	}

	Body.prototype.updateForTick_Collisions = function(world, i)
	{
		for (var j = i + 1; j < world.bodies.length; j++)
		{
			var bodyOther = world.bodies[j];
					
			if (this.bounds.overlapWith(bodyOther.bounds) == true)
			{
				this.collideWith(world, bodyOther);
			}
		}
	}

	Body.prototype.updateForTick_Integrity = function(world)
	{
		this.ticksSinceActionPerformed++;

		var ticksSinceActionPerformedMax = 60; // hack

		if (this.ticksSinceActionPerformed >= ticksSinceActionPerformedMax)
		{
			this.integrity = 0;
		}

		if (this.ticksToLive != null)
		{
			this.ticksToLive--;
			if (this.ticksToLive <= 0)
			{
				this.integrity = 0;
			}
		}

		if (this.integrity <= 0)
		{
			var update = new Update_Remove
			(
				this.id
			);
			world.updatesImmediate.push(update);
			world.updatesOutgoing.push(update);
		}
	}

	Body.prototype.updateForTick_Physics = function(world)
	{
		var bodyDefn = this.defn(world);

		this.vel.add
		(
			this.accel
		).trimToMagnitude
		(
			bodyDefn.speedMax
		);

		this.pos.add
		(
			this.vel
		).wrapToRange
		(
			world.size
		);

		this.accel.clear();

		this.boundsRecalculate(world);

		var update = new Update_Physics
		(
			this.id, this.pos, this.orientation
		);

		world.updatesOutgoing.push(update);
	}

	Body.prototype.updateForTick_UserInput = function(world, inputHelper)
	{
		this.actionNames.length = 0;
		
		var bodyDefn = this.defn(world);

		var keyCodesPressed = inputHelper.keyCodesPressed;
	
		for (var i = 0; i < keyCodesPressed.length; i++)
		{
			var keyCodePressed = keyCodesPressed[i];
			var action = world.actions[keyCodePressed];
			if (action != null)
			{
				var actionName = action.name;
				if (bodyDefn.actionNames.contains(action.name) == true)
				{
					this.actionNames.push(actionName);
				}
			}
		}

		var update = new Update_Actions
		(
			this.id, this.actionNames
		);

		world.updatesOutgoing.push(update);
	}
}

function BodyDefn
(
	name, 
	categoryNames,
	color, 
	integrityMax,
	ticksToLive,
	speedMax, 
	accelerationPerTick, 
	turnRate, 
	energyMax,
	energyPerTick,
	size, 
	actionNames,
	collide
)
{
	this.name = name;
	this.categoryNames = categoryNames;
	this.color = color;
	this.integrityMax = integrityMax;
	this.ticksToLive = ticksToLive;
	this.speedMax = speedMax;
	this.accelerationPerTick = accelerationPerTick;
	this.turnRate = turnRate;
	this.energyMax = energyMax;
	this.energyPerTick = energyPerTick;
	this.size = size;
	this.actionNames = actionNames;
	this.collide = collide;

	this.sizeHalf = this.size.clone().divideScalar(2);
	this.actionNames.addLookups("keyCode");
}
{
	BodyDefn.player = function(world, name)
	{
		var color = ColorHelper.random();

		var returnValue = new BodyDefn
		(
			name,
			[ "Player" ], // categoryNames
			color,
			1, // integrityMax
			null, // ticksToLive
			2, // speedMax 
			.15, // accelerationPerTick
			.15, // turnRate
			1, // energyMax
			.2, // energyPerTick
			new Coords(5, 5), // size
			world.actions.members("name"),
			null // collide
		);

		return returnValue;
	}
}

function Bounds(min, max)
{
	this.min = min;
	this.max = max;
	this.size = new Coords();
	this.recalculateDerivedValues();
}
{
	// static methods

	Bounds.fromPositionsMany = function(positions)
	{
		var positionMin = positions[0].clone();
		var positionMax = positions[0].clone();

		for (var i = 1; i < positions.length; i++)
		{
			var position = positions[i];

			if (position.x < positionMin.x)
			{
				positionMin.x = position.x;
			}
			else if (position.x > positionMax.x)
			{
				positionMax.x = position.x;
			}

			if (position.y < positionMin.y)
			{
				positionMin.y = position.y;
			}
			else if (position.y > positionMax.y)
			{
				positionMax.y = position.y;
			}
		}

		var returnValue = new Bounds(positionMin, positionMax);

		return returnValue;
	}

	// instance methods

	Bounds.prototype.clone = function()
	{
		return new Bounds(this.min.clone(), this.max.clone());
	}

	Bounds.prototype.overlapWith = function(other)
	{
		var returnValue = false;

		var bounds = [ this, other ];

		for (var b = 0; b < bounds.length; b++)
		{
			var boundsThis = bounds[b];
			var boundsOther = bounds[1 - b];			

			var doAllDimensionsOverlapSoFar = true;

			for (var d = 0; d < Coords.NumberOfDimensions; d++)
			{
				if 
				(
					boundsThis.max.dimension(d) < boundsOther.min.dimension(d)
					|| boundsThis.min.dimension(d) > boundsOther.max.dimension(d)
				)
				{
					doAllDimensionsOverlapSoFar = false;
					break;
				}
			}

			if (doAllDimensionsOverlapSoFar == true)
			{
				returnValue = true;
				break;
			}
		}

		return returnValue;
	}

	Bounds.prototype.overwriteWith = function(other)
	{
		this.min.overwriteWith(other.min);
		this.max.overwriteWith(other.max);
		this.recalculateDerivedValues();
		return this;
	}

	Bounds.prototype.recalculateDerivedValues = function()
	{
		this.size.overwriteWith(this.max).subtract(this.min);
	}
}

function ColorHelper()
{
	// static class
}
{
	ColorHelper.random = function()
	{
		var hueMax = 360;
		var hue = Math.floor(Math.random() * hueMax);

		var returnValue = 
			"hsl(" + hue + ", 100%, 50%)";

		return returnValue;
	}
}

function Coords(x, y)
{
	this.x = x;
	this.y = y;
}
{
	// constants

	Coords.NumberOfDimensions = 2;

	// instance methods

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

	Coords.prototype.clear = function()
	{
		this.x = 0;
		this.y = 0;
		return this;
	}

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

	Coords.prototype.dimension = function(dimensionIndex)
	{
		var returnValue;

		if (dimensionIndex == 0)
		{
			returnValue = this.x;
		}
		else 
		{
			returnValue = this.y;
		}

		return returnValue;
	}

	Coords.prototype.divide = function(other)
	{
		this.x /= other.x;
		this.y /= other.y;
		return this;
	}

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

	Coords.prototype.magnitude = function()
	{
		return Math.sqrt(this.x * this.x + this.y * this.y);
	}

	Coords.prototype.multiply = function(other)
	{
		this.x *= other.x;
		this.y *= other.y;
		return this;
	}

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

	Coords.prototype.normalize = function()
	{
		return this.divideScalar(this.magnitude());
	}

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

	Coords.prototype.randomize = function()
	{
		this.x = Math.random();
		this.y = Math.random();
		return this;
	}

	Coords.prototype.right = function()
	{
		var temp = this.y;
		this.y = this.x;
		this.x = 0 - temp;
		return this;
	}

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

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

	Coords.prototype.trimToMagnitude = function(magnitudeMax)
	{
		var magnitude = this.magnitude();

		if (magnitude > magnitudeMax)
		{
			this.divideScalar
			(
				magnitude
			).multiplyScalar
			(
				magnitudeMax
			);
		}

		return this;
	}

	Coords.prototype.wrapToRange = function(max)
	{
		while (this.x < 0)
		{
			this.x += max.x;
		}
		while (this.x >= max.x)
		{
			this.x -= max.x;
		}
		while (this.y < 0)
		{
			this.y += max.y;
		}
		while (this.y >= max.y)
		{
			this.y -= max.y;	
		}
		return this;
	}
}


function Log()
{}
{
	Log.IsEnabled = false;

	Log.write = function(message)
	{
		if (Log.IsEnabled == true)
		{
			console.log(message);
		}
	}
}

function DisplayHelper(size)
{
	this.size = size;
}
{
	DisplayHelper.prototype.clear = function()
	{
		this.graphics.fillStyle = "White";
		this.graphics.fillRect(0, 0, this.size.x, this.size.y);

		this.graphics.strokeStyle = "Gray";
		this.graphics.strokeRect(0, 0, this.size.x, this.size.y);
	}

	DisplayHelper.prototype.drawTextAtPos = function(text, drawPos)
	{
		this.graphics.fillStyle = "Gray";
		this.graphics.fillText
		(
			text, 
			drawPos.x, drawPos.y
		);
	}

	DisplayHelper.prototype.drawWorld = function(world)
	{
		this.clear();

		var drawPos = new Coords();
		var drawPos2 = new Coords();

		for (var i = 0; i < world.bodies.length; i++)
		{
			var body = world.bodies[i];
			var bodyPos = body.pos;
			var bodyDefn = body.defn(world);
			var bodySize = bodyDefn.size;

			this.graphics.strokeStyle = bodyDefn.color;

			drawPos.overwriteWith
			(
				bodyPos
			).subtract
			(
				bodyDefn.sizeHalf
			);

			this.drawTextAtPos(body.name, drawPos);

			this.graphics.strokeRect
			(
				drawPos.x, drawPos.y,
				bodySize.x, bodySize.y
			);

			this.graphics.beginPath();

			drawPos.add(bodyDefn.sizeHalf);

			this.graphics.moveTo(drawPos.x, drawPos.y);

			drawPos2.overwriteWith
			(
				body.orientation
			).multiplyScalar
			(
				bodySize.x
			).add
			(
				drawPos
			);

			this.graphics.lineTo(drawPos2.x, drawPos2.y);

			this.graphics.stroke();
		}		
	}

	DisplayHelper.prototype.initialize = function(document)
	{
		var canvas = document.createElement("canvas");
		canvas.width = this.size.x;
		canvas.height = this.size.y;

		this.graphics = canvas.getContext("2d");

		document.body.appendChild(canvas);

		this.domElement = canvas;
	}
}

function IDHelper()
{
	// static class
}
{
	IDHelper._idNext = 0;

	IDHelper.IDNext = function()
	{
		return "_" + IDHelper._idNext++;
	}
}

function InputHelper()
{
	// do nothing
}
{
	InputHelper.prototype.initialize = function(document)
	{
		this.keyCodesPressed = [];
		document.body.onkeydown = this.handleEventKeyDown.bind(this);
		document.body.onkeyup = this.handleEventKeyUp.bind(this);
	}

	// events

	InputHelper.prototype.handleEventKeyDown = function(event)
	{
		var keyCode = "_" + event.keyCode;
		if (this.keyCodesPressed[keyCode] == null)
		{
			this.keyCodesPressed.push(keyCode);
			this.keyCodesPressed[keyCode] = keyCode;
		}
	}

	InputHelper.prototype.handleEventKeyUp = function(event)
	{
		var keyCode = "_" + event.keyCode;
		this.keyCodesPressed.remove(keyCode);
		delete this.keyCodesPressed[keyCode];
	}
}

function Map(sizeInCells, sizeInPixels)
{
	this.sizeInCells = sizeInCells;
	this.sizeInPixels = sizeInPixels;

	this.numberOfCells = this.sizeInCells.x * this.sizeInCells.y;
	this.cellSizeInPixels = this.sizeInPixels.clone().divide
	(
		this.sizeInCells
	);

	this.cellsBuild();

	// temporary variables

	this.boundsInCells = new Coords();
}
{
	Map.prototype.cellAtPos = function(cellPos)
	{
		return this.cells[this.indexOfCellAtPos(cellPos)];
	}

	Map.prototype.cellsBuild = function()
	{
		this.cells = [];
		for (var i = 0; i < this.numberOfCells.length; i++)
		{
			var cell = new MapCell();
			this.cells.push(cell);
		}
	}

	Map.prototype.cellsOccupiedByBoundsAddToList = function
	(
		boundsInPixels, listToAddTo
	)
	{
		var boundsInCells = this.boundsInCells;

		boundsInCells.overwriteWith
		(
			boundsInPixels
		).divide
		(
			this.cellSizeInPixels
		).floorMinAndCeilingMax();

		var cellPosMin = boundsInCells.min;
		var cellPosMax = boundsInCells.max;

		var cellPos = new Coords();

		for (var y = cellPosMin.y; y <= cellPosMax.y; y++)
		{
			cellPos.y = y;

			for (var x = cellPosMin.x; x <= cellPosMax.x; x++)
			{	
				cellPos.x = x;
				var cellAtPos = this.cellAtPos(cellPos);
				listToAddTo.push(cellAtPos)
			}
		}
	}

	Map.prototype.indexOfCellAtPos = function(cellPos)
	{
		return cellPos.y * this.sizeInCells.x + cellPos.x;
	}
}

function MapCell()
{
	this.bodyIDsPresent = [];
}

function Serializer(knownTypes)
{
	this.knownTypes = knownTypes;

	for (var i = 0; i < this.knownTypes.length; i++)
	{
		var knownType = this.knownTypes[i];
		this.knownTypes[knownType.name] = knownType;
	}
}
{
	// constants

	Serializer.TypeNamesBuiltIn = 
	[
		"Boolean", "Function", "Number", "String", 
	];

	// static methods

	Serializer.default = function()
	{
		return new Serializer
		([ 
			Action,
			Body,
			BodyDefn,
			Update_Actions,
			Update_BodyCreate,
			Update_BodyDefnRegister,
			Update_ClientJoin,
			Update_Physics,
			Update_Remove, 
			Bounds,
			Coords,
			World,
		]);
	}

	// instance methods

	Serializer.prototype.deleteClassNameRecursively = function(objectToDeleteClassNameOn)
	{
		if (objectToDeleteClassNameOn == null)
		{
			return;
		}

		var className = objectToDeleteClassNameOn.constructor.name;

		if (this.knownTypes[className] != null)
		{
			delete objectToDeleteClassNameOn.className;

			for (var childPropertyName in objectToDeleteClassNameOn)
			{
				var childProperty = objectToDeleteClassNameOn[childPropertyName];
				this.deleteClassNameRecursively(childProperty);
			}
		}
		else if (className == "Array")
		{
			for (var i = 0; i < objectToDeleteClassNameOn.length; i++)
			{
				var element = objectToDeleteClassNameOn[i];
				this.deleteClassNameRecursively(element);
			}
		}
	}

	Serializer.prototype.deserialize = function(stringToDeserialize)
	{
		var objectDeserialized = JSON.parse(stringToDeserialize);

		this.setPrototypeRecursively(objectDeserialized);

		objectDeserialized = this.unwrapArraysRecursively(objectDeserialized);

		this.deleteClassNameRecursively(objectDeserialized);

		return objectDeserialized;
	}

	Serializer.prototype.serialize = function(objectToSerialize)
	{
		objectToSerialize = this.wrapArraysRecursively(objectToSerialize);

		this.setClassNameRecursively(objectToSerialize);

		var returnValue = JSON.stringify(objectToSerialize, null, 2);

		this.unwrapArraysRecursively(objectToSerialize);

		this.deleteClassNameRecursively(objectToSerialize);

		return returnValue;
	}

	Serializer.prototype.setClassNameRecursively = function(objectToSetClassNameOn)
	{
		if (objectToSetClassNameOn == null)
		{
			return;
		}

		var className = objectToSetClassNameOn.constructor.name;

		if (Serializer.TypeNamesBuiltIn.indexOf(className) >= 0)
		{
			// do nothing
		}
		else if (className == "SerializerArrayWrapper")
		{
			var arrayWrapped = objectToSetClassNameOn.arrayWrapped;
			for (var i = 0; i < arrayWrapped.length; i++)
			{
				var element = arrayWrapped[i];
				this.setClassNameRecursively(element);
			}
		}
		else // if (this.knownTypes[className] != null)
		{
			for (var childPropertyName in objectToSetClassNameOn)
			{
				var childProperty = objectToSetClassNameOn[childPropertyName];
				this.setClassNameRecursively(childProperty);
			}

			objectToSetClassNameOn.className = className;
		}

	}

	Serializer.prototype.setPrototypeRecursively = function(objectToSetPrototypeOn)
	{
		if (objectToSetPrototypeOn == null)
		{
			return;
		}
		var className = objectToSetPrototypeOn.className;

		var typeOfObjectToSetPrototypeOn = this.knownTypes[className];

		if (typeOfObjectToSetPrototypeOn != null)
		{
			objectToSetPrototypeOn.__proto__ = typeOfObjectToSetPrototypeOn.prototype;
			objectToSetPrototypeOn.constructor = typeOfObjectToSetPrototypeOn;
	
			for (var childPropertyName in objectToSetPrototypeOn)
			{
				var childProperty = objectToSetPrototypeOn[childPropertyName];

				if (childProperty == null)
				{
					// do nothing
				}
				else if (childProperty.className == "SerializerArrayWrapper")
				{
					var arrayWrapped = childProperty.arrayWrapped;
					var lookupPaths = childProperty.lookupPaths;
					if (lookupPaths != null)
					{
						for (var i = 0; i < lookupPaths.length; i++)
						{
							var lookupPath = lookupPaths[i];
							arrayWrapped.addLookups(lookupPath);
						}
						objectToSetPrototypeOn[childPropertyName] = childProperty;
					}
				}
				this.setPrototypeRecursively(childProperty);
			}
		}
		else if (className == "SerializerArrayWrapper")
		{
			this.setPrototypeRecursively(objectToSetPrototypeOn.arrayWrapped);
		}
		else 
		{
			var typeName = objectToSetPrototypeOn.constructor.name;
			if (typeName == "Array")
			{
				for (var i = 0; i < objectToSetPrototypeOn.length; i++)
				{
					var element = objectToSetPrototypeOn[i];
					this.setPrototypeRecursively(element);
				}
			}
			else if (Serializer.TypeNamesBuiltIn.indexOf(typeName) == -1)
			{
				throw ("Unknown type: " + className);
			}
		}
	}

	Serializer.prototype.unwrapArraysRecursively = function(objectDeserialized)
	{
		var returnValue = objectDeserialized;
		if (objectDeserialized == null)
		{
			return returnValue;
		}

		var typeName = objectDeserialized.className;

		if (typeName == null)
		{
			// do nothing
		}
		else if (typeName == "SerializerArrayWrapper")
		{
			var arrayWrapped = objectDeserialized.arrayWrapped;

			for (var i = 0; i < arrayWrapped.length; i++)
			{
				var element = arrayWrapped[i];
				element = this.unwrapArraysRecursively(element);
				arrayWrapped[i] = element;
			}

			returnValue = arrayWrapped;

			var lookupPaths = objectDeserialized.lookupPaths;
			if (lookupPaths != null)
			{
				for (var i = 0; i < lookupPaths.length; i++)
				{
					var lookupPath = lookupPaths[i];
					arrayWrapped.addLookups(lookupPath);
				}
			}
		}
		else
		{
			for (var childPropertyName in objectDeserialized)
			{
				var childProperty = objectDeserialized[childPropertyName];	
				objectDeserialized[childPropertyName] = this.unwrapArraysRecursively
				(
					childProperty
				);
			}		
		}

		return returnValue;
	}	

	Serializer.prototype.wrapArraysRecursively = function(objectToSerialize)
	{
		var returnValue = objectToSerialize;

		if (objectToSerialize == null)
		{
			return returnValue;
		}

		var typeName = objectToSerialize.constructor.name;

		if (Serializer.TypeNamesBuiltIn.indexOf(typeName) >= 0)
		{
			// do nothing
		}
		else if (typeName == "SerializerArrayWrapper")
		{
			// do nothing
		}
		else if (typeName == "Array")
		{
			var arrayToSerialize = objectToSerialize;

			for (var i = 0; i < arrayToSerialize.length; i++)
			{
				var element = arrayToSerialize[i];
				element = this.wrapArraysRecursively(element);
				arrayToSerialize[i] = element;
			}

			var wrapper = new SerializerArrayWrapper(arrayToSerialize);		

			returnValue = wrapper;
		}
		else
		{
			for (var childPropertyName in objectToSerialize)
			{
				var childProperty = objectToSerialize[childPropertyName];	
				objectToSerialize[childPropertyName] = this.wrapArraysRecursively
				(
					childProperty
				);
			}		
		}

		return returnValue;
	}	
}

function SerializerArrayWrapper(arrayWrapped)
{
	this.arrayWrapped = arrayWrapped;
	this.className = "SerializerArrayWrapper";
	this.lookupPaths = this.arrayWrapped.lookupPaths;
}

function Update_Actions(bodyID, actionNames)
{
	this.bodyID = bodyID;
	this.actionNames = actionNames;
}
{
	Update_Actions.prototype.updateWorld = function(world)
	{
		var body = world.bodies[this.bodyID];
		if (body != null)
		{
			body.ticksSinceActionPerformed = 0;
			body.actionNames.length = 0;
			body.actionNames.append(this.actionNames);
		}
	}
}

function Update_BodyCreate(body)
{
	this.body = body;
}
{
	Update_BodyCreate.prototype.updateWorld = function(world)
	{
		world.bodiesToSpawn.push(this.body);
	}
}

function Update_BodyDefnRegister(bodyDefn)
{
	this.bodyDefn = bodyDefn;
}
{
	Update_BodyDefnRegister.prototype.updateWorld = function(world)
	{
		world.bodyDefns.push(this.bodyDefn);
		world.bodyDefns[this.bodyDefn.name] = this.bodyDefn;
	}
}

function Update_ClientJoin(clientID)
{
	this.clientID = clientID;
}
{
	Update_ClientJoin.prototype.updateWorld = function(world)
	{
		world.idOfBodyControlledByUser = this.clientID;
	}
}

function Update_Physics(bodyID, pos, orientation)
{
	this.bodyID = bodyID;
	this.pos = pos;
	this.orientation = orientation;
}
{
	// instance methods

	Update_Physics.prototype.updateWorld = function(world)
	{
		var body = world.bodies[this.bodyID];
		if (body != null)
		{
			body.pos.overwriteWith(this.pos);
			body.orientation.overwriteWith(this.orientation);
			body.right.overwriteWith(this.orientation).right();
		}
	}
}

function Update_Remove(bodyID)
{
	this.bodyID = bodyID;
}
{
	Update_Remove.prototype.updateWorld = function(world)
	{
		var bodyToRemove = world.bodies[this.bodyID];
		world.bodiesToRemove.push(bodyToRemove);
		if (bodyToRemove.defn(world).categoryNames.contains("Player") == true)
		{
			Log.write(bodyToRemove.name + " was destroyed.");
		}
	}
}

function World(name, ticksPerSecond, size, actions, bodyDefns, bodiesInitial)
{
	this.name = name;
	this.ticksPerSecond = ticksPerSecond;
	this.size = size;
	this.actions = actions;
	this.bodyDefns = bodyDefns;

	this.bodies = [];

	this.bodiesToSpawn = bodiesInitial.slice();
	this.bodiesToRemove = [];

	this.updatesImmediate = [];
	this.updatesOutgoing = [];

	this.initializeLookups();
}
{
	// static methods

	World.demo = function()
	{
		var actions = 
		[
			new Action
			(
				"Accelerate",
				"_87", // keyCode
				function(world, body)
				{
					var bodyDefn = body.defn(world);
					var acceleration = bodyDefn.accelerationPerTick;

					body.accel.add
					(
						body.orientation.clone().multiplyScalar
						(
							acceleration
						)
					);
				}
			),

			new Action
			(
				"Fire",
				"_13", // keyCode
				function(world, body)
				{
					var energyToFire = 1;
					body.energy -= energyToFire;
					if (body.energy < 0)
					{
						body.energy = 0;
						return;
					}

					var bodyDefn = body.defn(world);					

					var projectileDefn = world.bodyDefns["Projectile"];

					var projectileID = IDHelper.IDNext();

					var projectile = new Body
					(
						projectileID,
						"", // no name
						projectileDefn.name,
						body.pos.clone().add
						(
							body.orientation.clone().multiplyScalar
							(
								bodyDefn.size.magnitude()
							)
						),
						body.orientation.clone()
					);

					projectile.vel.overwriteWith
					(
						body.orientation
					).multiplyScalar
					(
						projectileDefn.speedMax
					);

					var update = new Update_BodyCreate(projectile);
					world.updatesImmediate.push(update);
					world.updatesOutgoing.push(update);
				}
			),

			new Action
			(
				"Quit",
				null, // keyCode
				function(world, body)
				{
					// todo
				}
			),

			new Action
			(
				"TurnLeft",
				"_65", // keyCode
				function(world, body)
				{
					var bodyDefn = body.defn(world);
					var turnRate = bodyDefn.turnRate;

					body.orientation.subtract
					(
						body.right.clone().multiplyScalar
						(
							turnRate
						)
					).normalize();

					body.right.overwriteWith
					(
						body.orientation
					).right();
				}
			),

			new Action
			(
				"TurnRight",
				"_68", // keyCode
				function(world, body)
				{
					var bodyDefn = body.defn(world);
					var turnRate = bodyDefn.turnRate;

					body.orientation.add
					(
						body.right.clone().multiplyScalar(turnRate)
					).normalize();

					body.right.overwriteWith
					(
						body.orientation
					).right();
				}
			),
		];

		var actionNames = actions.members("name");

		var returnValue = new World
		(
			"World0",
			20, // ticksPerSecond
			new Coords(128, 128), // size
			actions,
			// bodyDefns
			[
				new BodyDefn
				(
					"Projectile", // name
					[], // categoryNames
					"Brown", // color 
					1, // integrityMax
					10, // ticksToLive
					8, // speedMax
					0, // accelerationPerTick 
					0, // turnRate
					0, // energyMax
					0, // energyPerTick
					new Coords(1, 1), // size
					[], // actionNames
					// collide
					function(world, collider, other)
					{
						this.integrity = 0;
						other.integrity--;
					}
				),
			],
			// bodies
			[
				// none
			]
		);

		return returnValue;
	}

	// instance methods

	World.prototype.bodyRemove = function(body)
	{
		if (body != null)
		{
			this.bodies.remove(body);
			this.bodies[body.id] = null;
			delete this.bodies[body.id];
		}
	}

	World.prototype.bodySpawn = function(body)
	{
		this.bodies.push(body);
		this.bodies[body.id] = body;
		body.initializeForWorld(this);
	}

	World.prototype.initializeLookups = function()
	{
		// hack
		this.actions.addLookups("name");
		this.actions.addLookups("keyCode");
		this.bodyDefns.addLookups("name");
		this.bodies.addLookups("id");
	}

	World.prototype.millisecondsPerTick = function()
	{
		return Math.floor(1000 / this.ticksPerSecond);
	}

	World.prototype.updateForTick_Remove = function()
	{
		for (var i = 0; i < this.bodiesToRemove.length; i++)
		{
			var body = this.bodiesToRemove[i];
			this.bodyRemove(body);
		}
		this.bodiesToRemove.length = 0;
	}

	World.prototype.updateForTick_Spawn = function()
	{
		for (var i = 0; i < this.bodiesToSpawn.length; i++)
		{
			var body = this.bodiesToSpawn[i];
			this.bodySpawn(body);
		}
		this.bodiesToSpawn.length = 0;
	}

	World.prototype.updateForTick_UpdatesApply = function(updatesToApply)
	{
		for (var i = 0; i < updatesToApply.length; i++)
		{
			var update = updatesToApply[i];
			update.updateWorld(this);
		}
		updatesToApply.length = 0;
	}
}

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