jueves, 15 de mayo de 2014

iOS Localization of Strings in C++

Hi all!

I've been playing around with localization lately and I want to share what I learned.

First of all, I've tried to be very specific in the title. This post is about externalizing strings to external files instead of hard-coding them. This means that I won't deal here with images and other types of files localization. Neither will I discuss here internationalization, which is a broader topic.

Picture from Freemake.com (http://www.freemake.com/blog/interview-with-founder-of-crowdin-net/)

Let's suppose that we're creating a super fantastic game and we want, at the beginning, to show a welcome message to the players: "Welcome!"

In a non-localized app, you'd do something as simple as the following (in C++):

std::cout << "Welcome!; (Note: we'd actually embed this string in a label or other high-level structure that presents the information on the device screen rather than on the IDE console).

Our goal is that the message is shown in the language that players have configured in their devices. How do we achieve this? The overall idea is the following: we create a special kind of file (with .strings extension) and we localize it, meaning that Xcode will take care of creating different directories for each language. Then, in our code, instead of hard-coding strings like in the previous example, we make references to these strings in the .strings file. Finally, when the app is launched in the device, the app will be able to pick the correct language depending on the device settings.

The process can be divided in two steps, the first one focused on configuration and the second one on programming.

1. Configuring the environment

In Xcode (note that I'm using Xcode 5.1), choose: 

1) File -> New -> File...
2) In the iOS section, choose Resource and Strings File
3) Name the new file as Localizable.strings (this is a default name; you can change it, but then each time you want to access the file, you have to provide the name).
4) Save the file in some of your app directories.
5) Select the file and in File Inspector, click in Localize. Choose the languages that you want to support. You can add new languages at any time.
6) Even if you haven't seen it, Xcode has created as many folders as the number of languages. These folders follow the same name convention: en.lproj (for English localized strings), es.lproj (for Spanish localized strings), etc. Each language has a unique code. You can check all the codes here.
7) I advise you to drag these folders to Xcode, uncheck the option "Copy items into destination group's folder (if needed)" and to check "Create folder references for any added folders". This way, your file system is synchronized with what you see in Xcode and any changes are reflected in one another.

Now, you can fill out these files according to the following format:
"key" = "value"; 

Following the welcome example, in the Localizable.strings under the en.lproj folder, we would write:
"WelcomeMessage" = "Welcome!";

Whereas in the Localizable.strings under the es.lproj folder, we would write:
"WelcomeMessage" = "¡Bienvenido!";

And so on with the rest of languages that we want to support. Don't forget the ";" at the end.

2. Programming

Now we're ready to make the appropriate changes to the code. In Objective-C, this is pretty straightforward as the Cocoa framework offers localization methods. In particular, we're interested in the following one:

NSLocalizedString("key", nil)

This method returns a NSString* with the value associated to the NSString passed as first argument. Therefore, if we called the method as NSLocalizedString("WelcomeMessage", nil), it would return "Welcome!" in English-configured devices, and "¡Bienvenido!" in Spanish ones. 

The problem is that given that we're assuming that we're using C++, we cannot directly access this method. For example, in my case I'm working with Cocos2d-x, which is a C++ framework. What can we do to access this Cocoa functionality from C++? 

Well, given that Objective-C and C++ have common roots, it's not difficult to pave a bridge between them. Therefore, I created a C++ wrapper over the aforementioned Objective-C method. The code of the header and implementation files are listed next:

LocalizationManager.h

#ifndef __Limball__LocalizationManager__
#define __Limball__LocalizationManager__

#include <iostream>
#include <string>
class LocalizationManager
{
public:
    static LocalizationManager* getInstance();
    std::string getLocalizedString(const std::string& key);
    
private:
    LocalizationManager();
    ~LocalizationManager();
    static LocalizationManager* lm;
    
};

#endif 

LocalizationManager.mm

#include "LocalizationManager.h"

LocalizationManager* LocalizationManager::lm = nullptr;
LocalizationManager* LocalizationManager::getInstance()
{
   if (!lm)
   {
       lm = new LocalizationManager();
   }
    return lm;
}

LocalizationManager::LocalizationManager()
{
}
LocalizationManager ::~LocalizationManager()
{
    if (lm)
    {
        delete lm;
    }
}

std::string LocalizationManager::getLocalizedString(const std::string& key)
{
    
    NSString *str = [NSString stringWithUTF8String: key.c_str()];
    std::string s([NSLocalizedString(str, nil) UTF8String]);
    return s;
    
}

What I do in the getLocalizedString method is basically transforming from NSString to std::string (using an intermediate C-style string), and viceversa. Note that the implementation file has no .cpp extension, but .mm extension. This is important as this file must be able to understand both C++ and Objective-C. You must change the type of this file to Objective-C++ Source in the File Inspector.

Now, in any class that needs to print a message, I only need to include the LocalizationManager.h, and I can use it as follows:

std::cout << LocalizationManager::getInstance() -> getLocalizedString("WelcomeMessage");

And that's all! Hope you find it useful.
FM

miércoles, 14 de mayo de 2014

Enabling iPhone non-retina in Xcode 5

Hi!

I'm currently working on another game (about which I will tell you more in upcoming posts), and I noticed that there was no iPhone non-retina simulator available for testing in Xcode 5.1. After researching a bit, I found out what I had to do.

1) Click on the simulator area, and More Simulators:



2) In Components, click on iOS 6.1 Simulator:



3) Then, once installation is finished, you have to ensure that your deployment target is 6.1 or below, as higher iOS versions are only supported by retina devices. For this, click on your project and in the General tab, choose the appropriate deployment target:


And that's all. After these steps, you'll be able to choose the iPhone non-retina running iOS 6.1. 



See you!
FM

martes, 13 de mayo de 2014

Multi-resolution Support in iOS with Cocos2d-x v3

EDIT: I updated this post to include new iPhone models, and to target Android devices. You can check it here.

Hi all!

Cocos2d-x is a branch of Cocos2d, one of the most used frameworks for developing iOS games, and the one I myself used to develop Chubby Buddy. Cocos2d-x was released to target cross-platform development, including iOS, Android and Windows Phone. It's powerful and widely used, but recently version 3 came out and brought some drastic changes with respect to versions 2.x.x.

Cocos2d-x offers a C++ front-end (also a Lua, a Javascript and HTML5 ones, but we'll let those aside in this post) as opposed to the Objective-C front-end provided by its cousin Cocos2d. 

In addition to the language, one of the first changes that a developer may find is the scheme to support multi-resolution support. In Cocos2d, you simply used a scheme based on suffixing assets according to their resolution. Therefore, if you had a bitmap called Player.png and you wanted to support all resolutions in iOS, you had to create four versions of it and name them like this:

Player.png (iPhone low resolution)
Player-hd.png (iPhone retina)
Player-ipad.png (iPad low resolution)
Player-ipadhd.png (iPad retina)

During application launch, you could specify different suffixes, but the above were the default choices.

This scheme is no longer used by Cocos2d-x. Instead, you must specify different directories where you can find the assets for each resolution. This is done in the applicationDidFinishLaunching() method in AppDelegate.cpp file, which is automatically called by the iOS framework during initialization. The resulting code is shown next:

bool AppDelegate::applicationDidFinishLaunching() {
    // initialize director
    auto director = Director::getInstance();
    auto glview = director->getOpenGLView();
    if(!glview) {
        glview = GLView::create("My Game");
        director->setOpenGLView(glview);
    }
    

    // turn off display FPS
    director->setDisplayStats(false);

    // set FPS. the default value is 1.0/60 if you don't call this
    director->setAnimationInterval(1.0 / 60);

    auto screenSize = glview->getFrameSize();
    
    auto fileUtils = FileUtils::getInstance();
    std::vector<std::string> searchPaths;
    
    if (screenSize.width == 2048 || screenSize.height == 2048)
    {
        glview -> setDesignResolutionSize(1536, 2048, ResolutionPolicy::NO_BORDER);
        searchPaths.push_back("ipadhd");
        searchPaths.push_back("ipadsd");
        searchPaths.push_back("iphone5");
        searchPaths.push_back("iphonehd");
        searchPaths.push_back("iphonesd");
    }
    else if (screenSize.width == 1024 || screenSize.height == 1024)
    {
        glview -> setDesignResolutionSize(768, 1024, 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)
    {
        glview -> setDesignResolutionSize(640, 1136, ResolutionPolicy::NO_BORDER);
        searchPaths.push_back("iphone5");
        searchPaths.push_back("iphonehd");
        searchPaths.push_back("iphonesd");
        
    }
    else if (screenSize.width == 960 || screenSize.height == 960)
    {
        glview -> setDesignResolutionSize(640, 960, ResolutionPolicy::NO_BORDER);
        searchPaths.push_back("iphonehd");
        searchPaths.push_back("iphonesd");
    }
    else
    {
        searchPaths.push_back("iphonesd");
        glview -> setDesignResolutionSize(320, 480, ResolutionPolicy::NO_BORDER);
    }
    
    fileUtils->setSearchPaths(searchPaths);
    
    //Load your first scene
    auto scene = GameManager::createScene();
    director->runWithScene(scene);

    return true;
}

If we look into the setSearchPaths method, we see that the search path is suffixed to a base search path that is platform-dependent. In the case of Xcode, the base search path is the Resources folder. Therefore, according to previous code, we would need the following directory structure:

Resources/
*ipadhd/  (assets for iPad retina)
*ipadsd/  (assets for iPad non-retina)
*iphone5/ (assets for iPhone5)
*iphonehd/ (assets for iPhone retina)
*iphonesd/ (assets for iPhone)

Note in the code that for each resolution, we also include directories for lower resolutions. We simply want to ensure that if an asset is not found for the targeted resolution, at least we try to find the next one with the best resolution. Also, note that I want to present the game in portrait mode, but it would be better to use screenSize.width and screenSize.height as parameters of setDesignResolutionSize(), just to ensure that everything will work in landscape mode as well. 

Once you have the directory structure, you can drag all the folders in Xcode. In this step, Xcode will ask how you want to deal with these folders. According to some problems found by other people, I suggest you to uncheck "Copy items into destination group's folder (if needed)" and to check "Create folder references for any added folders". You can read more information on this options here

That's all, tell me if you have any problems and we'll try to solve it.
FM

miércoles, 7 de mayo de 2014

Chubby Buddy: basic mechanics

Hi all!

As I mentioned in my last post, Chubby Buddy is finally a reality that you can find in the App Store. Also, if you're unsure about buying the game (it costs 0,89€ or 0,99$), you can also try the free version, which includes fewer and not so challenging levels, but which can give you an overall idea about the basic game mechanics. 

In this post I will discuss these basic mechanics that you first encounter when playing the free version of the game. I'll lay aside more advanced mechanics that are included in the paid version, and which I'll discuss in future posts. 

Figure 1 shows a sample level of the free version of the game. In the first levels there are only three types of elements in the game: the chubby kid (i.e. our chubby buddy), the cakes, and the fruit. The goal is to place the kid at the same position where the fruit is in at most the number of movements indicated in the top-right corner of the screen. You can move any of the elements, except for the fruit, which will remain in the same place all along the level. 


Figure 1. A sample level

The following rules apply to all the elements except for the fruit. So in this case, when I refer to 'element', I actually mean either the 'kid' or a 'cake':

(1) You can only move the element one position to the right or one position to the left.
(2) If there is one element on your right, and you move to the right, you will end up above the element; likewise, if there is one element on your left and you move to the left, you will end up above the element. 
(3) Probably the most important rule to remember (specially in the most advanced levels of the paid version): you CANNOT move if there is an element on top of you. 
(4) If there is more than one element on your right (e.g. two cakes, one on top of the other), you cannot move to the right; similarly, i there is more than one element on your left, you cannot move to the left. 

Each movement will decrease the movement counter (in the top-right corner of the screen) by one. 

Introducing the grandma

Figure 2 shows the last element that appears in the free version of the game: the grandma. Grandma is  subject to the aforementioned movement rules. The only difference is this: when the kid is right next to the grandma (i.e. when the kid is on the right of, left of, on top of, or under the grandma), the movement counter decreases by 3 for each movement.


Figure 2. Be aware of the grandmas!

This is it for now! If you have an iPhone or iPad, give it a try!

See you buddies!
FM