sábado, 25 de abril de 2015

Technical Take-aways from Limball

Hi!

In this post I want to summarize some of the technical issues I had to face while developing Limball. 

The first thing to highlight is that it has been my first experience, at least in a complete game, with Cocos2d-x, the framework that I have used for the implementation. In my first game, Chubby Buddy, I had used Cocos2d (now renamed to Cocos2d-SpriteBuilder), which uses Objective-C and is targeted exclusively at iOS developers. Cocos2d-x is written in C++ and targets both iOS and Android developers. I must say that the learning curve has been very smooth. The API is practically the same, so if you know how to call a certain function from Objective-C, you know almost intuitively how to call it from C++. Of course, a previous background on C++ is fundamental in order to get the most out of it as fast as posible, but you can use also other languages (e.g. Lua).



Developing for multiple platforms is made really easy thanks to Cocos2d-x, which allows keeping the same C++ codebase. Nonetheless, at some points I needed to do something different in the two platforms. For example, when integrating with Game Center, the social platform for iOS gamers, I noticed that the achievements and leaderboard sections were well integrated under a common interface. However, Google Play Game Services, the Android counterpart, does not integrate the two services under the same interface. This means that two different buttons are required in Android (one for the leaderboards, another one for the achievements), while in iOS does just fine with one button. In order to avoid lots of code duplication, I resorted to preprocessor macros. Cocos2d-x defines the macro CC_TARGET_PLATFORM, which may be assigned a value according to the platform where the code is to be executed. As a consequence, there are several parts in the code with the following pattern:

 #if CC_TARGET_PLATFORM == CC_PLATFORM_IOS  
  //do iOS-related stuff  
 #else  
  //do Android-related stuff  
 #endif  

Something else that I learnt is how to use the social platforms for gamers on iOS and Android. The integration with the former was smoother, also due to the easier integration between C++ and Objective-C. An example on how this C++/Objective-C integration can be painlessly achieved is discussed in an earlier post on localization.

As an example, consider the following code that unlocks an achievement on both platforms:

 #if CC_TARGET_PLATFORM == CC_PLATFORM_IOS  
       GKHWrapperCpp gkh;  
       gkh.reportAchievement( "Triple_Chain", 100.0, false );  
 #elif CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID  
       GooglePlayHelper::UnlockAchievement( COMBO_KIDDIE );  
 #endif  

GKHWrapperCpp is a class that belongs to a open source library that you can find here and that simplifies the management of Game Center related stuff. GooglePlayHelper is a utility class that I made in order to manage interactions to Android-specific features through the Java Native Interface (JNI).

JNI is not difficult to use, provided you have the previous background on Java and C++, but it is easy to make some small mistakes that are almost impossible to debug. In my case, I was experiencing a game crash only on Nexus 5 running Android 5.0.1. I even tested on the same device running a lower version of Android and it worked perfectly. For some time, I had no clue on what was happening and I ended up blaming the OpenGL implementation on that device for that version of Android. In the end though, it turned out that I was making a memory management mistake: I wasn't removing a local reference that I had created:

 jbyteArray bArray = t.env -> NewByteArray( key.length() );  
 jbyte bytes[50];  
 for( int i = 0; i < key.length(); ++i )  
 {  
    bytes[i] = key[i];  
 }  
 t.env -> SetByteArrayRegion( bArray, 0, key.length(), bytes );  
 res = (jstring) t.env -> CallStaticObjectMethod( t.classID, t.methodID, bArray );  
 t.env -> DeleteLocalRef(t.classID);  
 t.env -> DeleteLocalRef( bArray );  

Adding the last line of the previous snippet did the trick, just one day before the intended release date..

Another example of system-specific feature is in-app purchases. In Limball, you can remove the ads banners and interstitials (full-screen ads) by buying a non-ads product from inside the app. Both iOS and Android offer a simple way to tackle this, so that was not a problem. In the case of Android, I used the In-app Billing v3 workflow, which basically comes down to the following snippet of code:

 buyIntentBundle = _appActivity.mService.  
           getBuyIntent( 3, _appActivity.getPackageName(), productId, "inapp", "noads" );  
           //If everything is fine, then proceed with the transaction  
           if ( buyIntentBundle.getInt( "RESPONSE_CODE" ) == 0 )  
           {  
             _appActivity.pauseGame();  
             PendingIntent pendingIntent = buyIntentBundle.getParcelable( "BUY_INTENT" );  
             _appActivity.startIntentSenderForResult( pendingIntent.getIntentSender(),   
                                  REQUEST_INAPP_CODE,  
                                  new Intent(),   
                                  Integer.valueOf( 0 ),   
                                  Integer.valueOf( 0 ),  
                                  Integer.valueOf( 0 ) );  
           }  

On iOS, the strategy (as usual) is to create a delegate that will manage the purchase with the following calls:

 SKMutablePayment *payment = [SKMutablePayment paymentWithProduct: productToBuy ];  
 [[SKPaymentQueue defaultQueue] addPayment: payment];  

It first create the payment with product id, and it introduces it in the queue. Then, the payment is processed by the payment queue delegate, typically as part of the AppController. This delegate is in charge of looking at the state of the transaction and to deliver the functionality once the transaction is in the SKPaymentTransactionStatePurchased state, as depicted in the following code:

 - (void) paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions  
 {  
   for ( SKPaymentTransaction* transaction in transactions )  
   {  
     switch ( transaction.transactionState ) {  
       case SKPaymentTransactionStatePurchasing:  
         ConfigManager::GetInstance() -> PauseGame();  
         break;  
       case SKPaymentTransactionStatePurchased:  
         ConfigManager::GetInstance() -> EnableNoAds();  
         ConfigManager::GetInstance() -> ResumeGame();  
         [[SKPaymentQueue defaultQueue] finishTransaction: transaction];  
         break;  
      //...  

For the inclusion of advertisements, I have used the iAd network on iOS and Google's Admob. Actually, on iOS the strategy is to prioritize iAd, and only if it is unavailable, fall back to Admob.  Given that iAd does not provide interstitials for iPhones, I used the Admob feature for that purpose. Again, the integration was smooth, because the frameworks provide usable APIs. In this earlier post, I explained how you could integrate a Cocos2d-x project with iAD.

And these are the most important technical issues I have learnt about. Hope you found them useful.
See you!

Tweet: Technical take-aways from making #Limball. Take a look: http://ctt.ec/Nb893+ #gamedev #indiedev

sábado, 18 de abril de 2015

Our new Game: Challenge yourself and others with Limball

Hi!

After some time, I can finally introduce you the new jewel, future resounding success of the AppStore and Google Play. Yes, it is our new game: Limball!

Limball is a casual, free game that will challenge your speed, reflexes and tapping rythm. It is available from today on Android and iOS devices. Without further ado, this is the promotional video of the game, where you can get a grasp of its mechanics:

video

After seing this game and our previous one, Chubby Buddy, you might think: "Wow! These guys are obsessed with food!". But what can we say? It may be true after all :)

By the way, when I say "we", I refer to myself (obviously, right?) and to Manuela Ruiz, who again has been in charge of the fantastic art design of the game. 

If you have an iPhone or iPad, get it here.
If you have an Android device, get it here

In the next post, I will tell you about the development experience, including what I have learnt all along the process.

See you and hope you enjoy it!

Tweet: Hey, check #Limball, a free game for #ios and #android. It's challenging and it's free! Beat your friends' scores! #indiedev #gamedev

lunes, 13 de abril de 2015

On the Subtleties of Implicit Assumptions

Hi!

In this post, I just want to throw a reflection on the difficulties that you may encounter in changing your mindset when you have some implicit assumptions deep in your mind. This post is more about programming than it is about game development but anyway, the former is closely related to the latter.

From time to time, I like to read about Lua, a scripting language that is gaining traction among game developers for its multiple benefits, which include its easy integration with C/C++ and its reduced footprint, which makes it efficient for real-time applications. 

In order to practice, I decided to implement the well-known mergesort algorithm. Just as a short explanation/reminder, mergesort is used to sort a collection of elements (i.e. an array), being the main representative of the divide-and-conquer paradigm. The idea is simple: you split the collection in two halves, you sort each half and you merge it, as you would do with a deck of cards. You repeat this process recursively until you have a straightforward problem (e.g. one card), which constitutes the base case.

Without further ado, this is the algorithm implemented in Lua:

 function merge( a1, a2 )  
   local j = 1  
   local i = 1  
   local k = 1  
   local  b = {}  
   while i <= #a1 and j <= #a2 do  
     if a1[i] < a2[j] then  
       b[k] = a1[i]  
       i = i + 1    
     else  
       b[k] = a2[j]  
      j = j + 1    
     end  
     k = k + 1  
   end  
   if i <= #a1 then  
     for t = i, #a1 do  
       b[k] = a1[t]  
       k = k + 1   
     end  
   else  
     for t = j, #a2 do  
       b[k] = a2[t]  
       k = k + 1  
     end  
   end  
   return b  
 end   
 function mergeSort( a )  
   if #a <= 1 then  
     return a    
   else  
     local b = {}  
     for i = 1, math.floor(#a/2) do  
       b[i] = a[i]  
     end  
     local c = {}  
     for i = math.floor(#a/2) + 1, #a do  
       c[i - math.floor(#a/2)] = a[i]  
     end    
     array1 = mergeSort(b)  
     array2 = mergeSort(c)  
     return merge( array1, array2 )  
   end  
 end  

The thing is that, even when I was pretty sure that the algorithm was well implemented, it was not working as expected. And it took me a while to understand why, because the reason is hidden as an implicit assumption that I was making as a result of being used to programming in other languages, such as C++.

This assumption is that all variables are, by default, local to their scope. However, in Lua, unless you specify otherwise, all variables are global by default. Only if you add the modifier local before the name of the variable, the variable is actually local. Therefore, variables array1 and array2 are global variables, and upon recursion, they do not change their values by the values that correspond to the current stack, breaking the recursion mechanism. This problem is fixed by writing the local keyword before array1 and array2.

Anyway, what I wanted to discuss here is that when we are changing among different technologies/languages, we also need to update our implicit assumptions, which is what makes these changes challenging. Note that I didn't have a problem with the syntax: this kind of problems are pretty easy to detect and fix. My problem was more with the semantics and this is a much harder problem to detect.

So watch out! Your assumptions might be your worst enemies from time to time.

See you.




lunes, 6 de abril de 2015

Multi-resolution support for Android and iOS with Cocos2d-x v3

Hi!

In this post I wrote some time ago, I explained how you could target different resolutions for your iOS game with Cocos2d-x v3. That post didn't include the new resolutions for iPhone 6 and iPhone 6 Plus, and it didn't discuss how to approach the multi-resolution problem for Android devices.

First of all, this is the updated code I use for iOS devices. 

   auto screenSize = glview->getFrameSize();  
   auto fileUtils = FileUtils::getInstance();  
   std::vector<std::string> searchPaths;  
 #if CC_TARGET_PLATFORM == CC_PLATFORM_IOS  
   //Iphone 6 plus  
   if ( screenSize.width == 2208 || screenSize.height == 2208 )  
   {  
     ConfigManager::GetInstance() -> SetDeviceType( iPhone6Plus );  
     glview -> setDesignResolutionSize( screenSize.width, screenSize.height, ResolutionPolicy::NO_BORDER );  
     searchPaths.push_back("iphone6plus");  
     searchPaths.push_back("ipadhd");  
     searchPaths.push_back("iphone6");  
     searchPaths.push_back("ipadsd");  
     searchPaths.push_back("iphone5");  
     searchPaths.push_back("iphonehd");  
     searchPaths.push_back("iphonesd");  
   }  
   //Ipad HD  
   else if ( screenSize.width == 2048 || screenSize.height == 2048 )  
   {  
     ConfigManager::GetInstance() -> SetDeviceType( iPadRetina );  
     glview -> setDesignResolutionSize( screenSize.width, screenSize.height, ResolutionPolicy::NO_BORDER);  
     searchPaths.push_back("ipadhd");  
     searchPaths.push_back("iphone6");  
     searchPaths.push_back("ipadsd");  
     searchPaths.push_back("iphone5");  
     searchPaths.push_back("iphonehd");  
     searchPaths.push_back("iphonesd");  
   }  
   //Iphone 6  
   else if ( screenSize.width == 1334 || screenSize.height == 1334 )  
   {  
     ConfigManager::GetInstance() -> SetDeviceType( iPhone6 );  
     glview -> setDesignResolutionSize( screenSize.width, screenSize.height, ResolutionPolicy::NO_BORDER);  
     searchPaths.push_back("iphone6");  
     searchPaths.push_back("ipadsd");  
     searchPaths.push_back("iphone5");  
     searchPaths.push_back("iphonehd");  
     searchPaths.push_back("iphonesd");  
   }  
   else if (screenSize.width == 1024 || screenSize.height == 1024)  
   {  
     ConfigManager::GetInstance() -> SetDeviceType( iPad );  
     glview -> setDesignResolutionSize( screenSize.width, screenSize.height, ResolutionPolicy::NO_BORDER );  
     searchPaths.push_back("ipadsd");  
     searchPaths.push_back("iphone5");  
     searchPaths.push_back("iphonehd");  
     searchPaths.push_back("iphonesd");  
   }  
   else if (screenSize.width == 1136 || screenSize.height == 1136)  
   {  
     ConfigManager::GetInstance() -> SetDeviceType( iPhone5 );  
     glview -> setDesignResolutionSize( screenSize.width, screenSize.height, ResolutionPolicy::NO_BORDER );  
     searchPaths.push_back("iphone5");  
     searchPaths.push_back("iphonehd");  
     searchPaths.push_back("iphonesd");  
   }  
   else if (screenSize.width == 960 || screenSize.height == 960)  
   {  
     ConfigManager::GetInstance() -> SetDeviceType( iPhoneRetina );  
     glview -> setDesignResolutionSize( screenSize.width, screenSize.height, ResolutionPolicy::NO_BORDER );  
     searchPaths.push_back("iphonehd");  
     searchPaths.push_back("iphonesd");  
   }  
   else  
   {  
     ConfigManager::GetInstance() -> SetDeviceType( iPhone );  
     searchPaths.push_back("iphonesd");  
     glview -> setDesignResolutionSize( screenSize.width, screenSize.height, ResolutionPolicy::NO_BORDER );  
   }  

As you can see, I only added the new resolutions for iPhone 6 and iPhone 6 Plus.

As for Android, I follow a strategy that I'll explain in short. This strategy has worked for me, but it's important that you understand the facts that I considered for following it:
  1. My resources are designed to fit the iOS devices resolutions. 
  2. My game is only played in portrait mode, that is, in vertical orientation.
  3. I use True Type Fonts, and I use different font sizes for each iOS device. If I want to reuse the same font sizes for Android devices (which I certainly do), I need to know which iOS device resolution is the most similar to the Android device resolution, and use that corresponding font size. 
With these facts in mind, I follow these coarse-grained steps:
  1. The design resolution size is the actual resolution size of the Android device. 
  2. I set a content scale factor taking iOS screens resolutions as references (because my resources are designed to fit these resolutions). Given that my game will only be played in portrait mode, the content factor is set in terms of the height dimension.
  3. For determining the font size, I take the ratio between the actual screen width and the screen width of an iOS device. If this ration is above 1.5f, I move to the font size of the next iOS device with higher resolution.
  4. Backgrounds images are scaled to fit the full screen (depending on the device, they can be a bit stretched or compressed).
Here's the code:

 #elif CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID  
   //Iphone 6 plus  
   if (screenSize.width >= 2208 || screenSize.height >= 2208)  
   {  
           director -> setContentScaleFactor( 2208.0f / screenSize.height );  
           ConfigManager::GetInstance() -> SetDeviceType( iPhone6Plus );  
           glview -> setDesignResolutionSize( screenSize.width, screenSize.height, ResolutionPolicy::NO_BORDER );  
           searchPaths.push_back("iphone6plus");  
           searchPaths.push_back("ipadhd");  
           searchPaths.push_back("iphone6");  
           searchPaths.push_back("ipadsd");  
           searchPaths.push_back("iphone5");  
           searchPaths.push_back("iphonehd");  
           searchPaths.push_back("iphonesd");  
      }  
      //Ipad HD  
      else if ( screenSize.width >= 2048 || screenSize.height >= 2048 )  
      {  
           director -> setContentScaleFactor( 2048.0f / screenSize.height );  
           ConfigManager::GetInstance() -> SetDeviceType( iPadRetina );  
           glview -> setDesignResolutionSize( screenSize.width, screenSize.height, ResolutionPolicy::NO_BORDER);  
           searchPaths.push_back("ipadhd");  
           searchPaths.push_back("iphone6");  
           searchPaths.push_back("ipadsd");  
           searchPaths.push_back("iphone5");  
           searchPaths.push_back("iphonehd");  
           searchPaths.push_back("iphonesd");  
      }  
      //Iphone 6  
      else if ( screenSize.width >= 1334 || screenSize.height >= 1334 )  
      {  
        director -> setContentScaleFactor( 1334.0f / screenSize.height );  
        if ( screenSize.width / 750.0 >= 1.5f )  
        {  
             ConfigManager::GetInstance() -> SetDeviceType( iPhone6Plus ); //bigger font  
        }  
        else  
        {  
             ConfigManager::GetInstance() -> SetDeviceType( iPhone6 );  
        }  
           glview -> setDesignResolutionSize( screenSize.width, screenSize.height, ResolutionPolicy::NO_BORDER);  
           searchPaths.push_back("iphone6");  
           searchPaths.push_back("ipadsd");  
           searchPaths.push_back("iphone5");  
           searchPaths.push_back("iphonehd");  
           searchPaths.push_back("iphonesd");  
      }  
      else if (screenSize.width >= 1024 || screenSize.height >= 1024)  
      {  
        director -> setContentScaleFactor( 1024.0f / screenSize.height );  
        if ( screenSize.width / 768.0 >= 1.5f )  
        {  
                  ConfigManager::GetInstance() -> SetDeviceType( iPhone6Plus ); //bigger font  
        }  
        else  
        {  
             ConfigManager::GetInstance() -> SetDeviceType( iPad );  
             }  
           glview -> setDesignResolutionSize( screenSize.width, screenSize.height, ResolutionPolicy::NO_BORDER );  
           searchPaths.push_back("ipadsd");  
           searchPaths.push_back("iphone5");  
           searchPaths.push_back("iphonehd");  
           searchPaths.push_back("iphonesd");  
      }  
      else if (screenSize.width >= 1136 || screenSize.height >= 1136)  
      {  
        director -> setContentScaleFactor( 1136.0f / screenSize.height );  
        if ( screenSize.width / 640.0 >= 1.5f )  
        {  
                ConfigManager::GetInstance() -> SetDeviceType( iPhone6 ); //bigger font  
        }  
        else  
        {  
                ConfigManager::GetInstance() -> SetDeviceType( iPhone5 );  
        }  
           glview -> setDesignResolutionSize( screenSize.width, screenSize.height, ResolutionPolicy::NO_BORDER );  
           searchPaths.push_back("iphone5");  
           searchPaths.push_back("iphonehd");  
           searchPaths.push_back("iphonesd");  
      }  
      else if (screenSize.width >= 960 || screenSize.height >= 960)  
      {  
        director -> setContentScaleFactor( 960.0f / screenSize.height );  
        if ( screenSize.width / 640.0 >= 1.5f )  
        {  
             ConfigManager::GetInstance() -> SetDeviceType( iPhone6 ); //bigger font  
        }  
        else  
        {  
             ConfigManager::GetInstance() -> SetDeviceType( iPhoneRetina );  
        }  
           glview -> setDesignResolutionSize( screenSize.width, screenSize.height, ResolutionPolicy::NO_BORDER );  
           searchPaths.push_back("iphonehd");  
           searchPaths.push_back("iphonesd");  
      }  
      else  
      {  
        director -> setContentScaleFactor( 480.0f / screenSize.height );  
        if ( screenSize.width / 320.0 >= 1.5f )  
        {  
             ConfigManager::GetInstance() -> SetDeviceType( iPhoneRetina ); //bigger font  
        }  
        else  
        {  
             ConfigManager::GetInstance() -> SetDeviceType( iPhone );  
        }  
           searchPaths.push_back("iphonesd");  
           glview -> setDesignResolutionSize( screenSize.width, screenSize.height, ResolutionPolicy::NO_BORDER );  
      }  
 #endif  
   fileUtils->setSearchPaths(searchPaths);  

Finally, as explained in bullet 4, each time I have to show a background image, I scale it to fit the screen, as follows:

   mBackground = Sprite::create("menuBackground.png");  
   mBackground -> setPosition(Point(origin.x + visibleSize.width / 2, origin.y + visibleSize.height / 2));  
 #if CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID  
   mBackground -> setScale( visibleSize.width / mBackground -> getContentSize().width,  
                            visibleSize.height / mBackground -> getContentSize().height );  
 #endif  
   this -> addChild(mBackground);  

Hope you find it useful. The complete guides to understand multi-resolution design is here and here.

See you!