Sunday, 22 July 2012

Multiplayer Games Need An Activity Feed

Working on a multiplayer game is pretty fun, when you're challenging and playing your friend next to you, it's pretty easy to set up and play the game.

When you're challenging and playing someone somewhere around the world, it gets harder. As the communication channels just aren't there.

Some of the problems faced when setting up multiplayer games is trying to inform the players who's connected, and when someone else is available to play. Asynchronous multiplayer games have it easier as players can action their moves turn by turn in asynchronous time. Real time multiplayer games are more challenging.

Last week we introduced push notifications to help inform players when someone else was waiting to play, however we noticed that although this solution was pretty popular with our players, there was still some major issues. As several players were receiving the message at the same time, when more than one logs in to accept the challenge, the other players would log in to find no one waiting to play. Or even worse, by the time a player had connected to accept the challenge, the challenger would disconnect.

To solve this, we're trying out an activity feed, which logs all the major actions of the players logged in.

To implement such a system was pretty simple.

On the server side, we keep an array of the last 10 activity messages. When a player logs in, or registers to play, or logs out, or a match is made/ended, these messages are pushed onto the stack along with the UTC time the request was made.

 BurgersServer.prototype.updateActivityFeed = function(message)  
 {  
   console.log( "updateActivityFeed", message );  
   
   var activityFeed = this.activityFeed;  
   while( activityFeed.length > 10 )  
   {  
     activityFeed.popFirst();  
   }  
   
   var feedMessage = [GetTime(), message];  
   activityFeed.add( feedMessage );  
   
   var sockets = this.sockets;  
   for( var i=0; i<sockets.length; ++i )  
   {  
     var socket = this.sockets.list[i];  
     socket.emit( 'BurgersGameUpdate', { feed: [feedMessage] } );  
   }  
 }  

The messages are then broadcast to all connected clients. On the clientside javascript, the UTC time is converted back into local time.
 socket.on( 'BurgersGameUpdate', function (data)  
   {  
     debugLog( 'BurgersGameUpdate ' + JSON.stringify( data ) );  
   
     if( data.feed )  
     {  
       var minutesOffset = -( new Date().getTimezoneOffset() );  
       var hoursOffset = minutesOffset / 60;  
       minutesOffset = minutesOffset % 60;  
   
       var feed = data.feed;  
       var length = feed.length;  
       for( var i=0; i<length; ++i )  
       {  
         var time = feed[i][0];  
         var serverTime = time.split( ":" );  
         var hours = parseInt( serverTime[0], 10 );  
         var minutes = parseInt( serverTime[1], 10 );  
         minutes += minutesOffset;  
   
         if( minutes < 0 )  
         {  
           hours--;  
           minutes += 60;  
         }  
         else if( minutes >= 60 )  
         {  
           hours++;  
           minutes += 60;  
         }  
   
         hours += hoursOffset;  
         if( hours < 0 )  
         {  
           hours += 24;  
         }  
         else if( hours >= 24 )  
         {  
           hours -= 24;  
         }  
   
         if( hours < 10 )  
         {  
           hours = "0" + hours;  
         }  
         if( minutes < 10 )  
         {  
           minutes = "0" + minutes;  
         }  
   
         feed[i][0] = hours + "," + minutes;  
       }  
     }  

Then in the game, the last 10 messages are displayed in the character select lobby screen.
 if( json_object_get( jsonData, "feed" ) )  
   {    
     json_t *feedData = json_object_get( jsonData, "feed" );  
     if( feedData != NULL )  
     {  
       CCText time;  
       CCText message;  
       if( json_is_array( feedData ) )  
       {  
         const uint length = json_array_size( feedData );  
         for( uint i=0; i<length; ++i )  
         {  
           json_t *jsonFeed = json_array_get( feedData, i );  
           if( jsonFeed != NULL )  
           {  
             if( json_is_array( jsonFeed ) )  
             {  
               if( json_array_size( jsonFeed ) == 2 )  
               {  
                 json_t *jsonFeedTime = json_array_get( jsonFeed, 0 );  
                 json_t *jsonFeedMessage = json_array_get( jsonFeed, 1 );  
                 if( jsonFeedTime != NULL && jsonFeedMessage != NULL )  
                 {  
                   time = json_string_value( jsonFeedTime );  
                   message = json_string_value( jsonFeedMessage );  
                   updateActivityFeed( time.buffer, message.buffer );  
                 }  
               }  
             }  
           }  
         }  
       }  
     }  
   }  


 void SceneBurgersManager::updateActivityFeed(const char *time, const char *message)  
 {  
   CCText combinedMessage = "<";  
   combinedMessage += time;  
   combinedMessage += "> ";  
   combinedMessage += message;  
     
   CCTile3DButton *tile = NULL;  
   if( activityFeedTiles.length < 10 )  
   {  
     tile = new CCTile3DButton( this );  
     tile->setupText( " ", camera->targetHeight * 0.025f, true, false );  
     tile->drawOrder = 205;  
     tile->setTextColour( 1.0f );  
     tile->setTileScale( 1.0f );  
     activityFeedTiles.add( tile );  
   }  
   else  
   {  
     for( int i=0; i<activityFeedTiles.length-1; ++i )  
     {  
       CCTile3DButton *current = activityFeedTiles.list[i];  
       CCTile3DButton *next = activityFeedTiles.list[i+1];  
       current->setText( next->getTextModel()->getText().buffer, true );  
     }  
     tile = activityFeedTiles.last();  
   }  
     
   tile->setText( combinedMessage.buffer, true );  
     
   if( !( gameState >= GameState_CharacterSelectScreen && gameState <= GameState_ReadyToPlay ) )  
   {  
     tile->setTextAlpha( 0.0f, false );  
   }  
     
   // refresh the scene tile positioning and range  
   beginOrientationUpdate();  
 }