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

3 comentarios: