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.

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.