Hi all!
Today I want to share with you an important caveat about composition in the context of Cocos2d-x. Composition is an object-oriented technique where a class contains (is composed of) another class to which it delegates functionality related to the latter.
This technique is frequently suggested as an alternative to inheritance in complex hierarchies in order to avoid tangled structures, specially in those languages that do not support multiple inheritance (i.e. a class cannot have more than one parent class), such as Java. More information about the composition-inheritance binomial can be read here.
In the context of Cocos2d-x, composition could be realized through a class that contains a Sprite object as part of its private members, for example:
class Ball
{
public:
explicit Ball( const BallType &bt )
{
switch(bt)
{
case BallType::Hard:
ballSprite = Sprite::create("Balls/Hard.png");
break;
//more cases...
}
}
~Ball()
{
//Clean-up by removing heap-allocated data
}
inline cocos2d::Sprite *getBallSprite() { return ballSprite; }
private:
cocos2d::Sprite *ballSprite;
//more data
};
In this case, the Ball constructor simply takes a scoped enum value and creates a sprite object depending on the type. We also provide a getter function to retrieve the pointer to the sprite object. The destructor, as always, should clean up the memory allocated by the object.
At this point, you may be tempted to place a delete ballSprite statement, but this would yield a compiler error, because Cocos2d-x uses its own reference counter for managing the memory of its objects. Therefore, when a Cocos2d-x object (such as a Sprite) is no longer referenced by any other object, the former is automatically deleted.
At this point, you may be tempted to place a delete ballSprite statement, but this would yield a compiler error, because Cocos2d-x uses its own reference counter for managing the memory of its objects. Therefore, when a Cocos2d-x object (such as a Sprite) is no longer referenced by any other object, the former is automatically deleted.
Now, a client class (e.g. a cocos2d::Layer ) could use the Ball class as follows:
//A method in our layer class
{
//start some loop
{
BallType bt = getRandomType();
//balls is a member variable of type std::vector<std::unique_ptr<Ball>>
std::unique_ptr newBall = std::make_unique<Ball>(bt);
layer -> addChild( newBall -> getSpriteBall() );
balls.pushBack( std::move(newBall) );
//... process balls
}
//All balls destroyed; all
Director::getInstance() -> replaceScene( AnotherScene::create() );
} //end of method
In a loop, this method creates balls of random types, adds the balls sprites to the layer to actually show the sprites on screen, and processes them (maybe it detects touches and so on). Upon termination of the loop, there is a change of Cocos2d-x scene, which means that all the children of the current layer (including the sprites of the balls) are safely deleted. Also, as the vector of balls goes out of scope, the destructor of the std::vector class is called, which in turns calls the destructor of the std::unique_ptr objects, which in turn calls the destructor of each Ball object. Everything perfect so far.
However, consider the following scenario. In this scenario, we want to show only on screen some balls of a specific type, whereas other types of balls should be stored and shown at a later time, for example upon termination of the loop. The code to perform this could be:
This code, which seems ok at a first glance, has an important flaw. The problem arises because of the automatic reference counting mechanism of Cocos2d-x, which we are not using properly. When we create a Sprite object in our Ball, the reference counter is still 0, because there is no object pointing at it. This means that if right after creating the Ball object we don't add the associated Sprite object to the layer hierarchy (via addChild() ), the next iteration of the framework will detect that the reference counter is 0 and will remove the Sprite object. However, if we add the Sprite to the layer hierarchy, we increase the reference counter by 1, which prevents the framework from deleting the Sprite.
In the first example, there was no problem because right after the creation of a Ball object, unconditionally, we added the Sprite to the layer hierarchy. However, in the following example, we are creating Ball objects and we are deferring the inclusion of their sprites in the hierarchy of the layer. Therefore, most likely, when we try to retrieve the Sprite object in the for loop, we simply retrieve garbage, because the pointer is dangling.
How to fix this? The solution is easy and fast. We simply need that Ball object reclaims (shared) ownership of its Sprite. Basically, we need that a Ball object is capable of expressing: "hey, I need the Sprite object, so don't remove it even if nobody else is using it". This is where the Cocos2d-x functions retain() and release() come into play, as shown next:
So this is the main consideration when using composition with Cocos2d-x objects! Easy, right?
Hope you enjoyed it!
FM
However, consider the following scenario. In this scenario, we want to show only on screen some balls of a specific type, whereas other types of balls should be stored and shown at a later time, for example upon termination of the loop. The code to perform this could be:
//Method in our layer class
{
//start a loop
{
BallType bt = getRandomType();
std::unique_ptr newBall = std::make_unique<Ball>(bt);
if ( bt == BallType::Hard )
{
layer -> addChild( newBall -> getSpriteBall() );
balls.pushBack( std::move(newBall) );
}
else
{
ballsToShowLater.pushBack( std::move(newBall) );
}
//... process balls
}
//end of the loop
//show hidden balls
for (const auto& b : ballsToShowLater)
{
layer -> addChild( b -> getSpriteBall() );
}
//..some more stuff with the new balls shown
Director::getInstance() -> replaceScene( AnotherScene::create() );
} //end of the method
This code, which seems ok at a first glance, has an important flaw. The problem arises because of the automatic reference counting mechanism of Cocos2d-x, which we are not using properly. When we create a Sprite object in our Ball, the reference counter is still 0, because there is no object pointing at it. This means that if right after creating the Ball object we don't add the associated Sprite object to the layer hierarchy (via addChild() ), the next iteration of the framework will detect that the reference counter is 0 and will remove the Sprite object. However, if we add the Sprite to the layer hierarchy, we increase the reference counter by 1, which prevents the framework from deleting the Sprite.
In the first example, there was no problem because right after the creation of a Ball object, unconditionally, we added the Sprite to the layer hierarchy. However, in the following example, we are creating Ball objects and we are deferring the inclusion of their sprites in the hierarchy of the layer. Therefore, most likely, when we try to retrieve the Sprite object in the for loop, we simply retrieve garbage, because the pointer is dangling.
How to fix this? The solution is easy and fast. We simply need that Ball object reclaims (shared) ownership of its Sprite. Basically, we need that a Ball object is capable of expressing: "hey, I need the Sprite object, so don't remove it even if nobody else is using it". This is where the Cocos2d-x functions retain() and release() come into play, as shown next:
class Ball
{
public:
explicit Ball( const BallType &bt )
{
switch(bt)
{
case BallType::Hard:
ballSprite = Sprite::create("Balls/Hard.png");
break;
//more cases...
}
ballSprite -> retain(); //This is my sprite, so don't mess up with it!
}
~Ball()
{
//Clean-up by removing heap-allocated data
ballSprite -> release(); //I don't need this sprite object anymore
}
inline cocos2d::Sprite *getBallSprite() { return ballSprite; }
private:
cocos2d::Sprite *ballSprite;
//more data
};
So this is the main consideration when using composition with Cocos2d-x objects! Easy, right?
Hope you enjoyed it!
FM