cocos2d-x从2.x版本到上周刚刚才发布的cocos2d-x 3.0 final版,其引擎驱动核心依旧是一个单线程的“死循环”,一旦某一帧遇到了“大活儿”,比如size很大的纹理资源加载或网络io或大量计算,画面将 不可避免出现卡顿以及响应迟缓的现象。从古老的win32 gui编程那时起,guru们就告诉我们:别阻塞主线程(ui线程),让worker线程去做那些“大活儿”吧。
手机游戏,即便是休闲类的小游戏,往往也涉及大量纹理资源、音视频资源、文件读写以及网络通信,处理的稍有不甚就会出现画面卡顿,交互不畅的情况。虽然引擎在某些方面提供了一些支持,但有些时候还是自己祭出worker线程这个法宝比较灵活,下面就以cocos2d-x 3.0 final版游戏初始化为例(针对android平台),说说如何进行多线程资源加载。
我们经常看到一些手机游戏,启动之后首先会显示一个带有公司logo的闪屏画面(flash screen),然后才会进入一个游戏welcome场景,点击“开始”才正式进入游戏主场景。而这里flash screen的展示环节往往在后台还会做另外一件事,那就是加载游戏的图片资源,音乐音效资源以及配置数据读取,这算是一个“障眼法”吧,目的就是提高用 户体验,这样后续场景渲染以及场景切换直接使用已经cache到内存中的数据即可,无需再行加载。
一、为游戏添加flashscene
在游戏app初始化时,我们首先创建flashscene,让游戏尽快显示flashscene画面:
return true;
}
在flashscene init时,我们创建一个resource load thread,我们用一个resourceloadindicator作为渲染线程与worker线程之间交互的媒介。
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;
};
// flashscene.cpp
bool flashscene::init()
{
bool bret = false;
do {
cc_break_if(!ccscene::init());
size winsize = director::getinstance()->getwinsize();
//flashscene自己的资源只能同步加载了
sprite *bg = sprite::create("flashsceenbg.png");
cc_break_if(!bg);
bg->setposition(ccp(winsize.width/2, winsize.height/2));
this->addchild(bg, 0);
this->schedule(schedule_selector(flashscene::updatescene)
, 0.01f);
//start the resource loading thread
rli.load_done = false;
rli.context = (void*)this;
pthread_mutex_init(&rli.mutex, 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(
"all-sprites.plist");
… …
//set loading done
scene->setresourceloadindicator(true);
return null;
}
bool flashscene::getresourceloadindicator()
{
bool flag;
pthread_mutex_lock(&rli.mutex);
flag = rli.load_done;
pthread_mutex_unlock(&rli.mutex);
return flag;
}
void flashscene::setresourceloadindicator(bool flag)
{
pthread_mutex_lock(&rli.mutex);
rli.load_done = flag;
pthread_mutex_unlock(&rli.mutex);
return;
}
我们在定时器回调函数中对indicator标志位进行检查,当发现加载ok后,切换到接下来的游戏开始场景:
到此,flashscene的初始设计和实现完成了。run一下试试吧。
二、解决崩溃问题
在genymotion的4.4.2模拟器上,游戏运行的结果并没有如我期望,flashscreen显现后游戏就异常崩溃退出了。
通过monitor分析游戏的运行日志,我们看到了如下一些异常日志:
很是奇怪啊,我们在创建线程时,明明设置了 pthread_create_detached属性了啊:
怎么还会出现这个问题,而且居然有三条日志。翻看了一下引擎内核的代码texturecache::addimageasync,在线程创建以及线程主函数中也没有发现什么特别的设置。为何内核可以创建线程,我自己创建就会崩溃呢。debug多个来回,问题似乎聚焦在resource_load_thread_entry中执行的任务。在我的代码里,我利用simpleaudioengine加载了音效资源、利用userdefault读取了一些持久化的数据,把这两个任务去掉,游戏就会进入到下一个环节而不会崩溃。
simpleaudioengine和userdefault能有什么共同点呢?jni调用。没错,这两个接口底层要适配多个平台,而对于android 平台,他们都用到了jni提供的接口去调用java中的方法。而jni对多线程是有约束的。android开发者官网上有这么一段话:
由此看来pthread_create创建的新线程默认情况下是不能进行jni接口调用的,除非attach到vm,获得一个jnienv对象,并且在线 程exit前要detach vm。好,我们来尝试一下,cocos2d-x引擎提供了一些jnihelper方法,可以方便进行jni相关操作。
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;
}
关于什么是javavm,什么是jnienv,android developer官方文档中是这样描述的:
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.
三、解决黑屏问题
上面的代码成功解决了线程崩溃的问题,但问题还没完,因为接下来我们又遇到了“黑屏”事件。所谓的“黑屏”,其实并不是全黑。但进入游戏 welcomscene时,只有scene中的labelttf实例能显示出来,其余sprite都无法显示。显然肯定与我们在worker线程加载纹理 资源有关了:
我们通过碎图压缩到一张大纹理的方式建立spriteframe,这是cocos2d-x推荐的优化手段。但要想找到这个问题的根源,还得看monitor日志。我们的确发现了一些异常日志:
通过google得知,只有renderer thread才能进行egl调用,因为egl的context是在renderer thread创建的,worker thread并没有egl的context,在进行egl操作时,无法找到context,因此操作都是失败的,纹理也就无法显示出来。要解决这个问题就 得查看一下texturecache::addimageasync是如何做的了。
texturecache::addimageasync只是在worker线程进行了image数据的加载,而纹理对象texture2d instance则是在addimageasynccallback中创建的。也就是说纹理还是在renderer线程中创建的,因此不会出现我们上面的 “黑屏”问题。模仿addimageasync,我们来修改一下代码:
void flashscene::updatescene(float dt)
{
if (getresourceloadindicator()) {
// construct texture with preloaded images
texture2d *allspritestexture = texturecache::getinstance()->
addimage(allspritesimage, "all-sprites.png");
allspritesimage->release();
spriteframecache::getinstance()->addspriteframeswithfile(
"all-sprites.plist", allspritestexture);
director::getinstance()->replacescene(welcomescene::create());
}
}
完成这一修改后,游戏画面就变得一切正常了,多线程资源加载机制正式生效。
如对本文有疑问, 点击进行留言回复!!
android开发实例(activity生命周期和启动模式、IPC机制)
realme真我V5 5G发布会在哪看 8月3号realme真我V5直播地址入口
Android Studio中gradle文件下载慢解决办法
Android 应用进程 ServiceManager 的实现
Shimmer-Android使用FrameLayout给视图添加微光效果
网友评论