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!

martes, 17 de marzo de 2015

Localizing Android Games in Cocos2d-x

Hi all!

I'm polishing the last details of my new game to come, and I lastly finished localizing the game in the Android platform. You can also check this post where I explained the steps to localize a game in iOS. 

I'm implementing my game in Cocos2d-x so I had two choices to implement localization:
- Reading the current language of the device from Cocos2d-x and provide a custom LocalizedString C++ class in order to translate each string to the corresponding language.
- Using the built-in localization mechanism in Android through the Java Native Interface (JNI). 

Since I've been learning recently JNI and I wanted to experiment more with this technology, I opted for the latter.  Here you can find tips to follow the first path. 

Localization in Android via Resources

Localization in Android is typically done by means of resources, as explained here. In my case, I only want to localize strings, so I'll let images and other types of resources aside. 

In the /res directory of your project, you find a sub-directory called /values. In turn, this directory contains an XML file called strings.xml. This is the place where all the strings to be localized are located. For example, an excerpt of my strings.xml is shown next:

 <resources>  
   ...
    <string name ="Accuracy">Accuracy</string>  
    <string name = "Art">Art</string>  
    <string name = "Programming">Programming</string>  
    <string name = "Music">Music</string>  
   ...
 </resources>  

We are specifying that the default value of the string "Accuracy" is Accuracy, and so on with the others. Say that we want to localize in Spanish. In that case, we will create a new sub-directory under /res with the name values-es, and we will create a strings.xml file with the following contents:

 <resources>  
   ...
    <string name ="Accuracy">Precisión</string>  
    <string name = "Art">Arte</string>  
    <string name = "Programming">Programación</string>  
    <string name = "Music">Música</string>  
   ...
 </resources>  

Upon the launch of the app, Android will find out the locale (language configuration) of the device and will determine whether to use the Spanish localization or the default localization. The key here is the suffix that we append to values directory. "-es" is the international suffix for Spain, whereas "-de" is the international suffix for Germany, for example. This is how Android knows in which strings.xml to look up. You can check the international suffices for languages and countries here and here.

Once we have our resources ready, we can access them programmatically from an Activity as follows:

String localizedString = getString( R.string.Accuracy );

The above statement would provide the variable localizedString with the value "Accuracy" in a device with English locale, and with the value "Precisión" in a device with a Spanish locale. R.string.Accuracy is actually an automatic identification number (integer) that Android generates for you in the auto-generated R.java file. The important thing here is that if you want to access any resource, you need to retrieve first its identification number.

Localizing in Cocos2d-x via JNI

In order to localize in Cocos2d-x, we need to communicate from the game logic (written in C++) to the main activity, written in Java. First, I added a new static method to the main activity (AppActivity.java):

 public static String getLocalizedString( byte[] b )  
 {  
     String str = "";  
     try {  
         str = new String( b, "UTF-8" );  
     } catch (UnsupportedEncodingException e) {  
         e.printStackTrace();  
     }  
     int id = _appActivity.getResources().getIdentifier( str, "string", _appActivity.getPackageName() );  
     String res;  
     if ( id == 0 )  
     {  
         res = str;  
     }  
     else   
     {  
         res = _appActivity.getString( id );  
     }  
     return res;  
 }  

Note that the method receives a byte array, which is converted to a UTF-8 string (since this is what I'll be sending from C++). This string is then used to retrieve the identification number of the resource (getIdentifier() method), and this identification number is in turn used to retrieve the actual localized string (getString() method). In case that a wrong identification number is found (id == 0), I return the original string. Also note that all the method calls concerning the activity are accessed through _appActivity, which is a static variable that holds a reference to the main activity. We need to to do this because otherwise we would lose the reference of the activity once the onCreate() method finishes.

Now that we have this method, we only need to make the corresponding call from the game logic when it is required. For this, I made a simple utility C++ class that manages the localization in the C++ side:

LocalizationManager.h

 #ifndef __Limball__LocalizationManager__  
 #define __Limball__LocalizationManager__  
 #include <string>  
 class LocalizationManager  
 {  
 public:  
   static LocalizationManager* GetInstance();  
   static void DestroyInstance();  
   std::string GetLocalizedString( const std::string& key );  
 private:  
   LocalizationManager();  
   ~LocalizationManager();  
   LocalizationManager( const LocalizationManager & ) = delete;  
   LocalizationManager& operator=( const LocalizationManager & ) = delete;  
   static LocalizationManager* lm;  
 };  
 #endif   

LocalizationManager.cpp

 #include "LocalizationManager.h"  
 #include "cocos2d.h"  
 #include "platform/android/jni/JniHelper.h"  
 #include <jni.h>  
 LocalizationManager* LocalizationManager::lm = nullptr;  
 LocalizationManager* LocalizationManager::GetInstance()  
 {  
   if (!lm)  
   {  
     lm = new LocalizationManager();  
   }  
   return lm;  
 }  
 void LocalizationManager::DestroyInstance()  
 {  
   delete lm;  
 }  
 LocalizationManager::LocalizationManager() {}  
 LocalizationManager ::~LocalizationManager()  
 {  
   lm = nullptr;  
 }  
 std::string LocalizationManager::GetLocalizedString( const std::string& key )  
 {  
     cocos2d::JniMethodInfo t;  
     jstring res;  
     if (cocos2d::JniHelper::getStaticMethodInfo(t, "org/cocos2dx/cpp/AppActivity", "getLocalizedString", "([B)Ljava/lang/String;"))  
     {  
         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 );  
     }  
     return cocos2d::JniHelper::jstring2string( res );  
 }  

As an utility class, it is implemented following the Singleton pattern. The most interesting method is the one that actually calls the Java method defined earlier: GetLocalizedString(). First, by means of the JniHelper utility class provided by Cocos2d-x, we retrieve information about the static method that we defined in the AppActivity.java file. Then, we allocate memory for a byte array and we fill this byte array with the contents of the string passed as an argument. Next we call the static method and receive the result of type jstring. After releasing the memory allocated deleting the local reference, we convert the jstring to std::string and return the result of this conversion.

At this point, some of you may wonder: why do you use a byte array to pass a string to Java? Isn't there any other better way? Well, actually, it exists the function NewStringUTF( const char * ), which receives a C (null-terminated) string and returns a jstring that can be passed to Java directly as a regular Java String. However, as explained here, there is a bug from Android 4.0 and above that may cause app crashes, and this is why a byte array is the most recommended way to deal with handing strings to Java.

Now, the client code can use it as in the following example:

 std::stringstream ss1;  
 ss1 << accuracy;  
 std::string total( LocalizationManager::GetInstance() -> GetLocalizedString("Accuracy") + ": +" + ss1.str());  
 accuracyLabel = Label::createWithTTF(total, "fonts/GILSANUB.TTF", ConfigManager::GetInstance() -> GetFontSizeForScoreScreen() );  
 accuracyLabel -> setPosition(Vec2( accuracyLabel -> getContentSize().width / 2, visibleSize.height/ 3 ));  
 this -> addChild(accuracyLabel);  

Hope you found this post useful and enjoyable. In the next post, I expect to show a promotional video for the game I'm working on.

LocalizationManager::GetInstance() -> GetLocalizedString("See you!!")

EDIT: After researching a bit more, I noticed that I made a mistake in the LocalizationManager::GetLocalizedString() method. In particular, when you invoke NewByteArray(), you're creating a local reference. In a native method (a method that has been called from a Java environment), this local reference is automatically deleted when the the C++ method returns. However, given that we're not in a native method, we have to remove the reference manually by calling DeleteLocalRef(). Also, before I called ReleaseByteArray(), but this is only required when you call GetByteArray(). I changed the code accordingly. You can find references here and here.