Cocos2d-x 3.0多线程异步加载资源实例

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画面:


代码如下:

// AppDelegate.cpp
bool AppDelegate::applicationDidFinishLaunching() {
    … …
    FlashScene* scene = FlashScene::create();
    pDirector->runWithScene(scene);

return true;
}

在FlashScene init时,我们创建一个Resource Load Thread,我们用一个ResourceLoadIndicator作为渲染线程与Worker线程之间交互的媒介。


代码如下:

//FlashScene.h

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后,切换到接下来的游戏开始场景:


代码如下:

void FlashScene::updateScene(float dt)
{
    if (getResourceLoadIndicator()) {
        Director::getInstance()->replaceScene(
                              WelcomeScene::create());
    }
}

到此,FlashScene的初始设计和实现完成了。Run一下试试吧。

二、解决崩溃问题

在GenyMotion的4.4.2模拟器上,游戏运行的结果并没有如我期望,FlashScreen显现后游戏就异常崩溃退出了。

通过monitor分析游戏的运行日志,我们看到了如下一些异常日志:


代码如下:

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

很是奇怪啊,我们在创建线程时,明明设置了 PTHREAD_CREATE_DETACHED属性了啊:

代码如下:

pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);

怎么还会出现这个问题,而且居然有三条日志。翻看了一下引擎内核的代码TextureCache::addImageAsync,在线程创建以及线程主函数中也没有发现什么特别的设置。为何内核可以创建线程,我自己创建就会崩溃呢。Debug多个来回,问题似乎聚焦在resource_load_thread_entry中执行的任务。在我的代码里,我利用SimpleAudioEngine加载了音效资源、利用UserDefault读取了一些持久化的数据,把这两个任务去掉,游戏就会进入到下一个环节而不会崩溃。

SimpleAudioEngine和UserDefault能有什么共同点呢?Jni调用。没错,这两个接口底层要适配多个平台,而对于Android 平台,他们都用到了Jni提供的接口去调用Java中的方法。而Jni对多线程是有约束的。Android开发者官网上有这么一段话:


代码如下:

All threads are Linux threads, scheduled by the kernel. They're usually started from managed code (using Thread.start), 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.

由此看来pthread_create创建的新线程默认情况下是不能进行Jni接口调用的,除非Attach到Vm,获得一个JniEnv对象,并且在线 程exit前要Detach Vm。好,我们来尝试一下,Cocos2d-x引擎提供了一些JniHelper方法,可以方便进行Jni相关操作。

代码如下:

#if (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID)
#include "platform/android/jni/JniHelper.h"
#include <jni.h>
#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;
}

关于什么是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线程加载纹理 资源有关了:

代码如下:

SpriteFrameCache::getInstance()->addSpriteFramesWithFile("All-Sprites.plist");

我们通过碎图压缩到一张大纹理的方式建立SpriteFrame,这是Cocos2d-x推荐的优化手段。但要想找到这个问题的根源,还得看monitor日志。我们的确发现了一些异常日志:

代码如下:

libEGL: call to OpenGL ES API with no current context (logged once per thread)

通过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,我们来修改一下代码:


代码如下:

static void* resource_load_thread_entry(void* param)
{
    … …
    allSpritesImage = new Image();
    allSpritesImage->initWithImageFile("All-Sprites.png");
    … …
}

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());
    }
}

完成这一修改后,游戏画面就变得一切正常了,多线程资源加载机制正式生效。

(0)

相关推荐

  • android开发教程之handle实现多线程和异步处理

    这次浅谈一下Handler,为什么会出现Handler这个功能特性呢?首先,在之前的基本控件,基本都是在Activity的onCreate(Bundle savedInstanceState)方法中调用和处理的,但是,在有些情况,比如在网络上下载软件等一些需要等待响应时间比较长的操作,如果同样放在Activity的该方法中的话,那么在执行该方法的时候,整个Activity是不可动的,用户只能干等着,这样的用户体验是十分差的,这种处理方式带来的最好结果是等待了一段时间后,得到了想要的结果,不好的情

  • PHP 异步执行方法,模拟多线程的应用分析

    PHP 本身没有多线程的东西,但可以曲线的办法来造就出同样的效果,比如多进程的方式来达到异步调用,只限于命令模式.另外还有一种更简单的方式可用于 Web 程序中,那就是用 fsockopen().fputs() 来请求一个 URL, 而无需等待返回,如果你在那个被请求的页面(URL)中做些事情就相当于异步了.关键代码如下: 复制代码 代码如下: <?php  $fp = fsockopen('localhost',80,&$errno,&$errstr,5);    if(!$fp)

  • 个人对于异步和多线程的关系的理解分享

    个人的理解是这样的: 1.  异步通信的意思是,当A发送完消息之后,不等待B的回应,继续执行之后的程序.在将来的某个时刻,A再来检查是否收到B的回应. 异步就是彼此独立,在等待某事件的过程中继续做自己的事,不需要等待这一事件完成后再工作. 2. 多线程是程序设计的逻辑层概念,它是进程中并发运行的一段代码.多线程可以实现线程间的切换执行. 3. 异步和同步是相对的,同步就是顺序执行,执行完一个再执行下一个,需要等待.协调运行.线程就是实现异步的一个方式.异步是让调用方法的主线程不需要同步等待另一线

  • Android多线程及异步处理问题详细探讨

    1.问题提出 1)为何需要多线程? 2)多线程如何实现? 3)多线程机制的核心是啥? 4)到底有多少种实现方式? 2.问题分析 1)究其为啥需要多线程的本质就是异步处理,直观一点说就是不要让用户感觉到"很卡". eg:你点击按钮下载一首歌,接着该按钮一直处于按下状态,那么用户体验就很差. 2)多线程实现方式implements Runnable 或 extends Thread 3)多线程核心机制是Handler 4)提供如下几种实现方式 --1-–Handler ----------

  • 实现PHP多线程异步请求的3种方法

    在网上看过很多版本的PHP异步请求方法,这里简单总结几个常用方法分享给大家 1.用CURL实现一步请求 CURL扩展是我们在开发过程中最常用的一种方法,他是一个强大的HTTP命令行工具,可以模拟POST/GET等HTTP请求,然后得到和提取数据,显示在"标准输出"(stdout)上面. 示例: 复制代码 代码如下: <?php $cl = curl_init(); $curl_opt = array(CURLOPT_URL, 'http://www.uncletoo.com/de

  • 异步/多线程/任务/并行编程之一:如何选择合适的多线程模型?

    异步.多线程.任务.并行编程之一:选择合适的多线程模型 本篇概述: @FCL4.0中已经存在的线程模型,以及它们之间异同点: @多线程编程模型的选择. 1:异步.多线程.任务.并行的本质 这四个概念对应在CLR中的本质,本质都是多线程. 异步,简单的讲就是BeginInvoke.EndInvoke模式,它在CLR内部线程池进行管理: 多线程,体现在C#中,可以由类型Thread发起.也可以由ThreadPool发起.前者不受CLR线程池管理,后者则是.FCL团队为了各种编程模型的方便,还另外提供

  • Cocos2d-x 3.0多线程异步加载资源实例

    Cocos2d-x从2.x版本到上周刚刚才发布的Cocos2d-x 3.0 Final版,其引擎驱动核心依旧是一个单线程的"死循环",一旦某一帧遇到了"大活儿",比如Size很大的纹理资源加载或网络IO或大量计算,画面将 不可避免出现卡顿以及响应迟缓的现象.从古老的Win32 GUI编程那时起,Guru们就告诉我们:别阻塞主线程(UI线程),让Worker线程去做那些"大活儿"吧. 手机游戏,即便是休闲类的小游戏,往往也涉及大量纹理资源.音视频资

  • ajax异步加载图片实例分析

    本文实例讲述了ajax异步加载图片的方法.分享给大家供大家参考,具体如下: 图片一般比较大,所以他们都是在基本网页加载后才逐渐加载上的,整个加载的过程非常不雅观,或者是从模糊逐渐变清晰,或者是从上往下拓展开(当然你也可以认为这些都是不错的特效).如果是通过定时更换img的src属性来实现图片的动态更换,由此带来的闪烁更让它难以接受,这可不是用alt属性就能让人愉快的. 联系时下比较热门的,号称"无"刷新的AJAX技术,利用XMLHttpRequest对象发起异步请求,待图像加载完毕再动

  • jquery zTree异步加载简单实例分享

    首先提供Ztree官方网站http://www.ztree.me. Ztree是一个使用jQuery实现的JSP页面的各种功能树,本文介绍一个异步获取数据到下拉树的实现方式. 当前版本Ztree 3.5.01 simple.html 复制代码 代码如下: <!DOCTYPE html> <html> <head> <title>simple.html</title> <meta http-equiv="keywords"

  • jquery zTree异步加载简单实例讲解

    本文实例讲解了jquery zTree异步加载,分享给大家供大家参考,具体内容如下 web.xml中Servlet配置如下: <?xml version="1.0" encoding="UTF-8"?> <web-app version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XM

  • yepnope.js 异步加载资源文件

    典型代码示例 复制代码 代码如下: yepnope({ test : Modernizr.geolocation, yep : 'normal.js', nope : ['polyfill.js', 'wrapper.js'] }); 当Modernizr.geolocation为真时,加载yep项也就是"normal.js",否则加载nope项--可以同时加载多个文件. yepnope和现有的xxx script loader有什么区别? 个人认为主要 是这两点: 可以同时处理jav

  • Android App中实现图片异步加载的实例分享

    一.概述 一般大量图片的加载,比如GridView实现手机的相册功能,一般会用到LruCache,线程池,任务队列等:那么异步消息处理可以用哪呢? 1.用于UI线程当Bitmap加载完成后更新ImageView 2.在图片加载类初始化时,我们会在一个子线程中维护一个Loop实例,当然子线程中也就有了MessageQueue,Looper会一直在那loop停着等待消息的到达,当有消息到达时,从任务队列按照队列调度的方式(FIFO,LIFO等),取出一个任务放入线程池中进行处理. 简易的一个流程:当

  • Android 异步加载图片分析总结

    研究了android从网络上异步加载图像,现总结如下: (1)由于android UI更新支持单一线程原则,所以从网络上取数据并更新到界面上,为了不阻塞主线程首先可能会想到以下方法. 在主线程中new 一个Handler对象,加载图像方法如下所示 复制代码 代码如下: private void loadImage(final String url, final int id) { handler.post(new Runnable() { public void run() { Drawable

  • Android 3.0引入的异步加载机制Loader

    Loader是谷歌在Android 3.0引入的异步加载机制,能够对数据异步加载并显示到Activity或Fragment上,使用者不需要对数据的生命周期进行管理,而是交给Loader机制来管理. 使用Loader的优点 假如我们需要从网络上获取数据,通常的做法是使用子线程Thread+Handler或者是使用AsyncTask来处理. Thread+Handler方法实现起来简单直观,不过会麻烦点,需要自己实现Handler子类,创建线程,还要管理Handler的生命周期. AsyncTask

  • Android 异步加载图片的实例代码

    异步加载图片的主要流程是进行判断缓存中是否存在图片,如果存在则直接返回,如果不存在则进行下载并进行缓存. 以下是建立一个异步下载类: 复制代码 代码如下: /** * User: Tom * Date: 13-5-13 * Time: 下午8:07 */public class AsnycImageLoader { //定义一个HashMap进行存放缓存的Image key为String Value为一个弱引用的一个资源文件    // 图片 为了方便JAVA的回收    private Map

  • Android实现从缓存中读取图片与异步加载功能类

    本文实例讲述了Android实现从缓存中读取图片与异步加载功能类.分享给大家供大家参考,具体如下: 在新浪微博的微博列表中的图片,为了加速其显示也为了加快程序的响应,可以参考该图片异步加载类实现. public class AsyncImageLoader { //SoftReference是软引用,是为了更好的为了系统回收变量 private HashMap<String, SoftReference<Drawable>> imageCache; public AsyncImag

随机推荐