Saturday, 28 July 2012

WebApp Multiplayer + Native UI

The networking architecture behind the instant multiplayer deathmatch games; Phone Wars and Food Fighters I've been prototyping are backed by web technologies. Even though the end package is delivered as a native application and powered by OpenGL, behind the scenes the app uses WebViews to handle the communication with the multiplayer server.

This allows us to reuse the same code base across multiple platforms be it Android, iPhone, PC or even the Web itself.

Initially when prototyping the idea out in a reverse-phoneGap approach, I thought it'd be too slow and unpractical to work. However, so far, it's been working great, and let me tell you the best reason why.

When working on a multiplayer game, you tend to come across lot's of different connection issues especially when gating users from around the world.

Using Socket IO, I found that clients have a tendency to drop out randomly. Typically, the way I match make players was to add players onto a queue when they're ready to play.

 socket.on( 'BSRegisterPlayer', function(type)  
   {  
     var playerQueue = service.playerQueue;  
     playerQueue.addOnce( socket );
     socket.playerType = type;
     service.matchmake();  
   });  


Now when another player comes along, in the matchmake function the server would assign them to an available game and instruct them to start loading.

matchmake = function()  
 {  
   if( playerQueue.length >= 2 )  
   {  
     var gameRooms = gameRooms;  
     var gameRoomsLength = gameRooms.length;  
     for( var gameIndex=0; gameIndex<gameRoomsLength; ++gameIndex )  
     {  
       var game = gameRooms.list[gameIndex];  
       var players = game.players;  
       if( players.length == 0 )  
       {  
         // Timeout seconds for loading the game  
         game.timer = 20;  
   
         var player1 = playerQueue.popFirst();  
         var player2 = playerQueue.popFirst();  
         players.addOnce( player1 );  
         players.addOnce( player2 );  
   
         player1.loadingGame = game;  
         player2.loadingGame = game;  
   
         var loadGameInfo = { loadGame: game.gameID };  
         loadGameInfo.timer = game.timer;  
         loadGameInfo.players = [];  
         {  
           var player = player1;  
           var playerData = { userID:player.userID };  
           playerData.wins = player.user.wins;  
           playerData.losses = player.user.totalLosses;  
           playerData.deviceType = player.user.deviceType;  
           loadGameInfo.players.push( playerData );  
         }  
         {  
           var player = player2;  
           var playerData = { userID:player.userID };  
           playerData.wins = player.user.wins;  
           playerData.losses = player.user.totalLosses;  
           playerData.deviceType = player.user.deviceType;  
           loadGameInfo.players.push( playerData );  
         }  
   
         player1.emit( 'GameUpdate', loadGameInfo );  
         player2.emit( 'GameUpdate', loadGameInfo );  
   
         player1.health = 100;  
         player2.health = 100;  
         player1.location = "-50, 0, 0";  
         player2.location = "50, 0, 0";  
         break;  
       }  
     }  
   }  
 }  


A timer to give a maximum of 20 seconds for both players to load the game and report back to the game that it's been loaded. Now, this logic seemed to work find in all my local tests over 3G and wifi using my phones against myself and friends. However, when playing players from China and various parts of the world I found myself occasionally being match made against ghost connections. Which are players that Socket IO think are connected, but have actually disconnected and Socket IO is just waiting for their close timeout interval to be exceeded.

This lead to a poor user experience with a game abandoned message after having the game load.

In an attempt to reduce the number of abandoned games, I wanted to add in a pre-start loading game handshake, that simply pinged both clients before signalling them to start loading the game. Now when you're releasing the first version of your game, it's fine to make last minute changes, but when you already have your game released, in order to make a change like this. You'd usually have to put a bunch of if( clientVersion > x ) checks around any new feature changes in order to not break the game for clients on older versions. For Android you can upload a new version within hours, iOS takes weeks, and actually waiting for the users to update your game, it could be a while to implement such a change.

But in the world, of web, you can simply modify the client side js file, and enforce that the native application re-downloads your file on each new connection. This means that every time a player logs in, you know they have the latest client side changes.

Knowing this, I added this split the matchmake function into two. One that pings the client.
matchmake = function()  
   {  
   console.log( "BS matchmake request", playerQueue.length );  
   
   if( playerQueue.length >= 2 )  
   {    
     var gameRoomsLength = gameRooms.length;  
     for( var gameIndex=0; gameIndex<gameRoomsLength; ++gameIndex )  
     {  
       var game = gameRooms.list[gameIndex];  
       var players = game.players;  
       console.log( "BS matchmake find game", game.gameID, players.length );  
   
       if( players.length == 0 )  
       {  
         var player1 = playerQueue.popFirst();  
         var player2 = playerQueue.popFirst();  
         players.addOnce( player1 );  
         players.addOnce( player2 );  
   
         player1.pingingGame = game;  
         player2.pingingGame = game;  
   
         // Timeout for ping back  
         game.timer = 5;  
         game.timeout = function() { BS.matchmakeTimeout( this ); };  
   
         player1.emit( 'BSMatchmakePingClient' );  
         player2.emit( 'BSMatchmakePingClient' );  
         break;  
       }  
     }  
   }  
 }  

Then in the Client js file, when the ping request is received I'd have it send the confirmation straight back.
 socket.on( 'BSMatchmakePingClient', function ()  
   {  
     socket.emit( 'BSMatchmakePingBackServer' );  
   });  

If the ping back was received in time, the server would go on to tell the clients to load the game, increasing the probability of ensuring both clients have a good connection to the server. If the client responded too late, the client who had responded would be re-match made with another player and the other one would either be disconnected or added to the back of the queue.
matchmakeTimeout = function(game)  
 {
   var players = game.players;
   while( players.length > 0 )  
   {  
     var player = players.popFirst();  
     if( player.pingingGame )  
     {  
       delete player.pingingGame;  
     }  
     else if( player.loadingGame )  
     {  
       delete player.loadingGame;  
       playerQueue.addFirst( player );  
     }  
   }  
   
   matchmake();  
 }  
   
   
matchmakePingBackServer = function(socket)  
 {  
   var game = socket.pingingGame;  
   if( game )  
   {  
     delete socket.pingingGame;  
     socket.loadingGame = game;  
   
     var gameReady = false;  
     var players = game.players;  
     var length = players.length;  
     for( var i=0; i<length; ++i )  
     {  
       var player = players.list[i];  
       if( player != socket )  
       {  
         if( player.loadingGame )  
         {  
           gameReady = true;  
           break;  
         }  
       }  
     }  
   
     if( gameReady )  
     {  
       matchLoadGame( game );  
     }  
   }  
   
   // Pingged back too slow, add to back of queue  
   else  
   {  
     playerQueue.add( socket );
     matchmake();  
   }  
 }  

This would all be done transparently, to hopefully give the players a better match making experience from the initial try to load, report disconnection after 20 seconds flow.

So there, just like that a new connection detection feature got added to the game, without going through the app stores approval process, and without having the client go through the manual update app process.

Awesome huh
JavaScript + Native UI.. It's the future?