Fun with WebSockets - The client layer
Last blog we covered how to create a very simple WebSockets layer for handling connections from an HTML / JS client and a Unity C# server. This time around it's the turn of the client.
To have a client, we have to have an idea of what we want to create. Our "game", and I use the term very loosely here, will allow us to move around a map, and see where the other people playing the game are.
If you were to add in touching anyone kills you or them then you'd have half the *.io games. We're not going to, but, you know, if you did...
To accomplish our goal then, we're going to create a simple UI that waits until a server exists and when it does, joins it and starts playing the "game" immediately.
The code
To keep it simple, we're going to create a very basic HTML setup that'll give us a waiting page and a map page.
<div id="server-waiting" class="hide">
<p>Waiting for server connection . . .</p>
</div>
<div id="game">
<div id="map">
<span class="client" id="pos">x</span>
</div>
</div>
When the "game" is waiting, the server-waiting layer will be displayed. As soon as the game is ready to start, that'll disappear and we'll see the game layer with the one map on it.
You play the "game" by pressing anywhere on the map which will make your avatar move slowly towards it. Where you press is represented by an "x", which is included in the html above. All of the client avatars will be shown using an "o" and drawn dynamically later.
We're now ready to handle the connection to the WebSockets layer.
var connection = new ReconnectingWebSocket('ws://localhost:100');
connection.onopen = function(e) {
console.log("connected");
connection.send('{"connect": "client"}');
};
connection.onerror = function (e) {
console.log('An error occurred.');
console.log(e);
connect(false);
};
connection.onmessage = function(e) {
var messageData = JSON.parse(e.data);
console.log(messageData);
if (messageData.client_id) {
clientId = messageData.client_id;
}
if (typeof messageData.server_data !== "undefined" && messageData.server_data.length !== 0) {
if (!isServerConnected) {
connect(true);
}
var clients = messageData.server_data.clients || [];
for (var id in clients) {
if (clients.hasOwnProperty(id)) {
moveClient(clients[id]);
}
}
}
if (messageData.server_disconnect) {
connect(false);
}
};
In the above code, we're using ReconnectingWebSocket()
instead of the default WebSocket()
. With the original WebSocket()
, if you lose connection then you have to reconnect manually. This code just handles that reconnection for you.
If you need to change where the WebSockets connection layer is hosted, change the url and port in this snippet of code.
When a connection is opened, we let the connection layer know that this connection is a client connection. The only real difference in this example code is there can only be one server, and the server manages what the clients see.
If there's an error, we'll close the connection. This normally occurs when the connection layer has gone away.
Messages make up the bulk of our interactions with the connection layer. All of our communication is in JSON, so first we need to decode that. If a client_id's set, then we store that so we know what number our client is.
The data we're passing around is relatively simple, so we're just going to store a list of clients with their ids, x-position, and y-position. If we find a list of clients then we're going to move each one around.
The code for moving the clients is:
function moveClient(data)
{
var clientObj = $('#client-' + data.id);
if (clientObj.length > 0) {
clientObj.remove();
}
var map = $('#map');
console.log(data.id, clientId);
var clientClass = data.id === clientId ? 'client-pos' : 'client-other';
clientObj = $('<div class="client ' + clientClass + '" id="client-' + data.id + '">o</div>')
.css('top', map.height() * data.y)
.css('left', map.width() * data.x);
map.append(clientObj);
}
It's inefficient, but good enough for our test. The above code removes all clients and then re-creates a new client object for this client and any others.
When the server connects and disconnects, we run a function connect().
This simply works out whether to show the server-waiting layer and does a few little tidying tasks:
function connect(isConnected)
{
isConnected = isConnected || false;
isServerConnected = isConnected;
$('#server-waiting').toggleClass('hide', !isConnected);
if (!isServerConnected) {
$('#pos').removeClass('visible');
$('.client-pos, .client-other').remove();
}
}
The last part of the puzzle is to allow pressing the map to send back coordinates to the connection layer:
$('#map').click(function (e) {
var posX = e.pageX - $(this).offset().left,
posY = e.pageY - $(this).offset().top;
$('#pos')
.css('left', posX)
.css('top', posY)
.addClass('visible');
connection.send(JSON.stringify({
"data": {
"x": posX / $(this).width(),
"y": posY / $(this).height()
}
}));
});
This works out the percentage of the x and y position and then sends it over.
And that's it for the client. The code can be downloaded from github.com/dittto/sockets-client. The next blog will complete this little run by covering how to speak to the connection layer in Unity and control the clients from there.