COCOS2D-X 3.0 New Data Structure
Basic data structures are crucial in game development. A logic in each frame may need to be searched, deleted, or added from an array, or saved/retrieved from a dictionary quickly, the game engine also needs to traverse and sort the UI tree. The operation speed of basic data affects the program performance, while the use of basic data affects the development efficiency. Of course, we should try our best to avoid frequent iteration and search computing for every frame in the game, and cache the results as much as possible.
The C ++ Standard Library provides excellent basic data structures such as arrays (std: vector) and dictionaries (std: map, however, they do not support Cocos2d-x memory management methods (for Cocos2d-x memory management, see Chapter 3 ). In Cocos2d-x 2. in x and earlier versions, the Cocos2d-x provides CCArray and CCDictionary to work together with the Cocos2d-x's memory management approach, but they do not well support iterator operations in the standard library, this affects the development efficiency in a certain program.
Cocos2d-x 3.0 with Vector and Map Instead of the previous CCArray and CCDictionary, the new container class uses the template class to avoid unnecessary data type conversion and perfectly supports various iterative operations in the standard library, such as std :: find (), std: sort (), and so on. In fact, in 3.0, Vector and Map Is for std: vector and std: unordered_map in the standard library So that it can be combined with the Cocos2d-x memory management mode. We can understand the characteristics of the new data structure from the following three aspects.
1.2.6.1 Map Performance
For Map By default, the Cocos2d-x uses std: unordered_map, std: unordered_map to convert each key value to a hash value for storage and sort by hash value, therefore, it is not the storage order of Key or Value values in the actual dictionary. Unordered_map provides a faster search speed for a single Key value. It only needs to convert the Key to the hash value and perform one or more equal comparisons. The complexity is O (n ), while std: map's find complexity is O (log2 (n), it uses less than comparison between each element.
Std: When unordered_map is initialized, a certain number of (usually few) buckets are allocated to store Key/Value pairs. Each bucket corresponds to a hash Value. The hash value is calculated based on the number of buckets. Therefore, when an element is added, a bucket may correspond to multiple hash values, resulting in a conflict. std :: unordered_map needs to re-calculate all hash values (rehash), which may cause certain performance problems. Therefore, if you need to insert a certain amount of data in a short period of time, it is best to use the resverve method to set the number of buckets and reduce unnecessary rehash calculations. Of course, this is not required if only data is inserted or deleted occasionally, because resverve will increase the memory usage of unordered_map.
Another note is the calculation of std: hash. std: unordered_map uses a special hash algorithm. When its type is an integer, it directly uses itself as the hash value, this reduces the calculation workload of hash values. Therefore, the game should try to use integer as Map The Key value type, which greatly improves the Map For more information about the performance, see reference 4 and 5.
1.2.6.2 combined with Cocos2d-x Memory Management
In 2.x scenarios, CCArray and CCDictionary are usually allocated to the stack and we have to consider releasing their memory in the appropriate place. The new container class is no longer inherited from Ref (2. CCObject in x), new container classes should usually be allocated to the stack for use, which simplifies memory management. We should focus on the container elements rather than the memory management of the container itself.
T and Map in Vector The V in must be of the Ref type because they need to work together with the Cocos2d-x's memory management approach. This simplifies the memory management of elements in the container, which is mainly reflected in two aspects.
The retain () method will be executed to add the left value newly added to the container in any form so that the reference count is + 1. Taking Vector as an example, such as copy constructor, value assignment operator, pushBack, replace, insert:
Class MyClass: public Ref
{};
Void testVector ()
{
Auto c1 = new MyClass ();
C1-> autorelease ();
Auto c2 = new MyClass ();
C2-> autorelease ();
CCLOG ("reference count c1: % d & c2: % d", c1-> getReferenceCount (), c2-> getReferenceCount ());
Vector V1;
V1.pushBack (c1 );
V1.insert (1, c2 );
CCLOG ("reference count c1: % d & c2: % d", c1-> getReferenceCount (), c2-> getReferenceCount ());
V1.popBack ();
CCLOG ("reference count c1: % d & c2: % d", c1-> getReferenceCount (), c2-> getReferenceCount ());
Vector V2 = Vector (V1 );
CCLOG ("reference count c1: % d & c2: % d", c1-> getReferenceCount (), c2-> getReferenceCount ());
Vector V3 = v1;
CCLOG ("reference count c1: % d & c2: % d", c1-> getReferenceCount (), c2-> getReferenceCount ());
}
The output is:
Cocos2d: reference count c1: 1 & c2: 1
Cocos2d: reference count c1: 2 & c2: 2
Cocos2d: reference count c1: 2 & c2: 1
Cocos2d: reference count c1: 3 & c2: 1
Cocos2d: reference count c1: 4 & c2: 1
The left value removed from the container in any form will be executed using the release () method to make reference count-1. For example, the destructor, erase, popBack, replace, and clear are tested by the reader.
These operations, in the same statement can clearly calculate its impact on the element reference count, so that the elements in the container can be automatically managed according to the memory management rules of the Cocos2d-x. However, the subscript operator [] returns a left value T &. The impact on container elements in the same statement is immeasurable. For example, the statement:
V3 [0]-> release ();
It will affect the memory management of elements in the container, so the container of the Cocos2d-x does not provide the subscript operator, and we should use the at method to return a right value:
Template
Class CC_DLL Vector
{
Public:
/** Returns the element at position 'index' in the vector .*/
T at (ssize_t index) const
{
CCASSERT (index> = 0 & index <size (), "index out of range in getObjectAtIndex ()");
Return _ data [index];
}
};
1.2.6.3 mobile Semantics
Finally, the new container class uses the C ++ 11 new moving (move, see reference 6, 7) semantics for the right value. They implement the mobile copy function and the mobile value assignment operator, in this way, some unnecessary temporary variables are generated and copied when the right value is used:
Template
Class CC_DLL Vector
{
Public:
/** Move constructor */
Vector (Vector & other)
{
Static_assert (std: is_convertible : Value, "Invalid Type for cocos2d: Vector !");
CCLOGINFO ("In the move constructor of Vector !");
_ Data = std: move (other. _ data );
}
/** Move assignment operator */
Vector & operator = (Vector & other)
{
If (this! = & Other ){
CCLOGINFO ("In the move assignment operator !");
Clear ();
_ Data = std: move (other. _ data );
}
Return * this;
}
};
For example:
Vector GetVector ()
{
Auto c1 = new MyClass ();
C1-> autorelease ();
Auto c2 = new MyClass ();
C2-> autorelease ();
Vector V1;
V1.pushBack (c1 );
V1.insert (0, c2 );
CCLOG ("reference count c1: % d & c2: % d", c1-> getReferenceCount (), c2-> getReferenceCount ());
Return v1;
}
Void testVectorMove ()
{
Vector V2 = Vector (GetVector ());
CCLOG ("reference count c1: % d & c2: % d", v2.at (1)-> getReferenceCount (), v2.at (0)-> getReferenceCount ());
Vector V3 = getVector ();
CCLOG ("reference count c1: % d & c2: % d", v3.at (1)-> getReferenceCount (), v3.at (0)-> getReferenceCount ());
}
The output is:
Cocos2d: reference count c1: 2 & c2: 2
Cocos2d: reference count c1: 2 & c2: 2
Cocos2d: reference count c1: 2 & c2: 2
Cocos2d: reference count c1: 2 & c2: 2
It can be seen that the above example uses the mobile copy function and the mobile value assignment operator, which reduces the generation and destruction of unnecessary temporary variables.
Main thread for COCOS2D-X
In game development, it is often difficult to design a parallel system for the game object model. On the one hand, there will be a lot of mutual dependencies between game objects, and the data produced by game objects may also be mutually dependent on those produced by multiple engine subsystems. On the other hand, Game objects can communicate with other game objects, and sometimes communicate with each other multiple times in the update loop. The mode of communication is unpredictable and affected by player input. This makes it difficult to update game objects in multiple threads.
Although, theoretically, some architectures can be designed to support parallel updating of Game objects. However, from the developer's ease of use perspective, most game engines are still dominated by single threads. However, in the lower-layer engine subsystem, some parallelism can be implemented so that it does not affect the upper-layer game object model. For example, many game engines currently separate rendering from the game engine, it can be drawn in different threads.
Cocos2d-x is still a single-threaded game engine, which makes it almost unnecessary to consider the thread security of game object updates. However, we still need to pay attention to some situations such as network requests, asynchronous file loading, or asynchronous processing of some logical algorithms.
3.6.1 asynchronous processing results in the main thread
Some methods must be executed in the main thread, such as GL-related methods. In other cases, in order to ensure the thread security of Ref object reference counting, we should also perform these operations in the main thread. Scheduler provides a simple mechanism to execute a method on the main thread:
Void schedread: extends mfunctionincosthread (const std: function & function)
{
_ Optional mmutex. lock ();
_ FunctionsToPerform. push_back (function );
_ Optional mmutex. unlock ();
}
First, register a method pointer with schedle. Scheduler stores an array of method pointers to be executed in the main thread. After all the systems of the current frame or custom schedule are executed, scheduler checks the array and executes the method:
Void Scheduler: update (float dt)
{
If (! _ FunctionsToPerform. empty ()){
_ Optional mmutex. lock ();
// Fixed #4123: Save the callback functions, they must be invoked after '_ future mmutex. unlock ()', otherwise if new functions are added in callback, it will cause thread deadlock.
Auto temp = _ functionsToPerform;
_ FunctionsToPerform. clear ();
_ Optional mmutex. unlock ();
For (const auto & function: temp ){
Function ();
}
}
}
With this mechanism, we can transfer a method to the main thread for execution. Note that these methods are executed by the main thread after all the systems or custom schedule, that is, before the UI tree traversal.
3.6.2 file asynchronously loaded
In the above mechanism, all the methods registered with Scheduler will be executed at the end of the frame. For some simple algorithms, this is no problem. The function list on the left is shown. However, for some time-consuming computing, in order not to affect the game performance, we need to distribute a series of time-consuming methods to each frame for execution.
After the asynchronous loading of the Cocos2d-x texture is complete, the texture needs to be uploaded to the GL memory, so this transfer process must be executed in the main thread. However, the glTexImage2D command for texture uploading is a time-consuming operation. If multiple images are loaded at the same time, these textures must be uploaded to the GL memory at the same frame, this may cause choppy UI and lead to poor user experience.
Therefore, the Cocos2d-x texture asynchronous loading callback uses a custom schedule for processing, inside the schedule, check the texture that has been loaded, each frame handles a texture, log out of schedule until all textures are processed. The file list on the right of the execution of the texture in the main thread:
One-frame execution and cross-frame execution
TextureCache registers an update callback addImageAsyncCallBack to Scheduler:
Void TextureCache: addImageAsyncCallBack (float dt)
{
// The image is generated in loading thread
Std: deque * ImagesQueue = _ imageInfoQueue;
_ ImageInfoMutex. lock ();
If (imagesQueue-> empty ())
{
_ ImageInfoMutex. unlock ();
}
Else
{
ImageInfo * imageInfo = imagesQueue-> front ();
ImagesQueue-> pop_front ();
_ ImageInfoMutex. unlock ();
AsyncStruct * asyncStruct = imageInfo-> asyncStruct;
Image * image = imageInfo-> image;
Const std: string & filename = asyncStruct-> filename;
Texture2D * texture = nullptr;
If (image)
{
// Generate texture in render thread
Texture = new Texture2D ();
Texture-> initWithImage (image );
# If CC_ENABLE_CACHE_TEXTURE_DATA
// Cache the texture file name
VolatileTextureMgr: addImageTexture (texture, filename );
# Endif
// Cache the texture. retain it, since it is added in the map
_ Textures. insert (std: make_pair (filename, texture ));
Texture-> retain ();
Texture-> autorelease ();
}
Else
{
Auto it = _ textures. find (asyncStruct-> filename );
If (it! = _ Textures. end ())
Texture = it-> second;
}
AsyncStruct-> callback (texture );
If (image)
{
Image-> release ();
}
Delete asyncStruct;
Delete imageInfo;
-_ AsyncRefCount;
If (0 = _ asyncRefCount)
{
Director: getInstance ()-> getScheduler ()-> unschedule (schedule_selector (TextureCache: addImageAsyncCallBack), this );
}
}
}
When an asynchronous file loading request is initiated to TextureCache, TextureCache registers an update callback addImageAsyncCallback to Scheduler and starts a new thread to asynchronously load files. When a file in a new thread is loaded, the texture data is stored in _ imageInfoQueue. When each frame of the main thread is updated and called back, check whether there is data, if yes, the texture data is cached in the TextureCache, uploaded to the GL memory, and deleted from _ imageInfoQueue. Finally, when all files are loaded, the update callback is canceled.
3.6.3 asynchronous Unit Testing
The execution of all logic algorithms on the main thread greatly reduces Program Complexity and allows you to use multiple threads freely in some aspects. However, this callback mechanism for Cocos2d-x also makes unit testing difficult because it depends on the main loop of the Cocos2d-x.
A unit test is usually used to test a synchronous method. If you execute this method, you will know the running result. The unit test may not depend on too many contexts, in fact, too many contexts make unit tests difficult.
For Asynchronous methods, people add a "wait time" to the unit test to listen to the callback function to modify the value of a Boolean variable and notify the callback to complete the unit test method. With this access, you can test the Asynchronous Method.
Then, asynchronous callbacks in the Cocos2d-x need to be driven by game loops. In addition to listening to asynchronous callbacks, unit tests also need to drive the game loop to execute Schedule, which makes unit tests difficult. In the last chapter of this book, we will provide a solution that enables it to test the "Asynchronous callback" in the Cocos2d-x ".