Skip to content

Instantly share code, notes, and snippets.

@rudiedirkx
Last active August 29, 2015 14:07
Show Gist options
  • Save rudiedirkx/d99d885744ac886cd4ab to your computer and use it in GitHub Desktop.
Save rudiedirkx/d99d885744ac886cd4ab to your computer and use it in GitHub Desktop.
<!doctype html>
<html lang="en">
<head>
<title>Multiplayer</title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=300, height=500, initial-scale=1" />
<style>
* { box-sizing: border-box; font-size: 20px; font-family: arial; margin: 0; padding: 0; }
html { height: 100%; border: solid 20px black; }
html.active { border-color: green; }
html.error { border-color: red; }
html, body {
overflow: hidden;
}
body {
height: 100%;
position: relative;
padding: 20px;
-webkit-user-select: none;
}
p {
margin: 0 0 20px 0;
}
.client {
position: absolute;
width: 30px;
height: 30px;
background: none #ddd;
z-index: 2;
margin-left: -15px;
margin-top: -15px;
}
.client.me-client {
border: solid 5px #000;
z-index: 3;
cursor: pointer;
}
</style>
<script>
function _log() {
return console.log.apply(console, arguments);
}
(function(P) {
P.on = P.addEventListener;
P.sendJSON = function(data, callback) {
return this.send(JSON.stringify(data), callback);
};
P.sendCmd = function(cmd, data) {
data || (data = {});
data.cmd = cmd;
return this.sendJSON(data);
};
})(WebSocket.prototype);
</script>
</head>
<body onload="init()">
<!-- <form onsubmit="socket.sendCmd('eval', {code: this.elements.code.value}); return false">
<p>Eval:<br><textarea name="code" rows="5" style="width: 100%">this.wsServer.connections.forEach(function(client) {
_log(client.data);
});</textarea></p>
<p><button>Run</button></p>
</form> -->
<div id="clients"></div>
<script>
window.onerror = function(e) {
alert(e);
};
var html = document.documentElement,
body = document.body,
clients = document.querySelector('#clients'),
bsize = [0, 0],
boffset = [0, 0];
function createClient(data, me) {
el = document.createElement('div');
el.dataset.id = data.id;
el.textContent = '';
el.className = 'client';
me && (el.className += ' me-client');
el.style.backgroundColor = data.color;
el.style.left = data.coords[0] + '%';
el.style.top = data.coords[1] + '%';
clients.appendChild(el);
return el;
}
function updateClientCoords(x, y, relative) {
var el = document.querySelector('.client.me-client');
if ( relative ) {
x = parseInt(el.style.left) + x;
y = parseInt(el.style.top) + y;
}
x = x / bsize[0] * 100;
y = y / bsize[1] * 100;
el.style.left = x + '%';
el.style.top = y + '%';
socket.sendCmd('move', {
coords: [x, y],
});
}
var commands = {
// The server accepts me and tells me where to start
join: function(msg) {
if ( !msg.coords ) {
throw "Missing 'coords' in message.";
}
var el = document.querySelector('.client.me-client');
if ( el ) {
// Server sent 'join' more than once = bad, but accept new coords
el.style.left = msg.coords[0] + '%';
el.style.top = msg.coords[1] + '%';
}
else {
createClient(msg, true);
msg.others.forEach(function(other) {
createClient(other);
});
console.timeEnd('Joined game.');
}
},
// Someone else joined the game
party: function(msg) {
createClient(msg);
},
// Someone left the game
unparty: function(msg) {
var client = document.querySelector('.client[data-id="' + msg.id + '"');
client.style.display = 'none';
},
// Someone moved
partymove: function(msg) {
var client = document.querySelector('.client[data-id="' + msg.id + '"');
client.style.left = msg.coords[0] + '%';
client.style.top = msg.coords[1] + '%';
},
};
var url = location.protocol.replace('http', 'ws') + "//" + location.hostname + ":8084",
socket;
function init() {
var cr = body.getBoundingClientRect();
boffset[0] = cr.left;
boffset[1] = cr.top;
bsize[0] = cr.width;
bsize[1] = cr.height;
console.time('Websocket initialized.');
console.time('Joined game.');
_log('initializing socket...');
socket = new WebSocket(url);
socket.on("open", function(e, name) {
_log('on open');
html.classList.add('active');
console.timeEnd('Websocket initialized.');
var tracking = false;
body.addEventListener('mousedown', function(e) {
if ( e.target.classList.contains('me-client') ) {
tracking = true;
}
});
body.addEventListener('mousemove', function(e) {
if ( tracking ) {
updateClientCoords(e.x - boffset[0], e.y - boffset[1]);
}
});
body.addEventListener('mouseup', function(e) {
tracking = false;
});
body.addEventListener('touchstart', function(e) {
e.preventDefault();
if ( e.target.classList.contains('me-client') ) {
tracking = true;
}
});
body.addEventListener('touchmove', function(e) {
e.preventDefault();
if ( tracking ) {
var x = e.targetTouches[0].pageX - boffset[0];
var y = e.targetTouches[0].pageY - boffset[1];
updateClientCoords(x, y);
}
});
body.addEventListener('touchend', function(e) {
tracking = false;
});
});
socket.on("message", function(e) {
_log('message', e);
try {
var msg = JSON.parse(e.data);
if ( !msg.cmd ) {
throw "Missing 'cmd' in message.";
}
if ( !commands[msg.cmd] ) {
throw "Invalid cmd '" + msg.cmd + "'.";
}
}
catch (ex) {
_log('INVALID INCOMING:');
_log(e.data);
return;
}
var cmd = commands[msg.cmd];
try {
_log("Executing '" + msg.cmd + "'", msg);
console.time("Executed '" + msg.cmd + "'");
cmd.call(this, msg);
console.timeEnd("Executed '" + msg.cmd + "'");
}
catch (ex) {
console.error("Error while executing '" + msg.cmd + "': " + ex);
}
});
socket.on("error", function(e) {
_log('on error', e);
});
socket.on("close", function(e) {
_log('on close', e);
html.classList.remove('active');
html.classList.add('error');
});
}
</script>
</body>
</html>
var websocket = require('websocket'),
rwebsocket = require('./rwebsocket.js');
function _log(msg) {
console.log.apply(console, arguments);
console.log('');
}
// `this` is a WebSocketConnection, which has a `wsServer`, which has a `connections`
var commands = {
// eval: function(msg) {
// eval(msg.code);
// },
_open: function() {
var x = Math.random() * 100,
y = Math.random() * 100,
coords = [x, y],
color = '#' + Math.random().toString(16).substr(-6);
this.data.coords = coords;
this.data.color = color;
var msg = {
id: this.data.id,
coords: coords,
color: color,
};
var others = [];
// Notify all other clients about this new client
this.withAllOtherClients(function(client) {
client.sendCmd('party', msg);
others.push({
id: client.data.id,
coords: client.data.coords,
color: client.data.color,
});
});
// Notify the client about hisself and all other clients
msg.others = others;
this.sendCmd('join', msg);
},
_close: function() {
this.withAllOtherClients(function(other) {
other.sendCmd('unparty', {id: this.data.id});
});
},
move: function(msg) {
this.data.coords[0] = msg.coords[0];
this.data.coords[1] = msg.coords[1];
msg.id = this.data.id;
this.withAllOtherClients(function(other) {
other.sendCmd('partymove', msg);
});
}
};
var options = {
port: 8084,
commands: commands,
}
var rws = rwebsocket(options, websocket);
var wsServer = rws.wsServer;
var http = require('http'),
util = require('util');
function _log(msg) {
console.log.apply(console, arguments);
console.log('');
}
function _inspect(ass, depth) {
undefined === depth && (depth = 3);
if ( typeof ass != 'string' && typeof ass != 'number' ) {
ass = util.inspect(ass, false, depth, true);
}
return _log(ass);
}
function _id() {
var id = String(Math.random()).substr(2, 12);
if ( id[0] == '0' ) {
return _id();
}
return id;
}
/**
* Options:
* - port
* - commands
*/
module.exports = function(options, websocket) {
var HTTPServer = http.Server;
var WebSocketServer = websocket.server;
var WebSocketConnection = websocket.connection;
var commands = options.commands || {};
// Extend all client objects
WebSocketConnection.prototype.sendCmd = function(cmd, data) {
data || (data = {});
data.cmd = cmd;
return this.send(JSON.stringify(data));
};
WebSocketConnection.prototype.withAllOtherClients = function(callback) {
this.wsServer.connections.forEach(function(client) {
if ( client != this ) {
callback.call(this, client);
}
}, this);
};
WebSocketConnection.prototype.withAllOtherClientsInGame = function(callback, game) {
if ( game == null ) {
game = this.data.game;
}
this.wsServer.connections.forEach(function(client) {
if ( client.data.game == game && client != this ) {
callback.call(this, client);
}
}, this);
};
// Dfine client event handlers here, so they exist only once
var onMessage = function(message) {
if (message.type === 'utf8') {
try {
var msg = JSON.parse(message.utf8Data); // THROWS
if ( !msg.cmd ) {
throw "Missing 'cmd' in message."; // THROWS
}
_log('Incoming:');
_inspect(msg);
var cmd = commands[msg.cmd];
if ( !cmd ) {
throw "'" + msg.cmd + "' is not a valid command."; // THROWS
}
try {
cmd.call(this, msg); // THROWS
}
catch (ex) {
throw "Error while executing '" + msg.cmd + "': " + ex.message; // THROWS
}
}
catch (ex) {
_log('Invalid cmd:');
_log(message.utf8Data);
_log(ex);
// this.sendCmd('error', {error: ex.message});
}
}
};
var onClose = function(reasonCode, description) {
var hours = (Date.now() - this.socket._opened) / 1000 / 60 / 60;
_log('Client leaves: ' + this.data.id + ', after ' + (Math.round(hours * 100) / 100) + ' hours');
if ( commands._close ) {
commands._close.call(this, {});
}
};
// Create HTTP and WebSocket servers
var httpServer = new HTTPServer().listen(options.port, function() {
_log('Server is listening on port ' + options.port);
});
var wsServer = new WebSocketServer({
httpServer: httpServer,
});
wsServer.on('request', function(request) {
// We take anyone
var client = request.accept();
client.wsServer = wsServer;
client.socket._opened = Date.now();
// Custom data
client.data || (client.data = {});
client.data.id = _id();
client.data.game = 1;
_log('New client: ' + client.data.id); // client.socket._peername.port
client.on('message', onMessage);
client.on('close', onClose);
// Optionally attach more client event listeners
if ( commands._on ) {
for ( var type in commands._on ) {
client.on(type, commands._on[type]);
}
}
// And then run the client init/open command
if ( commands._open ) {
commands._open.call(client, {});
}
});
return {
httpServer: httpServer,
wsServer: wsServer,
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment