Prerequisites
This article assumes that you already know the basics of C++ described in Crash Course in C++ for Cocos2d-x Developers (Part 1) .
You should already know the following:
- creating C++ classes, constructors, destructors
- class access control
- automatic and static memory
- virtual methods
- variables, pointers, references, and the auto keyword
- namespaces
- dynamic memory allocation with new and delete
- logic control statements such as if statements, for loops, and switch statements
- operator overloading
If you are not familiar with the above, then click here.
This article covers the following topics.
- Cocos2d-x Core Classes and Memory Management
- C++ Classes with Cocos2d-x
- Virtual Methods and Abstract Base Classes
- The Delegation Design Pattern
- Lambda Functions
- Considerations When Subclassing Cocos2d-x Classes
- C++ Typed Enumerations
- Cross Platform Class Design
Intro
The C++ programming language has been around for a long time. It is powerful, portable, flexible and performant. When it comes to making video games, C++ can be a great choice to enable productivity and expression of ideas. But the power of C++ does come at a price and that price must be understood.
C++ can be unforgiving if you don’t understand the basics. If you are the kind of person that just slaps out code without trying to understand the code — and if it works, then move on — then you will likely have problems. But with a little upfront diligence it is not that much work to become a proficient C++ programmer.
Leveraging quality libraries to do common tasks in C++ makes simple things quite easy to accomplish. Complex things are probably always possible due to the extensibility and low-level nature of C++.
With that all said, lets dive in to this cheatsheet of must know C++ coding practices for making games with C++ and Cocos2d-x.
Director, Scene, Node, and Sprite Cocos2d-x Classes
Before getting into the C++ aspects of using Cocos2d-x, it is important to be aware of the classes Director, Scene, Node and Sprite.
Director
The Director is a singleton instance that is used to direct which scene is active, among other things.
Scene
The Scene is a container where we add Nodes, Sprites, Layers, and other Node subclasses. The scene is the highest level object in what is called the scene graph.
Node and Sprite
The Node and Sprite classes are containers that contain things like game logic, sprite texture to draw, and many other Cocos2d-x class types such as actions, physics objects, particle effects, custom OpenGL drawing, etc.
Cocos2d-x Memory Management
During runtime of our Cocos2d-x game, we create a scene, and then add children to that scene. The children are all instances of classes derived from cocos2d::Node (most Cocos2d-x objects are subclasses of Node).
We then tell the Director to run the scene. When we tell the director to replace the scene with a different scene, or to end the scene, all children objects are recursively destroyed.
Cocos2d-x Node subclasses (including Sprite and Layer) use a factory method called create or createWith that returns an instance of the class. That factory method encapsulates the use of the keyword new to dynamically allocate an instance of the class.
When the scene is destroyed, all nodes are recursively also destroyed and delete is called on all instances of node.
A more detailed explanation of Cocos2d-x memory management was provided in part one of this article under theCocos2d-x Memory Management.
Basic Overview of Creating Classes in C++ with Cocos2d-x
The basic class in C++ is declared in a MyClassName.h header file like shown below. The Node class is a foundational class in Cocos2d-x. Most classes that you create will be subclasses of Node, Sprite or Layer.
Sprite and Layer are both subclasses of Node. For each of these classes, you can use the CREATE_FUNC macro to create a method to initialize the inherited features of the superclass. You then implement the init method in your .cpp implementation file to add your custom initialization. The superclass will call the init method for you.
The onEnter, onEnterTransitionDidFinish, and onExit methods can be overridden in your implementation if you want to use them. They are called as their name implies.
The #pragma once preprocessor command is used to ensure that this code is only included once into the program when the app is compiled. You should always add that at the top of each header file that you create.
The constructor and destructor are declared private. This is done so that it is not possible for a user of this class to try to create the class using the constructor. This is a Cocos2d-x thing because Cocos2d-x takes care of a lot of memory management for us, which is a nice thing to have.
Any subclass of Node can be added to another Node. Then that parent node superclass will take care of removing that child node from your game when the parent class is no longer needed by the game.
MyClassName.h Header File
#pragma once // Always use an include guard at the top of each header file. #include <string> // If you wanted to use std::string. #include <vector> // If you wanted to use std::vector. #include "cocos2d.h" // Include the cocos2d-x headers. class MyClassName : public cocos2d::Node { public: CREATE_FUNC(MyClassName); virtual bool init(); virtual void onEnter(); private: // Constructor private to make misuse of this class difficult. MyClassName(); // Constructor declared but defined in the cpp file. ~MyClassName(){} // Destructor declared and defined here. void doSomeSetup(); float myFloatVar; int myIntVar; };
The MyClassName.cpp file is shown below in the next code snippet.
Note the #include directive that is used to include the MyClassName.h header file in theMyClassName.cpp file below. All implementation .cpp files need to include the header file for the class that they are implementing.
The init method is where the majority of your game initialization should be conducted. You can organize your code into other methods in this class, such as the arbitrarily named method below doSomeSetup, or in other objects. But it is a good idea to initialize things from the init method.
The second most common location to init an object is the onEnter or onEnterTransitionDidFinish methods that are inherited from Node and called by the Node superclass.
Notice the onEnter method and how the superclass Node code is called by using the name of the class followed by two colons and then the name of the method. This is how superclass methods are called in C++. This is also the syntax for how static methods are called.
We should always call the superclass methods of the onEnter and onExit named methods because Node does some important code for us and we don’t want to hide that code with our own definition of these methods.
#include "MyClassName.h” USING_NS_CC; MyClassName::MyClassName() : myFloatVar(0.0), myIntVar(0) {} MyClassName::init() { if (!Node::init()) return false; // Do some initialization. this->doSomeSetup(); return true; } void MyClassName::doSomeSetup() { // Do some setup. } void MyClassName::onEnter() { Node::OnEnter(); // Do some initialization when this node becomes active. }
Using the above code, you can then create an instance of MyClassName in some other class like this.
auto myClassInstance = MyClassName::create();
The auto key word is a C++11 feature that can be used to declare a variable. The type is inferred from variable assignment.
If the myClassInstance was created inside of another Node, Layer, Scene, Sprite, or any subclass of Node, then you could add the myClassInstance node as a child to the parent node like this.
this->addChild(myClassInstance);
As noted above, when we add a node subclass to the scene graph, then the scene graph will take care of destroying that node subclass for us. The destructor of our Node subclass will still be called so that we can do any cleanup as necessary.
Virtual Methods and Abstract Base Classes
The virtual keyword can be used to tell the compiler to prefer a derived method instead of the method in the base class.
One common use of this is when defining an abstract base class for use in the delegation pattern. An abstract base class is essentially just an interface definition. If a class inherits that abstract base class, it is essentially just inheriting that interface and must provide an implementation for that interface by defining those methods in the header and .cpp implementation.
Abstract Base Class and the Delegation Pattern
The delegate pattern is a great way to decouple code into reusable classes that stand alone. We can then use the concept of property and delegate to enable two-way communication between a property and the property’s owner.
Take for example a race car class called RaceCar. The RaceCar needs a gas and brake pedal. The race car could have a property called HeyaldaUserControls that it adds as a private class member. The RaceCar can then inherit from the HeyaldaTouchDelegate class, add the HeyaldaTouchDelegate to the RaceCar interface, and implement the HeyaldaTouchDelegate methods.
Then in the init method of RaceCar, the RaceCar class would call the setDelegate of the HeyaldaUserControl property to tell the HeyaldaUserControl property that RaceCar will be its delegate.
When RaceCar no longer wants to be the HeyaldaUserControl’s delegate, it can call setDelegate(nullptr) on the HeyaldaUserControls property.
By using the delegation pattern, we can add HeyaldaUserControl as a property to any class and tell that instance of HeyaldaUserControls who its delegate is.
HeyaldaTouchDelegate.h
class HeyaldaTouchDelegate { public: virtual void touchBegan(cocos2d::Touch * touch)=0; virtual void touchMoved(cocos2d::Touch * touch)=0; virtual void touchEnded(cocos2d::Touch * touch)=0; virtual void touchCanceled(cocos2d::Touch * touch)=0; };
The above abstract base class can be inherited from and used in a class that wants to implement the base class. We can use multiple inheritance to enable the HeyaldaUserControls class to be a Node subclass and also inherit the interface defined for the HeyaldaTouchDelegate.
HeyaldaUserControls.h
#include "HeyaldaTouchDelegate.h" class HeyaldaUserControls : public Node { public: void touchBegan(cocos2d::Touch * touch); void touchMoved(cocos2d::Touch * touch); void touchEnded(cocos2d::Touch * touch); void touchCanceled(cocos2d::Touch * touch); setDelegate(HeyaldaTouchDelegate * delegate) {_delegate = delegate; } private: HeyaldaTouchDelegate * _delegate; };
HeyaldaUserControls.cpp
#include "HeyaldaUserControls.h" // Note that delegate is initialized to nullptr in the constructor HeyaldaUserControls::HeyaldaUserControls() : _delegate(nullptr) HeyaldaUserControls::touchBegan(cocos2d::Touch * touch) { if (_delegate != nullptr) { _delegate->touchBegan(touch); } } HeyaldaUserControls::touchMoved(cocos2d::Touch * touch) { if (_delegate != nullptr) { _delegate->touchMoved(touch); } } HeyaldaUserControls::touchEnded(cocos2d::Touch * touch) { if (_delegate != nullptr) { _delegate->touchEnded(touch); } } HeyaldaUserControls::touchCanceled(cocos2d::Touch * touch) { if (_delegate != nullptr) { _delegate->touchCanceled(touch); } }
RaceCar.h
#pragma once #include "cocos2d.h" #include "HeyaldaTouchDelegate.h" #include "HeyaldaUserControls.h" class RacerCar : public cocos2d::Node, public HeyaldaTouchDelegate { public: CREATE_FUNC(RacerCar); virtual bool init(); virtual void onExit(); void touchBegan(cocos2d::Touch * touch); void touchMoved(cocos2d::Touch * touch); void touchEnded(cocos2d::Touch * touch); void touchCanceled(cocos2d::Touch * touch); private: // Constructor private to make misuse of this class difficult. RacerCar(){} ~RacerCar(){} HeyaldaUserControls _userControls; };
RaceCar.cpp
#include "RaceCar.h" #include "HeyaldaUserControls.h" USING_NS_CC; MyClassName::init() { if (!Node::init()) return false; _userControls.setDelegate(this); return true; } void MyClassName::onExit() { // Remove this class from being a delegate of the HeyaldaUserControls. _userControls.setDelegate(nullptr); Node::onExit(); } HeyaldaUserControls::touchBegan(cocos2d::Touch * touch) { } HeyaldaUserControls::touchMoved(cocos2d::Touch * touch) { } HeyaldaUserControls::touchEnded(cocos2d::Touch * touch) { } HeyaldaUserControls::touchCanceled(cocos2d::Touch * touch) { }
Lambda Functions
The lambda is a block of code that can be passed around and executed. It is kind of like an anonymous function.
A common use case of lambda’s is to pass the lambda to an object that can use it as a callback. The alternative would be to define a callback function somewhere. But lambdas are nice because they can easily be defined inline. This can make writing and debugging code easier.
The C++11 lambda functions are a great tool to use and understand. But they also are an easy way to get yourself into trouble if you don’t know what you are doing.
Probably the most important thing to know about lambdas is the capture syntax. You can capture by value or by reference. For most cases, I find that capturing by reference is what I need.
The following code snippet is an example of a lambda function.
The value between the square brackets defines how to capture variables from the scope that this lambda exists in. By scope I mean data in the current method that defines the lambda and any other data that local scope has access to.
The instance variables this and the class member variable _someFlagVariable are captured by reference for the lambda to use, due to the & between the square brackets.
Alternatively, if you were to put an equal sign [=] in between the square brackets, then you would capture by value. So a copy of the object pointed to by this and a copy of _someFlagVariable would be passed into the lambda. That usually is not what you want. Passing by value can cause very strange bugs that can be hard to debug in a Cocos2d-x game.
The lambda below, arbitrarily defined here as playCallback and of type ccMenuCallback, can then be passed into a MenuItem object, where the MenuItem will execute the code in the lambda when the MenuItem is depressed.
ccMenuCallback playCallback = [&](Ref * sender) { this->doSomething(); _someFlagVariable = true; Scene *nextScene = GamePlayScene::createScene(); Director::getInstance()->replaceScene(nextScene); };
Subclassing a Cocos2d-x Class
You probably will find a need to subclass Node and Sprite (a subclass of Node) in your games.
In Cocos2d-x, most Cocos2d-x classes are a subclass of Node. Node takes care of some memory management for us. Since Cocos2d-x takes care of memory management, there are a few hoops to jump through to properly subclass one of the Cocos2d-x Node based classes.
As discussed above, the basic pattern for subclassing a Cocos2d-x class that uses the default static create method can be done like this.
Header File
class Asteroid : public cocos2d::Sprite { public: CREATE_FUNC(Asteroid); virtual bool init(); private: Asteroid(){} ~Asteroid(){} };
CPP File
bool Asteroid::init() { if (!Sprite::init()) return false; return true; }
But you might also want to use a custom create method. In this case you would not use the CREATE_FUNC macro. Instead, you would implement a static method using the same pattern that the CREATE_FUNC macro would create. The following is what that would look like if for some reason you wanted to pass a cocos2d::Size parameter into your create method.
Header File
class Asteroid : public cocos2d::Sprite { public: static Asteroid * createWithSize(cocos2d::Size size); virtual bool initWithSize(cocos2d::Size size); private: Asteroid(){} ~Asteroid(){} };
CPP File
// This is essentially what the CREATE_FUNC would create, but it has been expanded to accept passing the size parameter. Asteroid* Asteroid::createWithSize(cocos2d::Size size) { Asteroid* pRet = new Asteroid(); if (pRet && pRet->initWithSize(cocos2d::Size size)) { pRet->autorelease(); } else { CC_SAFE_DELETE(pRet); } return pRet; } bool Asteroid::initWithSize(cocos2d::Size size) { if (!Sprite::init()) return false; // Do something with the parameter passed through via the create method. e.g. size. return true; }
The C++ Standard Template Library
Some commonly used features of C++11 that you will want to become familiar with include STL containers such as std::string, std::vector, std::map, and std::unordered_map. The algorithms for operating on containers, found in the algorithm header, are also very useful.
Strings
Here is an example of using the std::string.
#include <string> std::string playerFirstName = "Jim"; std::string playerLastName = "Range"; // Combine strings together. auto fullName = playerFirstName + " " + playerLastName; // The fullName string will now contain the text "Jim Range". // Get a C string from the std::string. char * cStringName = fullName.c_str(); auto anotherName1 = "Jim Range"; auto anotherName2 = "Bill Gates"; // Use the std::string operator == to see if the text in two strings is the same. bool textIsTheSame = (fullName == anotherName1); // true textIsTheSame = (fullName == anotherName2); // false
Vectors
The std::vector is another useful container. It is essentially an array with a lot of extra functionality, and it can contain data of any type. The example below uses std::string as the data type of the vector. But you could use a Node* pointer, an int, or any other class or type.
#include <vector> std::vector <std::string> playerNames; playerNames.push_back("Jim"); playerNames.push_back("Greta"); playerNames.push_back("Katya"); // Loop through the vector and print out the names. for (auto playerName : playerNames) { CCLOG("Player Name:%s", playerName.c_str()); } // Get the number of players in the playerNames vector. size_t numberOfPlayers = playerNames.size(); // Get a copies of the std::string objects in the vector. auto player0 = playerNames[0]; auto player1 = playerNames[1];
Unordered Maps
The unordered_map is like a dictionary. I use std::string as the key and various data types for the value. Here is an example.
#include <string> #include <unordered_map> std::unordered_map<std::string,std::string> priceForProductID; priceForProductID["item1"] = "0.99"; priceForProductID["item2"] = "9.99"; priceForProductID["item3"] = "4.99"; std::string productOnePrice = priceForProductID["item1"]; // productOnePrice = "0.99"; priceForProductID.clear(); // Deletes all of the entries in the map.
Algorithms
Sometimes we need to leverage a standard algorithm to process some data. For example, we could have the names of some players sorted in one particular order. Maybe we want to reverse that order.
#include <string> #include <vector> #include <algorithm> // Create a vector of names for the sample. std::vector <vector>playerNames; playerNames.push_back("Jim"); playerNames.push_back("Greta"); playerNames.push_back("Katya"); // The elements of are playerNames currently are 0:Jim, 1:Greta, 2:Katya. // Reverse the order of the items in the vector. std::reverse(playerNames.begin(), playerNames.end()); // The elements of playerNames are now 0:Katya, 1:Greta, 2:Jim.
C++11 Typed Enumerations with Enum Class
Enumerations are used for code readability. We can create an enumeration of states that an object can be in. e.g. VEHICLE_RACING, VEHICLE_CRASHED, VECHICLE_AT_START_LINE, VEHICLE_FINISHED_RACE.
For enumerations, it is better to use enum class to create your enumeration. This enables each enumeration value to have a class type. This can help to avoid enumeration collisions that could otherwise more easily occur.
enum class VehicleState { AT_START_LINE, RACING, CRASHED, FINISHED_RACE };
You could then access values of this enumeration like this:
VehicleState vehicleState = VehicleState::AT_START_LINE;
Since the enum class uses int as the underlying type, it is possible to use the switch statement with enum class enumerations.
Cross Platform Class Design
Sometimes a specific platform, such as iOS, Android, or Windows Phone, will require platform specific code. But the whole point of using a cross platform game engine is so that we can write code once and then deploy to multiple targets. So how should this situation be handled in our Cocos2d-x game?
I prefer to use a common interface for all platforms and then create separate implementations for each platform when required. Then I can reuse this code in future games and only code using a single interface.
Take leaderboards and achievements for example. Apple has Game Center and Google Play has the Google Play Game Services. The common interface could be post score, post achievement, show leaderboard, show leaderboards, show achievements, etc.
A single header file can be created that defines this interface.
Then for each platform, I use a different cpp implementation. On iOS I use a .mm ObjC++ implementation with Game Center.
On Android I use a .cpp implementation with JNI code to talk to the Google Play Game Services Java code.
For Windows Phone, well, there are no “free” leaderboard services for Windows Phone. While the user base of Windows Phones is large in some countries, the monetization opportunity has not been compelling enough in my opinion to create and support a leaderboard service or pay for the use of one. But that could easily change in the future.
For some small implementations, it might be tempting to use preprocessor macros that detect the platform and only include code in the implementation for the platform being built. But this can get messy and really ugly to look at as time goes by and features are added to the single class full of preprocessor #if statements.
While using the macro detection of the platform does have it’s place, it is generally not a good idea to use it excessively in classes.
Here is an example of what NOT to do. Adding an excessive amount of preprocessor macros into a file can make it hard to read and ugly to look at.
void MyClass::myMethod1() { #if CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID #elif CC_TARGET_PLATFORM == CC_PLATFORM_IOS #elif CC_TARGET_PLATFORM == CC_PLATFORM_MAC #elif (CC_TARGET_PLATFORM == CC_PLATFORM_WIN32)|| (CC_TARGET_PLATFORM == CC_PLATFORM_WINRT) #endif } void MyClass::myMethod2() { #if CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID #elif CC_TARGET_PLATFORM == CC_PLATFORM_IOS #elif CC_TARGET_PLATFORM == CC_PLATFORM_MAC #elif (CC_TARGET_PLATFORM == CC_PLATFORM_WIN32)|| (CC_TARGET_PLATFORM == CC_PLATFORM_WINRT) #endif } ...
Avoid Preprocessor Defines
It might be tempting to use the #define syntax to define some constant value or macro. But in most cases you would be better off creating a function or a const static variable. The #define is used by the preprocessor as a string replace. This can result in hard to identify compile issues if you have a typo in a #define.
#define NUMBER_OF_THINGS 5 // Don't do this. const static int NUMBER_OF_THINGS = 5; // This is preferred. const static std::string LEVEL_ONE_SCORE_KEY = "L1-score"; // Good. #define MAX(x,y) (((x) < (y)) ? (y) : (x)) // Don't do this unless you have a really good reason. bool MAX(int x, int y) { return (x < y ? y : x;) } // This is preferred. // Or if you prefer generic programming with Templates, this is good too. template <class T> const T& MAX (const T& x, const T& y) { return ((x<y) ? y : x); }
You may find the situation where you want to use some code for development or testing that needs to be embedded into existing classes. Then it could make sense to have a preprocessor macro that could be set to 0 or 1 to provide the ability to add or remove that code at compile time. So there are still some valid uses for preprocessor macros.