Core JavaScript

Shared Worker with Multiplayer

This example demonstrates how to create a shared worker that is accessed by multiple pages that send and receiver messages in JavaScript.

SharedWorkerMultiplayer.html

<!DOCTYPE html>
<html lang="en">
	<head>
		<meta charset="utf-8" />
		<title>XoaX.net's Workers Demo</title>
		<style type="text/css">
			iframe {
				padding 0px;
				width: 517px;
				height: 1200px;
			}
		</style>
		<script>
			function AddAnotherPlayerPage() {
				// We could open two views by using index.html
				window.open('View.html');
			}
		</script>
	</head>
	<body>
		<!--	We will have 2 iframes because of the size, but we could use any number.
					To open another view, use the button to create a new page -->
		<p><button type=button onclick="AddAnotherPlayerPage()">Add another player</button></p>
		<p>Each additional player page opens in a new window. The number of players is restricted to 5.</p>
		<p>Use the arrow keys to move the player that has focus (red outline) and the <b>&lt;</b> and <b>&gt;</b> keys to change the terrain.</p>
		<iframe src="View.html"></iframe>
		<iframe src="View.html"></iframe>
	</body>
</html>

View.html

<!DOCTYPE html>
<html lang="en">
	<head>
		<meta charset="utf-8" />
		<title>XoaX.net's Workers Demo</title>
		<style type="text/css">
			#idMap {
				width: 500px;
				height: 500px;
				background-color:gray;
				position: absolute;
			}
			.cMapTile {
				width: 100px;
				height: 100px;
				background-color:black;
				position: absolute;
			}
			#idButtons {
				position: absolute;
				left: 175px;
				width: 150px;
				height: 150px;
				background-color:lightgray;
			}
			.cButton {
				position: absolute;
				width: 50px;
				height: 50px;
				border-radius:4px;
				font-size:xx-large;
				font-weight:bold;
			}
			.cButton:hover {
				background-color: #AAAAAA;
			}
		</style>
		<script>
			var gqSharedWorker = null
			var qaaMapTiles = [];
			var gqaManImageElements = [];
			
			// Message Handlers
			function SetName(e) {
				// Make sure that this is the name.
				if (e.data.substr(0, 5) != 'Name ') {
					return;
				}
				let sName = e.data.substr(5);
				if (parseInt(sName) == -1) {
					window.close();
				}
				let qTitleElement = document.createElement("h1");
				qTitleElement.innerHTML = "Player " + sName;
				let qBodyElement = document.body;
				qBodyElement.firstElementChild.after(qTitleElement);
			}
			
			function DisplayMap(e) {
				// Make sure that this is the view data.
				if (e.data.substr(0, 5) != 'View ') {
					return;
				}
				let saTiles = e.data.substr(5).split(',');
				let saImages = ["Grass.png", "Forest.png", "Bushes.png", "Water.png"];
				const kiSizeX = 5;
				const kiSizeY = 5;
				for (let i = 0; i < kiSizeX; ++i) {
					for (let j = 0; j < kiSizeY; ++j) {
						// Clean the tile before we fill it. This will get rid of prior image elements.
						RemoveAllChildren(qaaMapTiles[i][j]);
						let iIntValue = parseInt(saTiles[kiSizeX*i + j], 10);
						let sBackGroundImage = '';
						// Values outside the map have a value of -1. They do not get an image
						if (iIntValue >= 0) {
							// These tiles will have player images on them
							if (iIntValue > 3) { // We could check for the center and render it differently or use values and different images.
								//sBackGroundImage += 'url("Man.png"), ';
								let iImageIndex = (iIntValue >> 2) - 1;
								qaaMapTiles[i][j].appendChild(gqaManImageElements[iImageIndex]);
							}
							sBackGroundImage += 'url('+saImages[iIntValue%4]+')';
							qaaMapTiles[i][j].style.backgroundImage = sBackGroundImage;
						} else {
							qaaMapTiles[i][j].style.backgroundImage = "none";
						}
					}
				}
			}

			function CreateLocalMap() {
				for (let i = 0; i < 5; ++i) {
					gqaManImageElements[i] = document.createElement("img");//  new Image(100, 100);
					gqaManImageElements[i].src = "Man.png";
					gqaManImageElements[i].style.filter = "brightness(0%) invert(" +(20*(i + 1))+ "%) drop-shadow(5px 5px 2px black)";
				}
				// Create a 5x5 grid of divs for the local map
				let qMapDiv = document.getElementById("idMap");
				let saImages = ["Water.png", "Grass.png", "Bushes.png", "Forest.png"];
				const kiSizeX = 5;
				const kiSizeY = 5;
				for (let i = 0; i < kiSizeX; ++i) {
					qaaMapTiles[i] = [];
					for (let j = 0; j < kiSizeY; ++j) {
						let qNewTile = document.createElement("div");
						qNewTile.classList += "cMapTile";
						qNewTile.style.left = i*100+"px";
						qNewTile.style.top = j*100+"px";
						qaaMapTiles[i][j] = qNewTile;
						qMapDiv.appendChild(qNewTile);
					}
				}
			}

			function UpdatePublicMessages(e) {
				if (e.data.substr(0, 5) != 'Mess ') return;
				let sPlayerNumber = e.data.substr(5).split(' ', 1)[0];
				let sMessage = e.data.substr(5 + sPlayerNumber.length + 1);
				// Display "[Player Number]" before the message to indicate the sender
				let qPublicContainerElement = document.getElementById('idPublic');
				let qMessageContainer = document.createElement('p');
				let qButtonElement = document.createElement('button');
				qButtonElement.textContent = '[' + sPlayerNumber + ']';
				qButtonElement.onclick = function () { gqSharedWorker.port.postMessage('Chat ' + sPlayerNumber); };
					//'Recall that Player ' + sPlayerNumber + ' said \"' + this.nextSibling.innerHTML+'\"'); };
				qMessageContainer.appendChild(qButtonElement);
				let qMessageElement = document.createElement('span');
				qMessageElement.textContent = sMessage;
				qMessageContainer.appendChild(qMessageElement);
				qPublicContainerElement.appendChild(qMessageContainer);
			}

			function BeginPrivateChat(e) {
				if (e.data.substr(0, 5) != 'Chat ') return;
				let sPlayerNumber = e.data.substr(5).split(' ', 1)[0];
				let qPort = e.ports[0];
				// Create the elements for the chat
				let qMainUL = document.getElementById('idPrivate');
				let qNewLI = document.createElement('li');
				let qHeader = document.createElement('h3');
				qHeader.textContent = 'Private chat with ' + sPlayerNumber;
				qNewLI.appendChild(qHeader);
				let div = document.createElement('div');
				let fnAddMessage = function(sPlayerNumber, message) {
					let p = document.createElement('p');
					let n = document.createElement('strong');
					n.textContent = '[' + sPlayerNumber + '] ';
					p.appendChild(n);
					let t = document.createElement('span');
					t.textContent = message;
					p.appendChild(t);
					div.appendChild(p);
				};
				qPort.onmessage = function (e) {
					fnAddMessage(sPlayerNumber, e.data);
				};
				qNewLI.appendChild(div);
				let form = document.createElement('form');
				let p = document.createElement('p');
				let input = document.createElement('input');
				input.size = 50;
				p.appendChild(input);
				p.appendChild(document.createTextNode(' '));
				let button = document.createElement('button');
				button.textContent = 'Post';
				p.appendChild(button);
				form.onsubmit = function () {
					qPort.postMessage(input.value);
					fnAddMessage('me', input.value);
					input.value = '';
					return false;
				};
				form.appendChild(p);
				qNewLI.appendChild(form);
				qMainUL.appendChild(qNewLI);
			}

			function Initialization() {
				// This is done at the window level so that we can prevent the default scrolling.
				window.onkeydown=KeyDownFunction;
				CreateLocalMap();
				gqSharedWorker = new SharedWorker('MapWorker.js');
				let bUseCapture = false;
				gqSharedWorker.port.addEventListener('message', DisplayMap, bUseCapture);
				gqSharedWorker.port.addEventListener('message', SetName, bUseCapture);
				gqSharedWorker.port.addEventListener('message', UpdatePublicMessages, bUseCapture);
				gqSharedWorker.port.addEventListener('message', BeginPrivateChat, bUseCapture);
				// This is required for the event listener.
				gqSharedWorker.port.start();
			}
			function Send(sMessage) {
				gqSharedWorker.port.postMessage(sMessage);
			}
			function RemoveAllChildren(qElement) {
				let qLastChild = qElement.lastElementChild;
				while (qLastChild) {
					qElement.removeChild(qLastChild);
					qLastChild = qElement.lastElementChild;
				}
			}
			function OnFocus() {
				document.body.style.border = "1px red solid";
			}
			function OnBlur() {
				document.body.style.border = "none";
			}
			function KeyDownFunction(e) {
				var iKeyUp = 38;
				var iKeyLeft = 37;
				var iKeyRight = 39;
				var iKeyDown = 40;
				var iKeyLessThan = 188;
				var ikeyGreaterthan = 190;
				var iKeyCode = 0;
				if (e) {
					iKeyCode = e.which;
				} else {
					iKeyCode = window.event.keyCode;
				}
				switch (iKeyCode) {
					case iKeyUp: {
						Send('Move Up');
      			// Prevent the window scrolling
      			e.preventDefault();
						break;
					}
					case iKeyLeft: {
						Send('Move Left');
      			// Prevent the window scrolling
      			e.preventDefault();
						break;
					}
					case iKeyRight: {
						Send('Move Right');
      			// Prevent the window scrolling
      			e.preventDefault();
						break;
					}
					case iKeyDown: {
						Send('Move Down');
      			// Prevent the window scrolling
      			e.preventDefault();
						break;
					}
					case iKeyLessThan: {
						Send('Fill 1');
      			// Prevent the window scrolling
      			e.preventDefault();
						break;
					}
					case ikeyGreaterthan: {
						Send('Fill 3');
      			// Prevent the window scrolling
      			e.preventDefault();
						break;
					}
					default: {
						break;
					}
				}
			}
		</script>
	</head>
	<body onload="Initialization()" onfocus="OnFocus()" onblur="OnBlur()">
		<div style="position:static;width:500px;height:658px;">
			<div id="idMap"></div>
			<div style="position: absolute;top: 508px;background-color:#DDDDDD; width:500px;height:150px;">
				<div id="idButtons">
					<button type=button class="cButton" style="left:50px;top:0px;" onclick="Send('Move Up')">&#x2191;</button>
					<div>
						<button type=button class="cButton" style="left:0px;top:50px;" onclick="Send('Move Left')">&#x2190;</button>
						<!-- Add 1 or -1 = 3 mod 4 -->
						<button type=button class="cButton" style="height:25px;left:50px;top:50px;font-size:medium;" onclick="Send('Fill 1')">&#x21BA;</button>
						<button type=button class="cButton" style="height:25px;left:50px;top:75px;font-size:medium;" onclick="Send('Fill 3')">&#x21BB;</button>
						<button type=button class="cButton" style="left:100px;top:50px;" onclick="Send('Move Right')">&#x2192;</button>
					</div>
					<button type=button class="cButton" style="left:50px;top:100px;" onclick="Send('Move Down')">&#x2193</button>
				</div>
			</div>
		</div>
		<h2>Public Chat</h2>
		<div id="idPublic"></div>
		<form onsubmit="gqSharedWorker.port.postMessage('Mess ' + message.value); message.value = ''; return false;">
			<p><input type="text" name="message" size="50"><button>Post</button></p>
		</form>
		<h2>Private Chat</h2>
		<ul id="idPrivate"></ul>
	</body>
</html>

MapWorker.js

// To debug, use the URL brave://inspect/#workers or chrome://inspect/#workers
var giViewCount = 0;
var gqMap = null;

onconnect = function (qEvent) {
	// If the map was not created, perform the initializations that will created a new one.
	if (gqMap == null) {
		Initialization();
	}
	// Create the new player with his position on the map and get his index.
	let iPlayerIndex = gqMap.AddPlayer(qEvent.ports[0]);
	qEvent.ports[0].postMessage('Name ' + iPlayerIndex);
	qEvent.ports[0].onmessage = GetMessage;
	//let iaPlayerPosition = gqMap.GetPlayerPosition(iPlayerIndex);
	gqMap.UpdateAllMaps();
}
function GetMessage(qEvent) {
	// Find the player with the equivalent port
	let qPlayer = gqMap.FindPlayer(qEvent.target);
	let sAction = qEvent.data.substr(5);
	switch (qEvent.data.substr(0, 5)) {
		case 'Move ':
			switch (sAction) {
				case 'Up':
					qPlayer.Up();
				break;
				case 'Down':
					qPlayer.Down();
				break;
				case 'Left':
					qPlayer.Left();
				break;
				case 'Right':
					qPlayer.Right();
				break;
			}
			// Then call for an update of all of the maps
			gqMap.UpdateAllMaps();
		break;
		case 'Fill ':
			switch (sAction) {
				case '1':
					qPlayer.Bury();
				break;
				case '3':
					qPlayer.Dig();
				break;
			}
			// Then call for an update of all of the maps
			gqMap.UpdateAllMaps();
		break;
		case 'Mess ':
			let sText = qEvent.data.substr(5);
			let qaPlayers = gqMap.GetAllPlayers();
			let iSender = qaPlayers.findIndex(qCheck => qCheck == qPlayer);
			for (let i = 0; i < qaPlayers.length; ++i) {
				let qPlayerPort = qaPlayers[i].GetPort();
				qPlayerPort.postMessage('Mess ' + iSender + ' ' + sText);
			}
		break;
		case 'Chat ':
			let qFirstPlayer = qPlayer;
			let iFirstPlayerIndex = gqMap.GetAllPlayers().findIndex(qCheck => qCheck == qFirstPlayer);
			let iSecondPlayerIndex = parseInt(qEvent.data.substr(5).split(' ', 1)[0]);
			let qSecondPlayer = gqMap.GetAllPlayers()[iSecondPlayerIndex];
			let qChannel = new MessageChannel();
			qFirstPlayer.GetPort().postMessage('Chat ' + iSecondPlayerIndex, [qChannel.port1]);
			qSecondPlayer.GetPort().postMessage('Chat ' + iFirstPlayerIndex, [qChannel.port2]);
		break;
	}
}

function Initialization() {
	gqMap = new CMap();
}

class CMap {
	static #siMapSize = [25, 25];
	#mqaPlayers = [];
	#miaaMapSquares = null;
	constructor() {
		this.#miaaMapSquares = [];
		for (let i = 0; i < CMap.#siMapSize[0]; ++i) {
			this.#miaaMapSquares[i] = [];
			for (let j = 0; j < CMap.#siMapSize[1]; ++j) {
				this.#miaaMapSquares[i][j] = Math.floor(4*Math.random());
			}
		}
	}
	// Return the index of the added player.
	AddPlayer(qPort) {
		// Limit the number of players to 5
		if (this.#mqaPlayers.length < 5) {
			this.#mqaPlayers[this.#mqaPlayers.length] = new CPlayer(this, qPort);
			return this.#mqaPlayers.length - 1;
		} else {
			return -1
		}
	}
	GetAllPlayers() {
		return this.#mqaPlayers;
	}
	FindPlayer(qPort) {
		for (let k = 0; k < this.#mqaPlayers.length; ++k) {
			if (this.#mqaPlayers[k].HasSamePort(qPort)) {
				return this.#mqaPlayers[k];
			}
		}
		return null; // Nothing was found
	}
	UpdateAllMaps() {
		for (let i = 0; i < this.#mqaPlayers.length; ++i) {
			this.#mqaPlayers[i].UpdateConnectionMap();
		}
	}
	GetPlayerPosition(iPlayerIndex) {
		return this.#mqaPlayers[iPlayerIndex].GetPosition();
	}
	GetLocalMap(iaCenter, iaSize) {
		let iaLocalMap = [];
		let iaBoundsX = [iaCenter[0] - Math.floor(iaSize[0]/2), iaCenter[0] - Math.floor(iaSize[0]/2) + iaSize[0]];
		let iaBoundsY = [iaCenter[1] - Math.floor(iaSize[1]/2), iaCenter[1] - Math.floor(iaSize[1]/2) + iaSize[1]];
		for (let i = iaBoundsX[0]; i < iaBoundsX[1]; ++i) {
			for (let j = iaBoundsY[0]; j < iaBoundsY[1]; ++j) {
				// If the coordinates are out of bounds, return -1.
				if (i < 0 || j < 0 || i >= CMap.#siMapSize[0] || j >= CMap.#siMapSize[1]) {
					iaLocalMap.push(-1);
				} else {
					// Check for a player and add it to the map, if it exists.
					let iPlayer = 0;
					for (let k = 0; k < this.#mqaPlayers.length; ++k) {
						let iaPos = this.#mqaPlayers[k].GetPosition();
						if (iaPos[0] == i && iaPos[1] == j) {
							iPlayer = 4*(k + 1);
						}
					}
					iaLocalMap.push(this.#miaaMapSquares[i][j] + iPlayer);
				}
			}
		}
		return iaLocalMap;
	}
	GetValue(iX, iY) {
		return this.#miaaMapSquares[iX][iY];
	}
	SetValue(iX, iY, iValue) {
		this.#miaaMapSquares[iX][iY] = iValue;
	}
	IsOccupied(iX, iY, qPlayerPort) {
		for (let k = 0; k < this.#mqaPlayers.length; ++k) {
			// Only check the position of other players
			if (!this.#mqaPlayers[k].HasSamePort(qPlayerPort)) {
				let qOtherPlayer = this.#mqaPlayers[k];
				let iaOtherPlayersPos = qOtherPlayer.GetPosition();
				if (iX == iaOtherPlayersPos[0] && iY == iaOtherPlayersPos[1]) {
					return true;
				}
			}
		}
		return false;
	}
	static GetMapSize() {
		return CMap.#siMapSize;
	}
}

class CPlayer {
	static #siViewSize = [5, 5];
	static GetViewSize() {
		return CPlayer.#siViewSize;
	}
	#miaPos = [];
	#mqMap = null;
	#mqPort = null;
	constructor(qMap, qPort) {
		let iX = 0;
		let iY = 0;
		let bIsOccupied = true;
		let iaMapSize = CMap.GetMapSize();
		while (bIsOccupied) {
			iX = Math.floor(iaMapSize[0]*Math.random());
			iY = Math.floor(iaMapSize[1]*Math.random());
			bIsOccupied = qMap.IsOccupied(iX, iY, qPort);
		}
		this.#miaPos[0] = iX;
		this.#miaPos[1] = iY;
		this.#mqMap = qMap;
		this.#mqPort = qPort;
	}
	GetPort() {
		return this.#mqPort;
	}
	HasSamePort(qPort) {
		return (this.#mqPort == qPort);
	}
	GetPosition() {
		return [this.#miaPos[0], this.#miaPos[1]];
	}
	SetPosition(iX, iY) {
		if (this.#mqMap.IsOccupied(iX, iY)) {
			return false;
		} else {
			this.#miaPos[0] = iX;
			this.#miaPos[1] = iY;
			return true;
		}
	}
	GetLocalMap() {
		return this.#mqMap.GetLocalMap(this.#miaPos, CPlayer.#siViewSize);
	}
	UpdateConnectionMap() {
		let sTerrain = '';
		let iaLocalMap = this.GetLocalMap();
		let iLocalIndex = 0;
		// Add the first entry without a comma
		sTerrain += iaLocalMap[iLocalIndex];
		for (let i = 1; i < iaLocalMap.length; ++i) {
			++iLocalIndex;
			sTerrain += ','+iaLocalMap[iLocalIndex];
		}
		this.#mqPort.postMessage('View ' + sTerrain);
	}
	Up() {
		// Check whether there is anything in the way.
		let iNewY = this.#miaPos[1] - 1;
		if (iNewY >= 0) {
			if (!this.#mqMap.IsOccupied(this.#miaPos[0], iNewY)) {
				this.#miaPos[1] = iNewY;
			}
		}
	}
	Left() {
		// Check whether there is anything in the way.
		let iNewX = this.#miaPos[0] - 1;
		if (iNewX >= 0) {
			if (!this.#mqMap.IsOccupied(iNewX, this.#miaPos[1])) {
				this.#miaPos[0] = iNewX;
			}
		}
	}
	Right() {
		// Check whether there is anything in the way.
		let iNewX = this.#miaPos[0] + 1;
		if (iNewX < 25) {
			if (!this.#mqMap.IsOccupied(iNewX, this.#miaPos[1])) {
				this.#miaPos[0] = iNewX;
			}
		}
	}
	Down() {
		// Check whether there is anything in the way.
		let iNewY = this.#miaPos[1] + 1;
		if (iNewY < 25) {
			if (!this.#mqMap.IsOccupied(this.#miaPos[0], iNewY)) {
				this.#miaPos[1] = iNewY;
			}
		}
	}
	Bury() {
		let iCurr = this.#mqMap.GetValue(this.#miaPos[0], this.#miaPos[1]);
		let iTwoNewBits = (((iCurr & 3) + 1) % 4);
		iCurr = (iCurr & ~(iCurr & 3)) + iTwoNewBits;
		this.#mqMap.SetValue(this.#miaPos[0], this.#miaPos[1],iCurr);
	}
	Dig() {
		let iCurr = this.#mqMap.GetValue(this.#miaPos[0], this.#miaPos[1]);
		let iTwoNewBits = (((iCurr & 3) + 3) % 4);
		iCurr = (iCurr & ~(iCurr & 3)) + iTwoNewBits;
		this.#mqMap.SetValue(this.#miaPos[0], this.#miaPos[1],iCurr);
	}
}
 

Output

 
 

© 2007–2026 XoaX.net LLC. All rights reserved.