Thursday, February 4, 2016

Porting between iOS SpriteKit and Cocos2d-x


This post is based on my experience on porting a game that I developed on SpriteKit and then had to port to Cocos2d-x to support both iOS and Android. Spritekit has its advantages as it is natively supported by iOS and is easier to get a game up and running with all the other elements like in-app purchase, cloud sync etc.

I had written my game and was almost ready to publish it on iOS. But then iOS9 was released and all hell broke loose!!! Most of my game assets were drawn programmatically and with iOS9 it created a huge mess. Images started appearing unaligned or not visible at all. Frustrating of all was that there was no real support from apple on it. So decided to take the bite and looked into cocos2d-x. It proved to be a good move as now I have the game both on iOS and Android.

Although this post is based on porting from SpriteKit to Cocos2d-x, it is a good read for developers porting from Cocos2d-x to SpriteKit as well. SpriteKit code examples given below are in Swift.

Ok now let us dive in!!!

Completion blocks

Anyone who has done programming for iOS knows how useful a completion block is. This is a great way to run some small functionalities asynchronously only when the previous function completes. It is more like a call back function without creating an explicit function.
Guess what, C++ too provides this feature via lambdas.

Here is what a completion block looks like in Swift
self.animateGameSuccess { (success) in
   ...
}

Convert the same to C++
animateGameSuccess([&](bool success) {
   ...
}

Pretty straight forward, right? Not so fast!!!
The complexity lies in the way variables are passed to the completion block or lambda function. In Swift, all variables are passed by value to the completion block. Whereas in C++, the onus is on the developer to decide. See the '&' in between the square brackets - this tells the compiler on how to pass the variables. The '&' here means that all variables are passed by reference.

Now see the following scenario:
void GameScene::beginGame(bool showStartup) {
   animateBeginGame([&]() {
      if(showStartup) {
         ...
      }
   }
}

Here the parameter 'showStartup' is used inside the lambda function. But the lambda function gets called only after 'animateBeginGame' is done. So by the time the lambda function gets called, the calling function 'beginGame' might have gone out of scope and so does the variable 'showStartup'. So any reference to it could fail.
How do we solve this? One solution is to make all variables pass by value. If we replace '&' by '=', then all variables are passed by value. But this might be inefficient.
So the other method is to specify which variable you need to pass as value explicitly.
void GameScene::beginGame(bool showStartup) {
   animateBeginGame([&, showStartup]() {
      if(showStartup) {
         ...
      }
   }
}

By specifying the variable explicitly, you made sure this value can be used even if 'beginGame' goes out of scope and also made sure that other variables are passed by reference.

Special care should be taken when there are nested completion blocks. All the automatic variables used in the inner most block should be passed by value in every level.

Callback functions in Cocos2d-x

In SpriteKit, 'runAction' function has a completion block that gets executed at the end of 'runAction'. This feature is not available in Cocos2d-x. So the workaround is to use callback function.
Here is an example code in SpriteKit.
runAction(showAction) {
   self.userInteractionEnabled = true
}

Now converting this to Cocos2d-x
auto callback = CallFunc::create( [&]() {
   enableUserInteraction(true);
});
this->runAction(Sequence::create(showAction, callback, nullptr));

I am using a helper function 'enableUserInteraction' here.
A callback is created using lambda function that enables the user interaction. And this has to be run in sequence as an action.

Callback also can be created from a function.
auto callback = CallFunc::create(CC_CALLBACK_0(MyGame::showActionFn, this));

Memory management

Memory management in Swift is pretty easy, there is nothing to manage!!!
Swift uses ARC (Automatic Reference Counting) to track and manage the memory. When a variable's reference count goes to zero, it is automatically removed from memory. So user need not worry about deallocating memory.

C++ does not provide memory management like Swift. So Cocos2d-x implements a trick to emulate the memory management similar to Swift. All objects in Cocos2d-x are sub-classes of 'class Ref'. This class implements the reference count of the object. And all objects are created using the statically defined 'create' functions of the object. This automatically puts the object in autorelease pool. As long as the object is used i.e. it is a child of some UI element, it will not be released. Otherwise it gets automatically released at the end of a frame. If the object needs to be retained even after it is removed from its parent, we need to call 'retain()' function explicitly on the object. But then we should remember to call an explicit 'release()' too when we are done with the object or else the object will never get deallocated.

That is, for all Cocos2d-x objects. What about other class objects that you create?
Whenever an object is created using 'new', a corresponding 'delete' call is required to deallocate this memory. Better solution is to use the smart pointers that are built using C++ template tricks. Smart pointers are out of scope of this post, so please look it up online.

Anchor point

What is anchor point?
Anchor point decides which part of sprite aligns to overall sprite's position. If we rotate a sprite, it rotates around this anchor point. Anchor point is usually and (x,y) pair with values ranging between 0 and 1. A value of (0.5, 0.5) sets its anchor point to the middle point.

Now let us see how anchor point decides an object's position in SpriteKit.
var shape1 = SKShapeNode(rectOfSize: CGSize(width: 100, height: 100))
shape1.fillColor = UIColor.redColor()

var shape2 = SKShapeNode(rectOfSize: CGSize(width: 50, height: 50))
shape2.fillColor = UIColor.greenColor()

shape1.addChild(shape2)

self.addChild(shape1)


Default anchor point in SpriteKit is always the middle point i.e (0.5, 0.5).

Black dot shows the anchor point of both the shapes. As the anchor point is the center point for both the shapes, both are aligned to the center.

Now let us see what happens when the parent's anchor point is changed.
var shape1 = SKShapeNode(rectOfSize: CGSize(width: 100, height: 100))
shape1.fillColor = UIColor.redColor()
shape1.anchorPoint = CGPoint(x: 0.0, y: 0.0)
shape1.position = CGPoint(x: -50, y: -50)

var shape2 = SKShapeNode(rectOfSize: CGSize(width: 50, height: 50))
shape2.fillColor = UIColor.greenColor()

shape1.addChild(shape2)

self.addChild(shape1)




Shape1's anchor point is set to (0,0), so to bring it back to center of screen the position is set to (-50, -50). Now we did not change any parameters for shape2, so why is it's position changed? The reason is because position of a child is relative to it's parent's anchor point also. Shape2's anchor point is aligned to Shape1's anchor point.

Now let us do the same experiment in Cocos2d-x.
DrawNode *shape1 = DrawNode::create();
shape1->drawSolidRect(Vec2(0,0), Vec2(100,100), Color4F::RED);
shape1->setContentSize(Size(100,100));
shape1->setAnchorPoint(Vec2::ANCHOR_MIDDLE);

DrawNode *shape2 = DrawNode::create();
shape2->drawSolidRect(Vec2(0,0), Vec2(50,50), Color4F::GREEN);
shape2->setContentSize(Size(50,50));
shape2->setAnchorPoint(Vec2::ANCHOR_MIDDLE);

shape1->addChild(shape2);

addChild(shape1);

DrawNode is the equivalent in Cocos2d-x to create simple shapes like SKShapeNode in SpriteKit.
DrawNode needs explicit setting of content size as the drawing routines does not calculate the content size. And we can also use some nice const definitions provided by Vec2 for the anchor points.



Straight away we can see the difference. Parent's anchor point does not influence child's position. When a child is added, default position is always (0,0) and the anchor point of the child is placed at that position relative to the parent.
So how do we achieve the same result as the first image from SpriteKit example?
DrawNode *shape1 = DrawNode::create();
shape1->drawSolidRect(Vec2(0,0), Vec2(100,100), Color4F::RED);
shape1->setContentSize(Size(100,100));
shape1->setAnchorPoint(Vec2::ANCHOR_MIDDLE);

DrawNode *shape2 = DrawNode::create();
shape2->drawSolidRect(Vec2(0,0), Vec2(50,50), Color4F::GREEN);
shape2->setContentSize(Size(50,50));
shape2->setAnchorPoint(Vec2::ANCHOR_MIDDLE);
shape2->setPosition(Vec2(50,50));

shape1->addChild(shape2);

addChild(shape1);

Here we set the position of 'shape2' to the center point of 'shape1'.

It is a subtle difference but this will result in careful porting of that part of your code where it depends on the position of sprites based on anchor points.

SKShapeNode vs DrawNode

If you have implemented your shapes in SpriteKit, you would have mostly used SKShapeNode. SKShapeNode gives very nice and simple methods to draw your own shapes in runtime.
When porting the same to Cocos2d-x, the closest class that can give you this functionality is DrawNode.
Here are the major differences between these two in terms of its functionalities:

  • DrawNode is incomplete!!! Many of the functionalities seen in SKShapeNode is not readily available in DrawNode. In end I ended up making a subclass and adding the functionalities that I need into this subclass using the primitive drawing operations that DrawNode provides. Some examples are rounded rectangle, shapes with border, drawing a path like CGPath, some shapes with solid color etc.
  • One drawback seen with SKShapeNode is that for whatever reason it does not support cropping. This is not a problem with DrawNode.
  • DrawNode does not support setColor. So there is no way to change the color dynamically other than redrawing the node again. Due to this drawback, actions like FadeIn and FadeOut also does not work as these actions depend on setColor.

Interfacing C++ to Objective-C


Many of the third party libraries for iOS are implemented in Objective-C. So there is a need to interface this library from C++ if we need to access their functionalities from Cocos2d-x. There is a wonderful set of libraries called SDKBOX that supports many of the third party libraries and it does the hard labour for you. But sometimes we may need to use some libraries that are not supported by SDKBOX.

The trick is to have C++ objects in Objective-C file. By renaming your Objective-C file from '.m' to '.mm', it turns into an Objective-C++ file and then you can call methods from C++ objects.

I will be creating a detailed separate blog on this - stay put :-).

Conclusion

This whole exercise was a fun experience. In the end it helped me in learning many intricacies in Swift, Objective-C and C++. And obviously SpriteKit and Cocos2d-x. I was able to port my game to Cocos2d-x within a month and another one week to get it running on Android.
I hope this helps anyone who is looking into porting their games to Cocos2d-x or back to SpriteKit.

Here are the links to the game 3Shapes:
 Google play store




3 comments:

  1. Hi Arun, Downloaded your app, your controls for Play and Start Game looks awesome.

    I have just started developing my first game using Cocos2d-x. I needed to create a rounded rectangle for my game. So I have tried code in below link but the rounded corners doesn't look smooth as they looks in your controls.

    http://lp6m.hatenablog.com/entry/2015/04/02/053252

    I am assuming all your controls (Moves, Target, Play and Start Game) are DrawNodes. Are they all indeed DrawNodes? If yes, can you please guide me how to draw these controls. Any help is much appreciated! Thanks.

    ReplyDelete
    Replies
    1. Try setting multisampling to YES in your AppController.mm.

      // Init the CCEAGLView
      CCEAGLView *eaglView = [CCEAGLView viewWithFrame: [window bounds]
      pixelFormat: (NSString*)cocos2d::GLViewImpl::_pixelFormat
      depthFormat: cocos2d::GLViewImpl::_depthFormat
      preserveBackbuffer: NO
      sharegroup: nil
      multiSampling: YES
      numberOfSamples: 4 ];

      Delete
  2. This comment has been removed by the author.

    ReplyDelete