Ascendro Blog

Displaying 1-1 of 1 result.

Do not get annoyed, buddy

A huge surprise was waiting for our team at wednesday morning. They got called for a meeting and before they knew what is going on they were part of an online match of "Don't get annoyed, buddy".

What happened? In our continuous drive to keep up with latest technology we created a TechDemo featuring 3D-Graphic Rendering, Server Side Javascript and WebSockets. Making it possible to create real time web based applications.

But let's describe what we did:

The Game

From user perspective the game persists of two screens, a lobby and the game board itself. By going onto a specific url with a compatible browser (webgl and websockets) you got a short loading notification in which a connection to the server in form of a WebSocket is established. The lobby is displayed and you can choose between two ways to move on.

Besides entering your name you are able to join an ongoing game, if it doesn't contain already four players, or create a new game. Either way you will be switched onto the Gameboard and are now able to play!

There are no rule validations so everybody is responsible for keeping it ordered - like in the good old days. On the free rotatable and zoomable game board every player is represented with a small bar in his color. With this bar he can select and move around game pieces on the board.

A "dice box" in the lower left part of the screen shows the actual value of the dice and gives a button for rolling it again. A dice roll is done completely on the server side and synchronized with all clients.

Only a few test rounds already showed that cheating isn't that much of a problem but keeping track who made the last turn - as you don't see who is rolling the dice and it is even possible that a second player starts rolling the dice while the roll of the first player isn't finished - making both player believe that it is their move.

Beside this small design flaw it is a pretty solid game and is actually a lot of fun to play with your friends who really don't expect that if you present them an innocent looking link.

The Server

We are using node.js with the packages socket.io and mime to provide a simple web server supporting WebSockets.

For organizing the players and game rooms two additional javascript "classes" where created. They are used as a data storage and replace any need for a database. (We considered several different options but there's just nothing which can top in process memory if it comes about access speed.)

Socket.io provides a nice wrapper around WebSockets, HTTP Long Polling, Flash and other solutions in order to communicate bidirectional with a client. Though we never tested non compatible browsers we rely just on the simple interface Socket.io provides. It is possible to name messages and register special callbacks if a named message is received. Therefore it is very simple to implement an custom application protocol.

 //Move Player
  socket.on('mp', function (data) {
		socket.get('player', function (err, player) {
			player.setPosition(data[0],data[1]);
		}); 	  
  });

The example shows what happens, if the client sends a named message "mp". The player object which is attached to the socket is retrieved and then the position is changed.

Even though modern javascript is meant to be programmed event oriented we decided against a direct synchronization with the other connected players. As all players send constantly position updates as soon as they move their mouse this would result into PlayerCount * (PlayerCount-1) outgoing messages in the worst case scenario of every user moving (which is not uncommon as people like to move their mouses!).

p.setPosition = function(x,y) {
	this.position = [x,y];
	this.needsPosUpdate = true;	
}

In fact all it does is changing its attributes and marking itself as "needs to be updated". The synchronization with the other players is done in so called containers which are called periodically every x ms. We created two containers, one with 50ms and one with 1000ms. The 1000ms container is handling organizational stuff like the game list for players who are still in the lobby.

 var container1000ms = setInterval(function() {
	//Update of roomlist
	if (Room.prototype.static_roomListNeedsUpdate()) {
		var lobbyPlayer = Player.prototype.static_getLobbyList();
		var length = lobbyPlayer.length;	
		var roomList = Room.prototype.static_getOverviewList();
		for (var i = 0;i < length;i++) {
			lobbyPlayer[i].socket.emit('roomList', roomList);
		}
		Room.prototype.static_roomListUpdated();
	}
}, 1000);

Important here: We only send data if it changed and only to the players who are currently in the lobby. 

The 50ms container is handling player movement and other real time data. It adds on average a latency of 50/2ms (25ms) to the network latency due to the fact that we don't synchronize immediately. We are willing to take that price for the reduced amount of data packages from PlayerCount * (PlayerCount-1) to PlayerCount.

The Protocol

An example server client communication can look like this:

It is by far not the best imaginable protocol but it completely serves it purpose. If we look at the information we need to transfer we save already a lot by collecting all changes and pushing them out together. Still we make two different calls, one for moving the objects and one for moving the players.

Looking at such a data frame we can also see that the format isn't the best choice for fast data transmission. By using json-decoded data we are wasting a lot of bits for json syntax as well as strings and numbers in a text format. Better would be to set up a binary format, using max 8 bits for the message type (saving >11bytes) and an array of a fixed struct for the object position. (Using 1 byte for the object id as well as 32bit integer for a fixed comma value representing their position.)

     Message Type       Array of Objects to update with Object ID and Object Position in [x,y,z]
5:::{"name":"mo","args":[[[13,-401.6337666808153,-192.53480423463742,25]]]}

The example frame is using 76bytes - a binary format which could look like this:

typedef struct {
	unsigned char type;
	union {
		struct {
			unsigned char count;
			struct Object list[];
		} objectList;
	} u;
} Message;

typedef struct {
	unsigned char id;
	unsigned int x;
	unsigned int y;
	unsigned int z;
} Object;

could use just 19 bytes. (15 bytes + 4 bytes from the SocketIO Protocol).

Adding it together with around 40 byte header of the TCP/IP Stack we would compare 116 bytes versus 59 bytes - a saving of 50%. (The savings will increase in case there are more objects moved in a specific time)

This is a future improvement if we will run into performance issues. Much faster results can be achieved by cutting some numbers behind the comma already in the text form.

The container model can generate performance issues as well. If there are a huge amount of users on the server the container needs some time in order to process all the players which need to receive updates. In that time no other event can be handled. 

It is a simple pleasure to program in node.js without thinking about the problems concurrently running threads could cause but on the other side you can cause the server to be not able to respond in time if you run huge operations. Solutions for this can be:

  • Split the containers in several containers each running on 50ms or something - so that other events can be processed during that time.
  • Take the data storage out of the process memory and put them into a thread safe data storage (like webcache, mysql in memory tables or radis) and put the containers on a separate process.

The Client

The client is using three.js for rendering 3D objects with WebGL. The mechanics used are pretty similar to the demo provided by ThreeJS. Using the "OrbitControl" controller and modifying it slightly we get a solid base for displaying our gaming board which is itself just a plane with a texture on it.

The game objects where done with the ThreeJS Editor and then exported as geometries which are then read by the client script.

The main controller is a simple state machine dividing into "Lobby Screen" and "Game Room Screen" - two prepared HTML areas are respectively hidden or displayed.

<div id="pageStart">
    <h1>Ascendro Games</h1>        
    <p>
        <b>Your name:</b> <input id="playerName" type="text" /> 
    </p>
    <p>
        <b>Create a new game:</b> <input id="roomName" type="text" /> <button type="button" onclick="createRoom($('#roomName').val()); return false;">Create</button>
    </p>        
    <p>
        <b>Join existing game:</b> <br />
        <ul id="gameList"></ul>
    </p>            
</div>
<div id="pageGame">
    <div class="gameHeadline">
        <h2>Ascendro Games - <span id="gameRoomName">[ROOM NAME]</span></h2>
    </div>
    <div class="gameField"></div>
    <div class="gameFooter">
        <div class="room diceRoom">
            Dice:<br />
            <h1 id="dice">1</h1>
            <button type="button" onclick="rollDice(); return false;">Roll the dice!</button>
        </div>
        <div class="room chatRoom">
            <textarea id="chatMessages" readonly="readonly" cols="45" rows="5"></textarea><br />
            <input id="chatField" type="text" /> 
        </div>
        <div class="room playerList">
            <ol id="playerList"></ol>
        </div>
    </div>
</div>
socket.on('you',function(data) {
    //Go on with next step
    player = data;
    if (player.room == -1) {
        initPageLobby();
    } else {
        initPageRoom();
    }
});

function createRoom(name) {
    if (socket) {
        socket.emit('createRoom',name);
    }
}

The rest is handled via events received by the WebSocket which connection is established at the very beginning or by user input.

The WebGL Canvas is generated on initialization of the Game Room and the objects are placed on the receiving of the "fullRoomInfo" from the server.

socket.on('mo', function(objectPositions) {
    var length = objectPositions.length;
    for (var i = 0;i < length;i++) {
        updateObject(objectPositions[i][0],objectPositions[i][1],objectPositions[i][2],objectPositions[i][3]);
    }
});
                
function updateObject(id,x,y,z) {
    if (objects[id] && objects[id] != SELECTED) {
        objects[id].position.x = x;
        objects[id].position.z = y;
        objects[id].position.y = z;
    }
}

There is no buffer between the objects received by the server and the real drawn objects. Changes are directly applied to the graphic scenes of WebGL.The movement of objects is done by registering events on mouse move and click as well as raytracing to get any collision of the "mouseray" with a potential object.

This is shown in several three.js demos as well.

var vector = new THREE.Vector3( mouse.x, mouse.y, 0.5 );
projector.unprojectVector( vector, camera );
var raycaster = new THREE.Raycaster( camera.position, vector.sub( camera.position ).normalize() );
var intersects = raycaster.intersectObjects( fields );
if (intersects.length > 0 && playerObjects[currentPlayer]) {
    playerObjects[currentPlayer].position.x = intersects[0].point.x;
    playerObjects[currentPlayer].position.z = intersects[0].point.z;
    if (socket) {
        socket.emit("mp",[intersects[0].point.x,intersects[0].point.z]);
    }
}

Again objects are moved directly in the graphic scene without an extra instance of the object itself. 

All in all the client is written straight forward and could be reimplemented in a more dynamic approach - like allowing different gaming boards/type of games as well as different geometries for the gaming objects - but for our case it was enough.

Summary

Beside the great fun we had by implementing and testing this TechDemo, we got a nice insight into the world of server side Javascript and WebGL. It is very comfortable to use the same language for the frontend and backend development but there are several downsides with node.js as well. By handling all connections in a single process you are opening it to a complete new set of security vulnerabilities as well as stability issue. If there is only one little item which could cause the server to crash, all connections will be left unhandled. In comparison to Apache which spawns a thread per connection/using a thread pool and will handle each request on its own. 

The performance was very good - but unfortunately we couldn't test the limits. On our pretty weak host system we still never used more than 13% of the CPU on a full 8 player game (2 rooms a 4 player).

The article will end with the conclusion that much more technology needs to be tested before praising it too much. How it is about testing abilities, are the provided frameworks really serious alternative, is the simplicity of non-concurrently running code worth the drawbacks?

If you got interested in this demo as well, feel free to contribute on https://github.com/ascendro/AscendroGames

LEAVE A COMMENT

Displaying 1-1 of 1 result.

c says:

<script>alert(document.cookie);</script>