Channels ▼
RSS

Open Source

Building Portable Games in C++


Node Hierarchy

Whille it's beyond the scope of this article to explain all the nuances of tree structures in a game, the general idea is that every element that is displayed on the screen is of type CCNode, and multiple nodes can be grouped together as children of a parent node.

Every CCScene object should have a CCLayer child, and all other nodes in the scene will be added on top of that layer. This is an example of the Model-View-Controller (MVC) design paradigm, where the scene is the controller that is responsible for communicating between the models (game logic) and views (layer and its child nodes).

As mentioned before, CCDirector will present one CCScene object at a time. A scene represents different screens that will appear during the lifetime of your application. The scene navigation for Ready Steady Play is shown in Figure 1.

iPhoneGames
Figure 1: A typical scene navigation chart.

Sprite Sheets

The basic idea of a sprite sheet is that, if you have an animation of 50 images, you don't want to load 50 files from disk (since disk reading/writing are very slow operations). Sprite sheets allow you to load one large texture (that is, image) containing all the frames in the animation, along with a corresponding data file that defines the positions of each frame.

Most of the animations in Ready Steady Play are done using sprite sheets. Each animation is created in Adobe After Effects, exported as a sequence of PNG images at 25 frames-per-second, imported into a tool called TexturePacker, which exports them as corresponding data (.plist) and texture (.pvr.ccz) files.

TexturePacker has predefined settings for cocos2d projects, where it will automatically export the sprite sheets in HDR, HD, and SD sizes to correlate with the folders in your Resources folder. TexturePacker has great documentation, so I won't go into more detail here.

Porting to iOS

From the early stages (about a month into development), I made sure that the game ran on both iOS and Android. Having the two projects running side-by-side meant that the code was being written with cross-platform compatibility in mind. For example, I originally used the Apple arc4random() function for generating random numbers, until I tried to compile for Android. Then, I realized that I needed to use the ANSI drand48() instead.

Certain parts of the program needed to be written specifically for different operating systems. The simplest example is localization. While cocos2d-x will tell you the language the device is using, it doesn't provide any other localization tools (such as number or date formatting). To handle localization, I created three files to define the Localization class:

Localization.h
Localization.cpp
Localization.mm

The .mm file is an Objective-C++ implementation file. One of the most frustrating challenges of writing cross-platform code is cross-language interoperability. Communicating between C++ and Objective-C code is thankfully very easy. The iOS version of Ready Steady Play does not even compile Localization.cpp to define the Localization class, it only needs to use Localization.mm. The following is a modified excerpt from that file:

#include "Localization.h"
#import <Foundation/Foundation.h>

std::string Localization::localizedString(const char* key)
{
    return [[[NSBundle mainBundle] localizedStringForKey:
        [ NSString stringWithCString:key
              encoding:NSUTF8StringEncoding]
              value:@"”
              table:nil] UTF8String];
}

std::string Localization::formattedInteger(long number)
{
    static NSNumberFormatter *formatter;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        formatter = [[NSNumberFormatter alloc] init];
        [formatter setNumberStyle:NSNumberFormatterDecimalStyle];
        [formatter setMaximumFractionDigits:0];
        [formatter setMinimumIntegerDigits:1];
        [formatter setLocale:[NSLocale autoupdatingCurrentLocale]];
    });
    return [[formatter stringFromNumber:
        [NSNumber numberWithLong:number]] UTF8String];
}

It might seem strange to see C++ and Objective-C intertwined like that, but it's all perfectly legal. The one pitfall that might catch you is that the .h file can't mix the two languages. It has to be either a C++ header or an Objective-C header.

Porting to Android

The first major headache I encountered with the Android port was the build script. When you (finally) set up the project in Eclipse and try to build it, the first thing it does is try to run proj.android/build_native.sh. The script uses the Android NDK to build the project, which references the Makefile proj.android/jni/Android.mk. This needs to be customized to reference all the source files that need to be compiled for the Android project. Initially, it has just the template source files, and adding files one-by-one is a massive headache. I opted for the wildcard approach, so I use this:

  LOCAL_SRC_FILES   := $(wildcard $(LOCAL_PATH)/../../Classes/*.cpp)
  LOCAL_SRC_FILES   := $(LOCAL_SRC_FILES:$(LOCAL_PATH)/%=%)

Unfortunately, this option means that all my source files are thrown into a single folder; but since Xcode organizes everything for me, that wasn't a big problem.

The Makefile also needs to know of any external headers being referenced. For example, I use the C++ JSON parser rapidjson in parts of my code, so I needed to add the following line:

is a massive headache. I opted for the wildcard approach, so I use this:

LOCAL_C_INCLUDES += $(LOCAL_PATH)/../../cocos2d-x/extensions/CocoStudio/Json/rapidjson

The second major headache when porting to Android is Java Native Interface (JNI). As mentioned in the iOS section, interoperating between languages is tricky. How are you meant to convert a C string (const char*) to a Java string (java.lang.String)? This is where JNI comes in to play. It allows you to make calls from C++ to Java. But, unfortunately, it doesn't work the other way around, which means you might have to do polling to retrieve values asynchronously.

Generally, I tried to delve as little as possible into JNI. To keep the JNI code simple, I call static methods from one big Java file. It is by no means the recommended way to do things, but the less JNI code you have to write, the better.

The general data types that you'll want to use are fairly straightforward, such as jint, jlong, jboolean, jsize, etc. However, the trickiest data type of all is jstring. Consider the Localization class mentioned earlier:

#include "platform/android/jni/JniHelper.h"
#include "JNIString.h"

USING_NS_CC;
using namespace std;

string Localization::localizedString(const char* key)
{
    JniMethodInfo t;
    string ret("");
    if (JniHelper::getStaticMethodInfo(
                       t,
                       "io/hoverstud/readysteadyplay/ReadySteadyPlay",
                       "localizedStringJNI",
                       "(Ljava/lang/String;)Ljava/lang/String;"))
    {
        jstring jKey = t.env->NewStringUTF(key);
        jstring jstr = 
            (jstring)t.env->CallStaticObjectMethod(t.classID, t.methodID, jKey);
        jstringToString(t.env, jstr, &ret);
        
        t.env->DeleteLocalRef(t.classID);
        t.env->DeleteLocalRef(jKey);
        t.env->DeleteLocalRef(jstr);
    }
    
    return ret;
}

As you can see, it is easy to get a jstring object from a C string or as a return value. However, converting a jstring back into a C string is not so easy. At first, you might be tempted to use JniHelper::jstring2string, which calls the JNI method GetStringUTFChars. You must never use this method. What you really want is something that returns an array of UTF-8 characters, yet for some ungodly reason, GetStringUTFChars returns an array of modified UTF-8 characters, which appear as jumbled letters when displayed in a label.

Instead, I used my own jstringTostring method to call GetStringChars (not GetStringUTFChars), which returns an array of UTF-16 characters that I pass through to an open source UTF-8 converter from Nemanja Trifunovic to convert it to a valid C string.

Porting to Windows Phone 8

Support for Windows Phone 8 was introduced in cocos2d-x v2.2.3, but Ready Steady Play was built using v2.2. Therefore, the first stage of porting was upgrading cocos2d-x. The problem was that I forked cocos2d-x so that we could adapt the code to suit our game, so I couldn't simply swap to the latest git tag. I had to manually merge the code. This proved to be a painfully slow process, since I didn't want to use either git merge or rebase the code and risk losing my changes. For any of you working off v2.2.3 or later, be very thankful.

After working through the project for a few days, I eventually got the game running. Our test device in the office is a Nokia Lumia 520, which is recommended as the lowest-end device for testing. The biggest challenge with this device is the memory limit of 150MB, which is not a lot on today's devices.

Remember how I mentioned that cocos2d-x images have HDR, HD, and SD versions? Forget about it. No room for all that in so little memory. WP8 gets SD (sprite sheets) only. If you are planning on using music and sound with stereo channels, you'll need to put those plans aside. It's mono-only for WP8 (and Android, too, if you want the game to run smoothly).

Once the game was running without expending memory, the next stage was to get the localization working. This meant once again getting into cross-language interoperability. Windows Phone 8 XAML projects use "Managed C++," where you can use C#-esque objects in your C++ code. Let's go back to our good friend Localization.

#include "LocalizationComponent.h"
#include "CCWinRTUtils.h"

static inline std::string winRTStringToString(Platform::String^ str)
{
    std::string ret(cocos2d::CCUnicodeToUtf8(str->Data()));
    // Replace \r\n with \n
    size_t pos;
    while ((pos = ret.find("\r\n")) != std::string::npos)
        ret.replace(pos, 2, "\n");
    return ret;
}

std::string Localization::localizedString(const char* key)
{
    // Convert the key to a wstring
    std::wstring keyString = cocos2d::CCUtf8ToUnicode(key);
    // Convert the key to a Platform::String
    Platform::String^ str = 
        ref new Platform::String(keyString.data(), keyString.length());
    // Get localized string
    str = PhoneDirect3DXamlAppComponent::LocalizationComponent::Instance
              ->GetLocalizedString(str);
    return winRTStringToString(str);
}

std::string Localization::formattedInteger(long number)
{
    // Get localized string
    Platform::String^ 
      str = PhoneDirect3DXamlAppComponent::LocalizationComponent::Instance
                ->GetFormattedNumber(number);
    return winRTStringToString(str);
}

There are a couple of important things to note here:

CCWinRTUtils.h provides you with the basic methods you need for converting between C# strings and C strings, which is great. Unfortunately, they add a \r before a \n, so you need to remove that. Otherwise, little white boxes appear before a new line in your CCLabels.

As with Objective-C++ vs C++, the header files cannot be a mix of the two languages. This means you need to use an intermediary Managed C++ class to communicate between C++ and C#, which in the above example is PhoneDirect3DXamlAppComponent::LocalizationComponent.

The last hurdle was an unexpected one. I started preparing the screenshots for submission to the Windows Phone Store, which is time-consuming but wonderfully easy thanks to the Windows Phone 8 simulator, and everything seemed fine until I got to Asian languages. Suddenly, all my labels were printing out boxes instead of characters.

I thought at first there was a problem with my conversion of C# strings to C strings, but that wasn't the case. Eventually, I dug into platform/winrt/CCFreeTypeFont.cpp and discovered the problem. You see, when iOS and Android try to draw a glyph — what we normally call a "character" in an alphabet — that can't be found in the supplied font, they fall back to a system font that contains the glyph. Windows Phone 8 normally does this, too, but cocos2d-x creates labels for WP8 using CCFreeTypeFont, which doesn't perform the fallback.

At first, I considered adding a font file that contained all the required characters, but such a font file is approximately 22MB (there are a lot of Unicode characters out there) and not something that should be added manually.

After plenty of research, I discovered the following plan to solve my problem:

  1. CCFreeTypeFont has a method called loadSystemFont, which returns nullptr for WP8 by default but can actually be used. I found the solution on the cocos2d forum.
  2. The available system fonts for WP8 can be found here. Passing the right font name to your code is not straightforward. For example, if you want to load the Yu Gothic font (for Japanese), then you pass to loadSystemFont("YuGothic"), but if you want to load Microsoft Mhei (for Traditional Chinese), you need to pass in MSMhei.
  3. The FreeType library informs the code when a glyph is not found. At this point in the code, I set up the fallback mechanism to go through the pre-loaded system fonts until it finds one with the required glyph.

Conclusion

With these porting activities complete, we have published Ready Steady Play to iOS, Android, and Windows Phone 8, and made it to the vendor stores for each of these platforms.

It took about seven months to create the first release of Ready Steady Play. cocos2d-x has proven to be a great tool to use for developing a 2D mobile game, and it would take a lot to convince me to use something else. Because it's open source, developers can cater it to their own needs, as I did. The project has hundreds of contributors helping to make it better with each iteration

I really hope that this article is of use to anybody interested in game development and encourages you to dive right in using cocos2d-x. Remember that you're not alone in using this tool or any of the others out there (and there are many), with each having its own fast-growing community ready and willing to help you along the way.


Guy Kogus Lead Game Developer at Cowboy Games. This article was abridged and adapted from a write up that previously appeared on Microsoft's UK developer site. Used with permission.


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.