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:


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!