From the version of Cocos2d-x to the Cocos2d-x 3.0 Final version released just last week, its engine driver core is still a single-threaded "dead loop". Once a certain frame encounters a "big job", such as large-scale texture resource loading or network IO or large-scale computing, the screen will inevitably have lag and slow response. Since the ancient Win32 GUI programming, Guru have told us: Don't block the main thread (UI thread), let the Worker thread do those "big jobs".
Mobile games, even casual games, often involve a large number of texture resources, audio and video resources, file reading and writing, and network communication. If the processing is not done much, the screen will be stuck and the interaction will be poor. Although the engine provides some support in some aspects, sometimes it is more flexible to use the Worker thread magic weapon. Let’s take the initialization of the Cocos2d-x 3.0 Final version of the game as an example (for the Android platform), and talk about how to load multi-threaded resources.
We often see some mobile games. After starting, a flash screen with the company logo will be displayed, and then a game Welcome scene will be entered. Click "Start" to officially enter the main game scene. The display of Flash Screen often does another thing in the background, that is, load the game's picture resources, music and sound effects resources, and configuration data reading. This is a "dissue" and the purpose is to improve the user experience. In this way, subsequent scene rendering and scene switching can directly use the data that has been cached into memory, without loading again.
1. Add FlashScene to the game
When initializing the game app, we first create FlashScene to let the game display the FlashScene screen as soon as possible:
//
bool AppDelegate::applicationDidFinishLaunching() {
… …
FlashScene* scene = FlashScene::create();
pDirector->runWithScene(scene);
return true;
}
When FlashScene init, we create a Resource Load Thread, and we use a ResourceLoadIndicator as the medium for interaction between the rendering thread and the Worker thread.
//
struct ResourceLoadIndicator {
pthread_mutex_t mutex;
bool load_done;
void *context;
};
class FlashScene : public Scene
{
public:
FlashScene(void);
~FlashScene(void);
virtual bool init();
CREATE_FUNC(FlashScene);
bool getResourceLoadIndicator();
void setResourceLoadIndicator(bool flag);
private:
void updateScene(float dt);
private:
ResourceLoadIndicator rli;
};
//
bool FlashScene::init()
{
bool bRet = false;
do {
CC_BREAK_IF(!CCScene::init());
Size winSize = Director::getInstance()->getWinSize();
//FlashScene's own resources can only be loaded synchronously
Sprite *bg = Sprite::create("");
CC_BREAK_IF(!bg);
bg->setPosition(ccp(/2, /2));
this->addChild(bg, 0);
this->schedule(schedule_selector(FlashScene::updateScene)
, 0.01f);
//start the resource loading thread
rli.load_done = false;
= (void*)this;
pthread_mutex_init(&, NULL);
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
pthread_t thread;
pthread_create(&thread, &attr,
resource_load_thread_entry, &rli);
bRet=true;
} while(0);
return bRet;
}
static void* resource_load_thread_entry(void* param)
{
AppDelegate *app = (AppDelegate*)Application::getInstance();
ResourceLoadIndicator *rli = (ResourceLoadIndicator*)param;
FlashScene *scene = (FlashScene*)rli->context;
//load music effect resource
… …
//init from config files
… …
//load images data in worker thread
SpriteFrameCache::getInstance()->addSpriteFramesWithFile(
"");
… …
//set loading done
scene->setResourceLoadIndicator(true);
return NULL;
}
bool FlashScene::getResourceLoadIndicator()
{
bool flag;
pthread_mutex_lock(&);
flag = rli.load_done;
pthread_mutex_unlock(&);
return flag;
}
void FlashScene::setResourceLoadIndicator(bool flag)
{
pthread_mutex_lock(&);
rli.load_done = flag;
pthread_mutex_unlock(&);
return;
}
We check the indicator flag in the timer callback function. When we find that ok is loaded, switch to the next game start scene:
void FlashScene::updateScene(float dt)
{
if (getResourceLoadIndicator()) {
Director::getInstance()->replaceScene(
WelcomeScene::create());
}
}
At this point, the initial design and implementation of FlashScene has been completed. Let's try it.
2. Solve the crash problem
On GenyMotion's 4.4.2 emulator, the game did not run as I expected. After FlashScreen appears, the game crashed and exited.
Through the monitor analyzing the game's running log, we see some exception logs as follows:
threadid=24: thread exiting, not yet detached (count=0)
threadid=24: thread exiting, not yet detached (count=1)
threadid=24: native thread exited without detaching
It's very strange. When we created the thread, we clearly set the PTHREAD_CREATE_DETACHED attribute:
Why does this problem still occur? There are actually three logs. I looked through the engine kernel code TextureCache::addImageAsync, and I found no special settings in thread creation and thread main functions. Why can the kernel create threads? If I create it myself, it will crash. Debug multiple rounds and the problem seems to focus on tasks performed in resource_load_thread_entry. In my code, I used SimpleAudioEngine to load the sound effects resources, and used UserDefault to read some persistent data. Remove these two tasks and the game will enter the next step without crashing.
What can SimpleAudioEngine and UserDefault have in common? Jni calls. That's right, these two interfaces need to be adapted to multiple platforms, and for Android platforms, they both use the interface provided by Jni to call methods in Java. Jni has constraints on multi-threading. There is a passage on the official website of Android developers:
All threads are Linux threads, scheduled by the kernel. They're usually started from managed code (using ), but they can also be created elsewhere and then attached to the JavaVM. For example, a thread started with pthread_create can be attached with the JNI AttachCurrentThread or AttachCurrentThreadAsDaemon functions. Until a thread is attached, it has no JNIEnv, and cannot make JNI calls.
From this, it seems that the new thread created by pthread_create cannot make Jni interface calls by default, unless Attach to VM, a JniEnv object is obtained, and the online process needs to detach VM before exiting. OK, let's try it out. The Cocos2d-x engine provides some JniHelper methods, which can facilitate Jni-related operations.
#if (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID)
#include "platform/android/jni/"
#include <>
#endif
static void* resource_load_thread_entry(void* param)
{
… …
JavaVM *vm;
JNIEnv *env;
vm = JniHelper::getJavaVM();
JavaVMAttachArgs thread_args;
thread_args.name = "Resource Load";
thread_args.version = JNI_VERSION_1_4;
thread_args.group = NULL;
vm->AttachCurrentThread(&env, &thread_args);
… …
//Your Jni Calls
… …
vm->DetachCurrentThread();
… …
return NULL;
}
Regarding what is JavaVM and what is JniEnv, it is described in the official Android Developer documentation as follows:
The JavaVM provides the "invocation interface" functions, which allow you to create and destroy a JavaVM. In theory you can have multiple JavaVMs per process, but Android only allows one.
The JNIEnv provides most of the JNI functions. Your native functions all receive a JNIEnv as the first argument.
The JNIEnv is used for thread-local storage. For this reason, you cannot share a JNIEnv between threads.
3. Solve the black screen problem
The above code successfully solved the problem of thread crash, but the problem is not over yet because we encountered a "black screen" event next. The so-called "black screen" is actually not all black. But when entering the game WelcomeScene, only the LabelTTF instance in Scene can be displayed, and the rest of the Sprite cannot be displayed. Obviously it must be related to our loading of texture resources in the Worker thread:
We create a SpriteFrame by crushing the graph into a large texture, which is an optimization method recommended by Cocos2d-x. But to find the root cause of this problem, you still have to look at the monitor log. We did find some exception logs:
Google knows that only Renderer Thread can make egl calls, because egl context is created in Renderer Thread, and Worker Thread does not have EGL context. When performing egl operations, the context cannot be found, so the operations fail and the texture cannot be displayed. To solve this problem, you have to check out how TextureCache::addImageAsync is done.
TextureCache::addImageAsync just loads image data in the worker thread, while the texture object Texture2D instance is created in addImageAsyncCallBack. In other words, the texture is still created in the Renderer thread, so the "black screen" problem above will not occur. To imitate addImageAsync, let's modify the code:
static void* resource_load_thread_entry(void* param)
{
… …
allSpritesImage = new Image();
allSpritesImage->initWithImageFile("");
… …
}
void FlashScene::updateScene(float dt)
{
if (getResourceLoadIndicator()) {
// construct texture with preloaded images
Texture2D *allSpritesTexture = TextureCache::getInstance()->
addImage(allSpritesImage, "");
allSpritesImage->release();
SpriteFrameCache::getInstance()->addSpriteFramesWithFile(
"", allSpritesTexture);
Director::getInstance()->replaceScene(WelcomeScene::create());
}
}
After completing this modification, the game screen becomes normal and the multi-threaded resource loading mechanism takes effect.