Porting SpriteKit to Cocos2d-x

The following information is targeted to software developers who have created a game in Swift using Apple’s SpriteKit , who now want to also support Android devices.  One of the best ways to support Android is to port your game to C++ and to use the Cocos2d-x game library.  Why?  Because Cocos2d and SpriteKit are close cousins.  So, having gone through this process, I want share some of what I’ve learned in the process.

But if I knew in advance that I wanted to support Android, why not start with Cocos2d-x which supports both Apple and Android devices?  The reason for starting with SpriteKit was that this was my first smartphone game, and the documentation from Apple, raywenderlich.com, and others offered a much more accessible learning experience than was available for Cocos2d-x.  I wanted to get my app up and running quickly and I needed to get there without a steep learning curve.  Apple is great about making their documentation understandable and current.  Cocos2d-x is not as strong in this regard.

Here are some of the resources I found most useful as I was teaching myself about Cocos2d-x:

  • Online resources:

* http://www.cocos2d-x.org/docs/static-pages/programmers-guide.html,
* http://heyalda.com/crash-course-in-cpp-for-cocos2d-x-developers-part-2/,
* http://www.cprogramming.com/c++11/c++11-lambda-closures.html
* http://heyalda.com/13-things-every-cocos2d-x-c-android-game-developer-should-know/,
* http://techiefold-game.blogspot.com/2016/02/porting-between-ios-spritekit-and.html ,

  • And I found this book particularly useful:

Cocos2d-x Cookbook by Akihiro Matsuura, available with searchable PDF if ordered online directly from Packt Publishing.

In many respects, converting an app to Cocos2d-x from SpriteKit can be a fairly simple matter of changing the syntax from Swift to C++, and changing your game library calls from SpriteKit to the corresponding function calls in Cocos2d-x.  However, there are some tricky porting issues that I discovered along the way.  I’d like to highlight them now.

DEVELOPMENT ENVIRONMENT

NOTE:  Since Cocos2d-x version 3.16, the Terminal command-line approach I describe in this section is no longer available to Android developers.  Instead you must use Android Studio for Android development.  I’ve left this section here to document my approach using Cocos2d-x version 3.14, but if you’re using v 3.16 or later you can ignore most of the rest of this section and instead rely on this information for configuring your development environment.

The online forum here can be useful for answering specific questions that you post, but much of the material there is out of date.  Hence, it is NOT a good place to “search” for answers – especially answers to questions about setting up a development environment (which changes frequently).

For my Cocos2d-x Android development, I chose to use Xcode for editing, the Mac desktop target as an emulator for breakpoint debugging, and text-based Terminal for building and testing on Android devices.

I include something like the following in AppDelegate.cpp so that I can emulate Android devices with different aspect ratios when testing on the Mac desktop.  You may wish to do something similar:

#if (CC_TARGET_PLATFORM == CC_PLATFORM_MAC) // emulate Android sizes on Mac
    // uncomment one line here
    //auto screenSize = cocos2d::Size(1024, 768); //1.33
    //auto screenSize = cocos2d::Size(1280, 720); //1.78
    //auto screenSize = cocos2d::Size(2960, 1440); //2.06
#else // Android
    auto screenSize = Director::getInstance()->getWinSizeInPixels();
#endif
auto rawHeight = fminf(screenSize.height, screenSize.width);
auto rawWidth = fmaxf(screenSize.height, screenSize.width);
auto widthRatio = rawWidth / rawHeight;
auto screenHeight = 640;
auto screenWidth = (int)(widthRatio*(float)screenHeight + 0.5f);
// I then setDesignResolutionSize based on screenWidth and screenHeight

This is the Terminal command used to create a new Android project:
cocos new yourprogramname -p com.yourcompany.yourprogramname -l cpp

This builds it:
cocos compile -p android –ap android-yourminandroidversion#

This installs and runs it on your USB-connected test device (once the device is enabled for USB debugging):
cocos run -p android

This displays your debug output from ‘CCLOG(“formatstring”, data);’ statements in your program :
adb logcat | grep “D/cocos”

You’ll also need to edit MinSdkVersion in proj.android/AndroidManifest.xml, and add the names of all your cpp files to proj.android/jni/Android.mk.  Make sure to configure any editor you use, such as TextEdit.app, so that it doesn’t store tabs or curly quotations in these files, as it will cause your builds to fail.

HANDLING DIFFERENT SCREEN SIZES

One of the main differences between SpriteKit and Cocos2d-x is the way different screen sizes are handled.  I recommend learning as much as you can about this before you start porting your SpriteKit code to Cocos2d-x.  SpriteKit uses “points” to measure distances and sizes on the screen, using approximately 150 points per inch regardless of the iOS device’s screen size.

Cocos2d-x uses an entirely different method, as described here.  Using your chosen “design resolution”, every screen appears within your program to have the same height (or width, depending on your “resizing policy”), no matter how large the physical screen is.  This makes it easy for you game to look identical, no matter what size screen it appears on. You do need to anticipate the needs of different screen aspect ratios so that critical content is not cropped off the edges.

I dealt with these differences in a way that suits the needs of my game.  In my SpriteKit game I used a single set of graphic assets for my sprites and backgrounds. Some developers may prefer to make additional sets of better-looking images for different resolutions, but I decided that my game didn’t require it.  For SpriteKit I made sure they looked good on large iPad retina screens, and relied on downscaling to render these same assets on smaller screens.  For Cocos2d-x I selected a design resolution, resolution policy, and ContentScaleFactor so that I am able to use the same graphic assets in both SpriteKit and Cocos2d-x.  I spent quite a bit of time up-front figuring out what percentage of the screen height I wanted my images to occupy, and I selected parameters that required the fewest resizing tweaks in my Cocos2d-x project.

RESIZING SPRITES

In SpriteKit, sprites are resized using the “size” property.  In Cocos2d-x, sprites are resized using the “setScale” method.  I configured my design resolution and contentScaleFactor so that for most of my sprites, I don’t need to call setScale to resize it – it just works at its native size.  But for those sprites that needed rescaling in iOS, I also need to rescale them in cocos2dx.  In those cases I call setScale(2 * width in iOS in points / width of png in pixels).  One thing you should keep in mind is that you should be able to call setScale with some multiple of (iOS points width / png pixel width) to get the desired rescaling in Cocos2d-x.

Bear in mind that if you rescale a sprite in Cocos2d-x and that sprite has children sprites, the children will be rescaled by that same amount (which may not be what you want, since SpriteKit doesn’t do that).  In such cases, I found that I need to restructure the node tree in Cocos2d-x.  I create a placeholder node as the parent, the rescaled image of the parent as one of the placeholder node’s children, and the children sprites as other children of the placeholder node.  So the rescaled parent image sprite doesn’t affect the scale of the children.

OPTIONALS AND POINTERS

Swift supports optionals and SpriteKit uses them.  Instead of optionals, Cocos2d-x uses pointers to its objects, and a pointer equal to “nullptr” is equivalent to a nil optional in Swift.  The main issue you’ll encounter is that in Swift member functions may be called (and are ignored) when the calling object is a nil optional.  However, in C++ you’ll need to explicitly test for nullptr to determine if it’s safe to use an object pointer to call a member function.

POSITIONING CHILDREN SPRITES

In SpriteKit, a child sprite is positioned relative to the anchor point of its parent sprite.  In Cocos2d-x, a child sprite is positioned relative to the bottom-left corner of its parent sprite.  So you’ll need to adjust the position of your child sprites accordingly.

CLONING SPRITES

In SpriteKit, cloning a sprite with all of its children can be done with a single line of code.  Unfortunately, there’s no corresponding function in Cocos2d-x.  In Cocos2d-x you would need to decide which of the sprite’s instance variables need to be copied, if it has children that also need to be copied, and then manually make copies of these elements.  Here’s an example showing the SpriteKit code, followed by the Cocos2d-x equivalent, for a function that creates a clone of the last child of the current node.

// Here’s the Swift / SpriteKit code
func getCopyOfLastChild() -> SKSpriteNode? {
    return children.last?.copy() as? SKSpriteNode
}

// Here’s the equivalent C++ / Cocos2d-x code    
Sprite* MyClass::getCopyOfLastChild() {
    auto lastBlock = dynamic_cast<Sprite*>(getChildren().back());
    auto sprite = Sprite::createWithTexture(lastBlock->getTexture());
    sprite->setPosition(lastBlock->getPosition());
    sprite->setAnchorPoint(lastBlock->getAnchorPoint());
    sprite->setName(lastBlock->getName());
    return sprite;
}

Z-POSITION OF NODES

In SpriteKit, the zPosition of a node (which affects the order in which nodes are rendered) is a floating point number, relative to its PARENT only.  In Cocos2d-x, the zPosition of a node is an integer giving its rendering order relative to ALL other nodes in the scene.  For example, if A is the parent of both B and C, and A has zPosition=5, B has zPosition=3 and C has zPosition=2, in SpriteKit A will be rendered first, followed by C, followed by B.  In Cocos2d-x, C would be rendered first, followed by B, followed by A.

ANGLES

In SpriteKit, rotational angles are measured as counter-clockwise radians, with 0 at the three o’clock position.  In Cocos2d-x, rotational angles are measured as clockwise degrees, with 0 at the three o’clock position.  To convert between degrees and radians in Cocos2d-x, you can use the macros CC_DEGREES_TO_RADIANS and CC_RADIANS_TO_DEGREES, and don’t forget to invert the sign in the formulas you used in SpriteKit.

TOUCH DETECTION

In SpriteKit when you touch the screen, you get a callback to a common routine with an array of all nodes that fall beneath your touch, with the topmost node being first in the array.  All nodes in the current scene are automatically included in the list of potential nodes that may be in the array, but only the touched nodes appear in the array.  In Cocos2d-x, you must explicitly register a callback for any node you want to see touch events for, and ALL nodes you have registered will callback when you touch ANYWHERE on the screen.

To make Cocos2d-x behave more like SpriteKit, you should register callbacks for all the nodes you are interested in receiving touches for.  You should make all the nodes within a scene invoke the same callback function (using the clone() method when registering the callback).  Your callback routine should start with the following code, which will ignore any nodes that you are not currently touching:

bool myScene::myTouchCallback(Touch* touch, Event* event) {
    auto node = event->getCurrentTarget();
    if (!Rect(Vec2::ZERO, node->getContentSize()).containsPoint(node->convertToNodeSpace(touch->getLocation()))) {
        return false; // this node is not being touched
    }

    if (<this is a node I want to handle>) {
        <do stuff...>
        return true;
    } else if (<this is a node I want to handle>) {
        <do stuff...>
        return true;
    }
    …
    return false;
}

REFERENCE COUNTING

Both Swift-SpriteKit and Cocos2d-x use reference counting to automatically manage memory for you.  Ordinarily you don’t have to think about it, but there are situations where you may care.  SpriteKit uses Swift’s Automatic Reference Counting.  Any object will remain as long as there’s any reference to it, and will be removed once all references to the object go out of scope.  This includes all SpriteKit nodes and scenes.  Cocos2d-x also has reference counting, but it is handled differently.  Scenes are kept or discarded depending upon whether you transition between them using replaceScene() or pushScene().  And in Cocos2d-x, nodes will be discarded when they are no longer part of the current scene graph.   In other words, a node must be a child in the current scene graph or it will be removed.

ARTWORK AND EMOJIS

If you don’t already know it, you will soon discover that that Gimp or Photoshop will become your favorite artwork program that you love to hate.  I use Photoshop, and I’m rarely able to accomplish anything new without searching online for instructions.  It’s powerful but hard to use for anything new.  In my SpriteKit game I made heavy use of emojis that Apple provides for free with their fonts, so I wouldn’t have to search for free images or draw them myself.  The Android environment doesn’t provide the same wide variety of emojis in the free fonts that I was able to find.  So, you’ll have to come up with your own emoji replacements in Cocos2d-x and load them as sprites, rather than as text emojis.

IN-APP PURCHASES

SwiftyStoreKit works great for in-app purchases in Swift.  For Cocos2d-x, SDKBox can do essentially the same thing.  One thing I like about SwiftyStoreKit is that you are able to organize the code in your purchase scene using closures, so that the asynchronous messages from the store don’t interrupt the flow of the program.  I added a layer in my Cocos2d-x purchase scene to respond to messages from SDKBox, so that Lambda functions can be used just like closures are used with SwiftyStoreKit.

My in-app purchase is a non-renewable game upgrade.  But for testing purposes I activated a different, renewable in-app purchase product so that I could repeatedly test the purchase functionality without having to change user accounts.  Then, I switched to a non-renewable product when I needed to test the “restore” functionality.

To save a local record of in-app purchases that persists across software updates, I used a keychain entry in my SpriteKit game.  In Cocos2d-x, apparently the best way to save an update-persistent record is to store it in UserDefault.  It is recommended to set up your own server as protection against hackers trying to fake in-app purchases.

CLOSURES, LAMBDA FUNCTIONS AND ACTION CALLBACKS

Swift uses closures and C++ uses lambda functions to specify unnamed callback functions as function parameters.  This capability is particularly useful when used as a callback for when a runAction completes.  Using closures in Swift is straightforward, as seen in this code snippet:

run(.wait(forDuration: 1)) {
    (do something after waiting one second)
}

The same thing in C++ would look like this:

runAction(Sequence::create(DelayTime::create(1), CallFunc::create([=]() {
    (do something after waiting one second);
}), nullptr));

Pretty ugly!  To make C++ runAction callbacks more readable, I created these macros:
#define RUNACTION(action) runAction(Sequence::create(action,CallFunc::create(
#define END ),nullptr));

So, now my C++ runAction with lambda callback looks much more readable:

RUNACTION(DelayTime::create(1)) [=](){
    (do something after waiting one second);
}END

Furthermore, you may want to create your own no-parameter callback functions in C++.  To create a reference to your callback function, you would do something like this:
CC_CALLBACK_0(myClass::myCallbackFunction, this)

Now the following, written in Swift:
run(.wait(forDuration: 1), completion: myCallbackFuction)

will look like this when written in C++:
RUNACTION(DelayTime::create(1)) CC_CALLBACK_0(myClass::myCallbackFunction,this) END

Additionally, to pass your callback function as as a completion parameter to another function in your program, you can declare and use this typedef as your parameter type in C++:
typedef std::function<void()> Callbk; // Callbk is the parameter type

// in Swift:
func foo(_ completion: @escaping () -> Void) {
    ...
    completion()
}

// and the same function in C++ is:
void myClass::foo(Callbk completion) {
    ...
    completion();
}

CHANGES I MADE TO THE COCOS2D-X LIBRARY SOURCES

I found that collision detection didn’t work until I fixed ccPhysicsWorld.cpp by changing || to && as described here.

I realized that there was no way to show a transition when popping a scene, so I implemented Alex Wong’s solution shown at the end of this thread.  You’ll also see that I added my own suggestions to this same thread.

IN SUMMARY

If you are porting your SpriteKit game to Cocos2d-x, or if you are just looking for some pointers on getting started with Cocos2d-x, I hope you have found something useful here.  Please let me know if you’ve found anything that should be corrected or improved, by using my feedback form here.