Monday, 30 July 2012

Where in the world is your opponent?

Today I added in a feature to show you the country flag of where in the world your opponent is in the Phone Wars multiplayer shooter.

(Note: I'm not sure, but I think that hamburger is from Indonesia.)

I haven't heard feedback on this feature yet as it's just been released. But to me, it was so COOL and motivating, shooting the hell out of an Indonesian Hamburger, an American Android and a Chinese Android today.

I added in the flag over to the leaderboards too to add a little nationalism to the scores.

To get this implemented, involved a little C++, a little PHP and a little JavaScript.

First, to get the flag data, I went to http://www.geonames.org/countries/ and fished out the flags using a php script to download the image and rename it to the country name.
 if( isset( $_GET['generateflags'] ) )  
   {  
     $countryData;  
     url = "http://www.geonames.org/countries/";  
     OpenURL( $url, $countryData );   
     $countryData = SplitBetween( '<tr><th>', '</table>', $countryData );  
   
     $countryData = explode( '<tr', $countryData );  
     $countryDataLength = sizeof( $countryData );  
     for( $i=1; $i<$countryDataLength; ++$i )  
     {  
       $code = strtolower( SplitBetween( 'name="', '"', $countryData[$i] ) );  
       $country = strtolower( SplitBetween( '.html">', '</a>', $countryData[$i] ) );  
       $country = str_replace( " ", "", $country );  
   
       if( strlen( $country ) > 2 )  
       {  
         $fileGIF = "$currentDirectory/../geoips/flags/$country.gif";  
         $filePNG = "$currentDirectory/../geoips/flags/$country.png";  
         $flagURL = "http://www.geonames.org/flags/x/$code.gif";  
         $flagData;  
         if( OpenURL( $flagURL, $flagData ) )  
         {  
           SaveFile( $fileGIF, $flagData );  
   
           $imageData = imagecreatefromgif( $fileGIF );  
           $width = imagesx( $imageData );  
           $height = imagesy( $imageData );  
   
           // Calculate new size  
           $newWidth = 256;  
           $newHeight = floor( $height * ( $newWidth / $width ) );  
           $newImage = imagecreatetruecolor( $newWidth, $newHeight );  
   
           // copy and resize old image into new image  
           imagecopyresized( $newImage, $imageData, 0, 0, 0, 0, $newWidth, $newHeight, $width, $height );  
   
           // Save out new image  
           imagepng( $newImage, $filePNG );  
   
           // Delete old image  
           unlink( $fileGIF );  
         }  
         echo "$code $country <p>";  
       }  
     }  
   }  

So now we have our flags, we do a bit more php to geo locate our country from an ip address. That just required the reuse of our map code that we implemented previously which used IPInfoDB, who provide a free geoIP look up.

Next was to go over to the world of NodeJS and SocketIO, and have each connection, run a loop up of their location info.
 function getGeoIPData(socket)  
 {  
      var ip = socket.handshake.address;  
      var options = { host: 'api.ipinfodb.com', port: 80 };  
      options.path = '/v3/ip-city/?ip=' + ip.address;  
      var request = http.get( options );  
      request.on( 'response', function (result)  
      {  
           result.setEncoding( 'utf8' );  
   
           var data = "";  
           result.on( 'data', function(chunk)   
           {  
                data += chunk;  
           });  
           result.on( 'end', function()  
           {  
                if( data.length > 2 )  
                {  
                     socket.geoIPData = JSON.parse( data );  
                     console.log( "getGeoIPData", socket.sessionID, socket.geoIPData );  
                }  
           });  
      });  
      request.on( 'error', function (error)   
      {  
            console.log( "getGeoIPData ERROR:", socket.sessionID, error.message );  
      });  
 }  

Finally over in the matchmaking code, just pass over the countryName to the client. The C++ client will pick up the country name using Jansson, strip out any unwanted characters, spaces, full stops, see if it matches the names of one of the flags we have stored. If so, bam.. display it.
 if( geoLocationData.length > 0 )  
   {  
     json_error_t error;  
     json_t *root = json_loads( geoLocationData.buffer, 0, &error );  
     if( root )  
     {  
       CCText statusCode;  
       json_object_string( statusCode, root, "statusCode" );  
       if( CCText::Equals( statusCode.buffer, "OK" ) )  
       {  
         json_object_string( geoLocationCountry, root, "countryName" );  
         if( geoLocationCountry.length > 1 )  
         {  
           geoLocationCountry.toLowercase();  
           geoLocationCountry.replaceChars( " ", "" );  
           geoLocationCountry.replaceChars( ".", "" );  
           geoLocationCountry.replaceChars( "-", "" );  
           
           CCText file = "Resources/Common/flags/";  
           file += geoLocationCountry.buffer;  
           file += ".png";  
           const bool exists = CCFileManager::DoesFileExist( file.buffer, Resource_Packaged );  
           if( exists )  
           {  
             ScenePlayManager::scene->createFlag( file.buffer );  
           }  
         }  
       }  
       json_decref( root );  
     }  
   }  

Easy as that.

Side note, it's seriously awesome being able to code in several languages, the full solution also involved a bit of Java to support the Android client. If you've ever been shy of learning another language, just jump right in, you don't need to be an expert, but knowing a little + GoogleFu/StackOverflow sure does help.

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?

Sunday, 22 July 2012

Food Fighters Worldwide Player Activity Map

Was digging through the player activity stats for Food Fighters just now to see where in the world our players are located. Threw together a mashup of our players on a world map. Pretty cool seeing the global footprint of our player base.



If you're interested in making your own mashup, the OpenStreetMap apis are a breeze to use, especially with OpenLayers, and IPInfoDB gives out free reverse ip lookups.

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();  
 }  

Sunday, 15 July 2012

GCM Push Notifications for Android


Push notifications are officially awesome for real time multiplayer gaming. If your server has no other players, you can't really have a game. With push notifications however, you can register your interest in playing, and whenever someone logs in, you can immediately be told and be able to log right in and play against them.

Some of the quirks of getting push notifications on Android were.
  • Including the gcm.jar file compiled but always gave me a class not found run time error.
    • To solve, I included the source files provided in gcm-client instead.

  • GCMIntentService must be put in the same class package as your application package. (I tend to re-use a lot of my code between projects, so I have a generic class package name with different application package names).


Apart from that, everything seemed to pretty much work well.

Some good material to get your head around is the Google I/O 2012 talk and the documentation.

If you're doing push messaging with NodeJS and have any problems, let me know and I'll try to help. If you hate server side development, you can always give Parse a try.

Wednesday, 11 July 2012

Food Fighters (the game.. not the band)

Hi guys, whilst the Androids vs iPhones game is being reviewed by the kind folks at Apple, we decided to get high or hungry over a fast food shooting game.

Using the same mechanics as the iPhones vs Androids game and the same ability to play against different platforms (including PC/Mac/Linux),
you can have a sneak peak on how the Androids game will play out with FOOD FIGHTERS! (the game.. not the band).


Inline images 1

The Android version has been published (so should appear on the store momentarily).
I'll publish a Windows version later today (so you can play Windows vs Android).
If you'd like a version for another platform, let me know and I'll send you a beta.


If you'd like to see any other crazy/fun/stupid concepts (Justin Bieber vs King Kong anyone?), let me know.


Thanks for the support buddies :)

Friday, 6 July 2012

MiniChe - The Importance of an Art Pipeline

Two months ago, myself and two buddies entered a game hackathon in which we worked on a concept game idea we've been bouncing around called MiniChe.

In case you don't know I'm Egyptian and we've been going through a revolutionary period in our countries history, so we figured, wouldn't it be cool, if you played the role of Che Gueverra and went around the world causing revolutions?

We'll we thought it would be awesome and thanks to the help of the wonderfully talented DinoAhmed and Rez we managed to get together a prototype going.

Initially we planned on continuing the momentum from the hackathon and releasing the game within 2 weeks. However, interestingly enough, that didn't happen. Working remotely is a big challenge when it comes to coordination and dedication. When you're hacking something together with your buddies, it's pretty easy to point and say, wrong, right, do it this way. When you're working remotely off an undefined pipeline where a perceived understanding is assumed, problems occur.

Here's probably the top three biggest time vacuums we ran into.

Anchoring
A lot of art assets (sprites and 3d models) seem to have varying origin points.

To combat this issue, just manually edit the sprites yourself (it's a headache, it's gruntwork, but sometimes it's the quickest thing to do to get the job done). For 3d models, calculate the width, depth and height of an object and re-center them in code.


Poly Counts
Sometimes for some reasons, the models provided go over the agreed poly counts, which leads to inflated loading and rendering times.



To combat this issue, we tend to look at the asset, if it's worth keeping, we have to re-iterate and reduce the poly count. If it doesn't really look good and the process is taking too long. Just drop it.


Naming Conventions
Sometimes you tend to get filenames that go against the agreed naming convention.

To combat this issue, if it's just a one off hit, be a man, rename it yourself, it's a losing war trying to teach people basic skills.


Well, after 2 months of hiatus, I'm happy to announce that we're finally hitting ALPHA on the project with all the required* art assets complete.

*The best part of producing your own game is that it's a lot easier to scale down the requirements.