分类 技术志 下的文章

Cocos2d-x 3.0rc2针对Android平台的变动

Hello, Cocos2d-x 3.0》一文发出后没多久,我就迫不及待地将手头的一个习作尝试从2.2.2版本迁移到3.0rc0引擎上。

核心代码迁移相对顺利,大致流程如下:

  * 创建项目

    1) cd cocos2d-x-3.0rc0;
    2) 执行setup.py,设置引擎依赖的环境变量,脚本会将COCOS_CONSOLE_ROOTANT_ROOT写入到~/.bash_profile中; 执行source ~/.bash_profile使得环境变量生效;
    3) 在cocos2d-x-3.0rc0下建立projects目录;
    4) 利用cocos2d-console工具建立新项目: cocos new GameDemo -p com.tonybai.game.gamedemo -l cpp -d ./projects
    5) cd ./projects/GameDemo,我们可以看到项目目录结构如下:
            bin/  Classes/  CMakeLists.txt  cocos2d/  proj.android/ 
      proj.ios_mac/  proj.linux/  proj.win32/  Resources/

    6) 执行cocos compile -p android -j 4  –ap 19 -m release,这个Demo的apk就会被生成,大致就是一个cpp-empty-test;
   
  * 代码移植
    
     代码移植的主要工作包括:
    1) 改名
            带有CC前缀的类名大都要将前缀去掉;
            各主要类的单例方法sharedXXXX都改为getInstance;

    2) 菜单、按钮事件处理       
            由menu_selector(GameScene::menuStartCallback) 改为CC_CALLBACK_1(GameScene::menuStartCallback, this);

    3) 触屏事件处理

            在Cocos2d-x 2.2.2中,我们直接使用Layer的setTouchEnabled(true),并Override 三个触屏事件处理函数;
            在新版引擎中,我们需要建立事件Listener,并将Listener注册到全局EventDispatcher中,诸如:

                auto listener = EventListenerTouchOneByOne::create();
        listener->setSwallowTouches(true);
        listener->onTouchBegan = CC_CALLBACK_2(GameLayer::onTouchBegan, this);
        listener->onTouchMoved = CC_CALLBACK_2(GameLayer::onTouchMoved, this);
        listener->onTouchEnded = CC_CALLBACK_2(GameLayer::onTouchEnded, this);
        Director::getInstance()->getEventDispatcher()->addEventListenerWithSceneGraphPriority(listener, this);

            然后将这里的三个事件处理方法实现出来即可。

核心功能迁移后,GameDemo在genymotion 4.4 Android模拟器以及真机上都能正常运行,在模拟器上能保持40左右的帧率,在真机上帧率一直在60左右。玩了一会后,感觉引擎渲染性能的确有提升, 而且这种提升是可以在真机上直观感受到的。

不过好景不长,我又尝试将GameDemo在genymotion 2.3.7 Android上运行,这回得到的结果却是:黑屏。 又将Cocos2d-x 3.0rc0自带的cpp-empty-test编译后放到模拟器上运行,得到了同样的黑屏结果,显然这可能是rc0的一个问题。在Cocos2d-x forum上粗略搜到的结果是:升级到最新版本可以解决黑屏问题。于是到官方下载目前最新发布版Cocos2d-x 3.0rc2。这里也吐槽一下:cocos2d-x引擎包Size太大了,似乎也没有提供什么patch文件,导致每发一个版本都要下载几百M的包。官方 git repository也太大了,尝试clone了几次都失败了,最终还只能下载源码的zip包。

Cocos2d-x 3.0rc2下载解压后,先编译了一下cpp-empty-test,然后部署到Android 2.3.7上运行,这回“黑屏”的确不见了,看来rc2修正了这个问题。接下来就是将我的GameDemo移植到rc2上了。

我用解压后的“cocos2d-x 3.0rc2”替换GameDemo下的cocos2d,然后运行cocos compile编译,install到模拟器行运行,程序启动失败,从monitor logcat中看到一行错误日志:

    “ANativeActivity_onCreate not found

怎么会呢?ANativeActivity_onCreate是由NDK的 native_app_glue static library提供的,怎么会找不到呢?

于是乎打开GameDemo/cocos2d/cocos/2d/platform/android/Android.mk打算查看一下究竟:

LOCAL_WHOLE_STATIC_LIBRARIES    := cocos_png_static cocos_jpeg_static cocos_tiff_static cocos_webp_static

include $(BUILD_STATIC_LIBRARY)

$(call import-module,jpeg/prebuilt/android)
$(call import-module,png/prebuilt/android)
$(call import-module,tiff/prebuilt/android)
$(call import-module,webp/prebuilt/android)

Android.mk内容中居然没有将native_app_glue列入,又翻看了一下cocos2d-x 3.0rc0中的同位置Android.mk,后者是有native_app_glue的库依赖的。难道是rc2这块忘记了?于是我尝试将 native_app_glue依赖加上:

LOCAL_WHOLE_STATIC_LIBRARIES   := android_native_app_glue cocos_png_static cocos_jpeg_static cocos_tiff_static cocos_webp_static

include $(BUILD_STATIC_LIBRARY)

$(call import-module,jpeg/prebuilt/android)
$(call import-module,png/prebuilt/android)
$(call import-module,tiff/prebuilt/android)
$(call import-module,webp/prebuilt/android)
$(call import-module,android/native_app_glue)

再次尝试编译,不过这次连编译都没能通过,错误的build结果如下:

/home1/tonybai/android-dev/adt-bundle-linux-x86_64/android-ndk-r9c/sources/android/native_app_glue/android_native_app_glue.c:232: error: undefined reference to 'android_main'
collect2: error: ld returned 1 exit status
make: *** [obj/local/armeabi/libgamedemo.so] Error 1

从结果来看,链接器没能找到native_app_glue中android_main对 应的函数体定义。android_main可是cocos2d-x 3.0引擎提供的实现啊。于是乎再次进入到rc2引擎代码中查找原因,结果却让我很是吃惊:“NativeActivity被引擎移除了”!cocos2d/cocos/2d/platform /android目录下面已经没有了nativeactivity.h和nativeactivity.cpp了:

$ ls -F cocos2d/cocos/2d/platform/android
Android.mk         CCApplication.h  CCDevice.cpp            CCFileUtilsAndroid.h  CCGLView.cpp  CCPlatformDefine.h  java/             jni/
CCApplication.cpp  CCCommon.cpp     CCFileUtilsAndroid.cpp  CCGL.h                CCGLView.h    CCStdC.h            javaactivity.cpp

我们看到了一个新文件:javaactivity.cpp,打开该文件,我们发现了和cocos2d-x 2.2.2版本类似的名字:Java_org_cocos2dx_lib_Cocos2dxRenderer_nativeInit。难道rc2针对 Android平台的引擎入口代码回退到2.x版本的设计了?于是乎赶紧进到/cocos2d/cocos/2d/platform/android/java/src/org/cocos2dx/lib目 录下一看究竟。

果不其然,一切看起来都那么的熟悉:Cocos2dxActivity.java、Cocos2dxGLSurfaceView.java、 Cocos2dxRenderer.java….。自此可以断定,rc2中Android平台的引擎设计退回到了2.x版:
    – 你的GameActivity要集成Cocos2dxActivity;
    – mGLSurfaceView.setCocos2dxRenderer(new Cocos2dxRenderer())时,GLThread(渲染线程)诞生
    – 死循环调用Cocos2dxRenderer.onDrawFrame
    – 引擎逻辑就在Cocos2dxRenderer.onDrawFrame中被执行。

关于2.2.2版Cocos2dx引擎的结构说明可以参考我的《Hello, Cocos2d-x》一文。

回到了2.2.2版本设计的引擎在性能上是否会像rc0那样给人以直观提升的感觉呢,即便渲染器是新写的?真机测试的结果表明,没有直观感觉到提 升。难道是Native Thread(pthread_create创建)和Java Thread之间的差别?不得而知,后续慢慢体会吧。

另外要提一句:javaactivity.cpp将以往2.2.2版本放在项目jni中的 Java_org_cocos2dx_lib_Cocos2dxRenderer_nativeInit挪到了引擎中,本来就是基本不变的代码, 放在引擎中的确更好。rc2的设计回归也有一定好处,一是以前对引擎的认识还适用,二呢就是适合集成在2.2.2版本中的第三方工具的方法应该同样适合3.0rc2版本,这样移 植成本估计会小些。这样我们针对新的3.0引擎,重点还是去关注渲染器、事件分发机制以及物理引擎的变化吧。

最后要做的一件事,就是将上一篇blog的名字做下修改,那篇文章的分析只能对3.0rc0版本有效了,对后续版本无效,已经不能代表3.0的引 擎结构了。事实上NativeActivity是在rc1就被移除了,这种较大的改动让人始料不急。这么大的改动,这么短时间发布,让人对目前的 3.0引擎,至少是Android版本引擎的质量表示些许担忧啊。不知道3.0正式版中这块的代码会变啥样,拭目以待吧。

BTW,rc2版本cpp-empty-test在Android 2.3.7模拟器上的帧数在10帧以下,我的Demo也只有5帧,而在4.4版本模拟器上,可以达到40帧,还好还好。

Hello, Cocos2d-x 3.0rc0

Cocos2d-x 3.0版本已经发布了rc2,这让这段时间用熟了Cocos2d-x 2.2.2的我也有些蠢蠢欲动。按照触控科技主创人员在CocoaChina2014大会上的讲解,Cocos2d-x 3.0版本相比2.x版本在各方面都有不错的提升,于是乎就想把手头上的一款习作移植到3.0版本引擎下,看看运行效果如何。不过在移植之前,我先来看看 3.0与2.0相比在整体代码结构以及引擎驱动核心方面到底有哪些变化。一旦搞定这些原理,迁移什么都不是问题了。这里以Cocos2d-x 3.0rc0版的Android平台引擎为例。

一、从NativeActivity开始

Cocos2d-x 2.x版本中,游戏的Main Activity继承于引擎实现的Cocos2dxActivity(见《Hello,Cocos2d-x》),Cocos2dxActivity将一个 GLSurfaceView实例set给Window对象,并在为GLSurfaceView设置Renderer实例时创建Renderer Thread(渲染线程)。而Java代码则通过jni将游戏引擎中的C++代码引入。

Cocos2d-x 3.0版本则进一步摆脱Java束缚,当然也有新渲染器设计方面的考虑,3.0版本直接使用了Android NDK中提供的NativeActivity,我们游戏的主Activity(比如cpp-empty-test中的Cocos2dxActivity) 直接从NativeActivity继承:

// tests/cpp-empty-test/proj.android/src/org/cocos2dx/cpp_empty_test/Cocos2dxActivity.java
public class Cocos2dxActivity extends NativeActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // TODO Auto-generated method stub
        super.onCreate(savedInstanceState);

        … …

        //2.Set the format of window
        // getWindow().setFormat(PixelFormat.TRANSLUCENT);

    }
}

可以看出这里的Cocos2dxActivity啥也没做,因此整个Cocos2d-x 3.0游戏的起点就应该是NativeActivity了

NativeActivity是Android专为使用NDK开发Android应用而实现的一个Activity类,通过使用 NativeActivity,开发者可以最大程度地摆脱Java代码的编写,尽可能的使用C++代码。

二、NativeActivity的原理

NativeActivity在Android 2.3版本引入,其核心方法依旧是onCreate,我们一起来看一下(省略部分与分析无关的代码):

// NativeActivity.java(Android  4.4.2_r1)

@Override
    protected void onCreate(Bundle savedInstanceState) {
        String libname = "main";
        String funcname = "ANativeActivity_onCreate";

        ActivityInfo ai;
        … …

        mNativeContentView = new NativeContentView(this);
        mNativeContentView.mActivity = this;
        setContentView(mNativeContentView);
        mNativeContentView.requestFocus();
        mNativeContentView.getViewTreeObserver()
                          .addOnGlobalLayoutListener(this);
       
        try {
            ai = getPackageManager().getActivityInfo(
                    getIntent().getComponent(),
                    PackageManager.GET_META_DATA);
            if (ai.metaData != null) {
                String ln = ai.metaData.getString(META_DATA_LIB_NAME);
                if (ln != null) libname = ln;
                ln = ai.metaData.getString(META_DATA_FUNC_NAME);
                if (ln != null) funcname = ln;
            }
        } catch (PackageManager.NameNotFoundException e) {
            throw new RuntimeException("Error getting activity info", e);
        }
       
        String path = null;
       
        File libraryFile = new File(ai.applicationInfo.nativeLibraryDir,
                System.mapLibraryName(libname));
        if (libraryFile.exists()) {
            path = libraryFile.getPath();
        }
       
        if (path == null) {
            throw new IllegalArgumentException(
                      "Unable to find native library: " + libname);
        }
       
        byte[] nativeSavedState = savedInstanceState != null
          ? savedInstanceState.getByteArray(KEY_NATIVE_SAVED_STATE) : null;

        mNativeHandle = loadNativeCode(path, funcname, Looper.myQueue(),
                getAbsolutePath(getFilesDir()), getAbsolutePath(getObbDir()),
                getAbsolutePath(getExternalFilesDir(null)),
                Build.VERSION.SDK_INT, getAssets(), nativeSavedState);

        if (mNativeHandle == 0) {
            throw new IllegalArgumentException("Unable to load native library: "
                      + path);
        }
        super.onCreate(savedInstanceState);
    }

从NativeActivity的onCreate代码我们大致可以看出,NativeActivity在游戏对应的原生库(.so)中查找名为"ANativeActivity_onCreate"的函数,并执行该函数。其执行 是通过loadNativeCode这个Jni方法实现的。loadNativeCode在 /core/jni/android_app_NativeActivity.cpp中有实现:

 /core/jni/android_app_NativeActivity.cpp
static jint
loadNativeCode_native(JNIEnv* env, jobject clazz, jstring path, jstring funcName,
        jobject messageQueue,
        jstring internalDataDir, jstring externalDataDir, int sdkVersion,
        jobject jAssetMgr, jbyteArray savedState)
{
    LOG_TRACE("loadNativeCode_native");
    const char* pathStr = env->GetStringUTFChars(path, NULL);
    NativeCode* code = NULL;
    void* handle = dlopen(pathStr, RTLD_LAZY);
    env->ReleaseStringUTFChars(path, pathStr);
    if (handle != NULL) {
        const char* funcStr = env->GetStringUTFChars(funcName, NULL);
        code = new NativeCode(handle, (ANativeActivity_createFunc*)
                dlsym(handle, funcStr));
        env->ReleaseStringUTFChars(funcName, funcStr);
        if (code->createActivityFunc == NULL) {
            LOGW("ANativeActivity_onCreate not found");
            delete code;
            return 0;
        }

        … …
        code->createActivityFunc(code, rawSavedState, rawSavedSize);
        if (rawSavedState != NULL) {
            env->ReleaseByteArrayElements(savedState, rawSavedState, 0);
        }
    }
    return (jint)code;
}

做过系统编程的朋友想必对dlsym都很熟系,这个函数用来从一个打开的.so中(dlopen)获得某个函数对应的代码地址。 code->createActivityFunc则是执行这个函数。

我们在强化一下,费了半天劲儿找到并执行的这个函数的名字是:ANativeActivity_onCreate。 如果你要使用NativeActivity,你就必须提供一份ANativeActivity_onCreate函数的实现。在该函数的实现中, 你要为Activity注册各种生命周期事件以及其他输入事件的回调函数,比如onStart、onResume、onDestroy等。NDK 官方文档中有详细的说明。

不过这样一来,所有的事件处理均在NativeActivity所在的主线程里执行,为了不阻塞主线程的页面刷新以及交互响应,我们需要将这些回 调函数实现的短小精悍,不能拖泥带水,不能“干重活儿”。以前使用SDK时,Android SDK提供了AsyncTask, Handler, Runnable, Thread等诸多手段帮助在后台处理一些“重量级”的事情,但在NDK中,我们该如何处理呢?NDK也为我们提供了一种方案: android_native_app_glue

android_native_app_glue大致做了这么几件事:
1、实现了ANativeActivity_onCreate函数,注册了 Callback函数;
2、创建一个新的子Thread,用于干重活儿
3、在Main Thread和新线程之间建立了一个管道,用于Main Thread给新线程传递各种事件,以便后者读取并处理。

可以说native_app_glue的存在,进一步降低了 NativeActivity的使用门槛,否则以上诸事均要有开发人员自行实现。

下面结合源码做简单说明:

// android-ndk-r9c/sources/android/native_app_glue/android_native_app_glue.c

void ANativeActivity_onCreate(ANativeActivity* activity,
        void* savedState, size_t savedStateSize) {
    LOGV("Creating: %p\n", activity);
    activity->callbacks->onDestroy = onDestroy;
    activity->callbacks->onStart = onStart;
    activity->callbacks->onResume = onResume;
    activity->callbacks->onSaveInstanceState = onSaveInstanceState;
    activity->callbacks->onPause = onPause;
    activity->callbacks->onStop = onStop;
    activity->callbacks->onConfigurationChanged = onConfigurationChanged;
    activity->callbacks->onLowMemory = onLowMemory;
    activity->callbacks->onWindowFocusChanged = onWindowFocusChanged;
    activity->callbacks->onNativeWindowCreated = onNativeWindowCreated;
    activity->callbacks->onNativeWindowDestroyed = onNativeWindowDestroyed;
    activity->callbacks->onInputQueueCreated = onInputQueueCreated;
    activity->callbacks->onInputQueueDestroyed = onInputQueueDestroyed;

    activity->instance = android_app_create(activity, savedState, savedStateSize);
}

static struct android_app* android_app_create(ANativeActivity* activity,
        void* savedState, size_t savedStateSize) {
    struct android_app* android_app = (struct android_app*)malloc(sizeof(struct android_app));
    memset(android_app, 0, sizeof(struct android_app));
    android_app->activity = activity;

    pthread_mutex_init(&android_app->mutex, NULL);
    pthread_cond_init(&android_app->cond, NULL);

    … …
    int msgpipe[2];
    if (pipe(msgpipe)) {
        LOGE("could not create pipe: %s", strerror(errno));
        return NULL;
    }
    android_app->msgread = msgpipe[0];
    android_app->msgwrite = msgpipe[1];

    pthread_attr_t attr;
    pthread_attr_init(&attr);
    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
    pthread_create(&android_app->thread, &attr, android_app_entry, android_app);

    // Wait for thread to start.
    pthread_mutex_lock(&android_app->mutex);
    while (!android_app->running) {
        pthread_cond_wait(&android_app->cond, &android_app->mutex);
    }
    pthread_mutex_unlock(&android_app->mutex);

    return android_app;
}

上面的android_app_create创建了子线程,建立了两个线程的pipe,新线程的入口是android_app_entry

static void* android_app_entry(void* param) {
    struct android_app* android_app = (struct android_app*)param;

    android_app->config = AConfiguration_new();
    AConfiguration_fromAssetManager(android_app->config,
             android_app->activity->assetManager);

    print_cur_config(android_app);

    android_app->cmdPollSource.id = LOOPER_ID_MAIN;
    android_app->cmdPollSource.app = android_app;
    android_app->cmdPollSource.process = process_cmd;
    android_app->inputPollSource.id = LOOPER_ID_INPUT;
    android_app->inputPollSource.app = android_app;
    android_app->inputPollSource.process = process_input;

    ALooper* looper = ALooper_prepare(
                      ALOOPER_PREPARE_ALLOW_NON_CALLBACKS);
    ALooper_addFd(looper, android_app->msgread,
                  LOOPER_ID_MAIN,
                  ALOOPER_EVENT_INPUT, NULL,
                  &android_app->cmdPollSource);
    android_app->looper = looper;

    pthread_mutex_lock(&android_app->mutex);
    android_app->running = 1;
    pthread_cond_broadcast(&android_app->cond);
    pthread_mutex_unlock(&android_app->mutex);

    android_main(android_app);

    android_app_destroy(android_app);
    return NULL;
}

新线程建立了事件处理设施(looper),并通知主线程(通过条件变量)app正式开始运行了(running = 1),之后进入android_main

Cocos2d-x 3.0采用的就是android_native_app_glue这 种方案,而android_main则是Cocos2d-x 3.0引擎层的入口。

//cocos/2d/platform/android/Android.mk

LOCAL_WHOLE_STATIC_LIBRARIES    := android_native_app_glue cocos_png_static cocos_jpeg_static cocos_tiff_static cocos_webp_static
$(call import-module,android/native_app_glue)

、走进引擎

从android_main函数开始,我们就进入了Cocos2d-x 3.0引擎的范畴。android_main函数比较长,我们挑重点说:

cocos/2d/platform/android/nativeactivity.cpp

void android_main(struct android_app* state) {
    … …

    memset(&engine, 0, sizeof(engine));
    state->userData = &engine;
    state->onAppCmd = engine_handle_cmd;
    state->onInputEvent = engine_handle_input;
    state->inputPollSource.process = process_input;
    engine.app = state;

    // Prepare to monitor accelerometer
    … …

    while (1) {
        // Read all pending events.
        int ident;
        int events;
        struct android_poll_source* source;

        // If not animating, we will block forever waiting for events.
        // If animating, we loop until all events are read, then continue
        // to draw the next frame of animation.
        while ((ident=ALooper_pollAll(engine.animating ? 0 : -1,
                NULL, &events,
                (void**)&source)) >= 0) {

            // Process this event.
            if (source != NULL) {
                source->process(state, source);
            }
            … …
            // Check if we are exiting.
            if (state->destroyRequested != 0) {
                engine_term_display(&engine);

                memset(&engine, 0, sizeof(engine));
                s_methodInitialized = false;
                return;
            }
        }

        if (engine.animating) {
            // Done with events; draw next animation frame.
            engine.state.angle += .01f;
            if (engine.state.angle > 1) {
                engine.state.angle = 0;
            }

            // Drawing is throttled to the screen update rate, so there
            // is no need to do timing here.
            LOG_RENDER_DEBUG("android_main : engine.animating");
            engine_draw_frame(&engine);
        } else {
            LOG_RENDER_DEBUG("android_main : !engine.animating");
        }
        …
    }
}

android_main有些像cocos2d-x 2.2.2中GLThread的guardedRun方法,里面基本上就是一个死循环(while (1)),简化后的逻辑大致如下:

void android_main(struct android_app* state) {
    while (1) {
        Do Main Thread Event Processing & Input Event Processing;
        if (engine.animating) {
            // draw next animation frame 画下一帧
            engine_draw_frame(&engine);
        }
    }
}

而引擎的初始化和帧渲染就是在这个死循环中一步步完成的。

引擎的初始化始于APP_CMD_INIT_WINDOW事件,在engine_handle_cmd中,我们可以看到:

static void engine_handle_cmd(struct android_app* app, int32_t cmd)
{
    struct engine* engine = (struct engine*)app->userData;
    switch (cmd) {
        … …
        case APP_CMD_INIT_WINDOW:
            // The window is being shown, get it ready.
            if (engine->app->window != NULL) {
                cocos_dimensions d = engine_init_display(engine);
                if ((d.w > 0) &&
                    (d.h > 0)) {
                    cocos2d::JniHelper::setJavaVM(app->activity->vm);
                    cocos2d::JniHelper::setClassLoaderFrom(app->activity->clazz);

                    // call Cocos2dxHelper.init()
                    cocos2d::JniMethodInfo ccxhelperInit;
                    if (!cocos2d::JniHelper::getStaticMethodInfo(ccxhelperInit,
                                             "org/cocos2dx/lib/Cocos2dxHelper",
                                             "init",
                                             "(Landroid/app/Activity;)V")) {
                        LOGI("cocos2d::JniHelper::getStaticMethodInfo(ccxhelperInit) FAILED");
                    }
                    ccxhelperInit.env->CallStaticVoidMethod(ccxhelperInit.classID,
                                                            ccxhelperInit.methodID,
                                                            app->activity->clazz);

                    cocos_init(d, app);
                }
                engine->animating = 1;
                engine_draw_frame(engine);
            }
            break;
           … …
    }
}

当收到主线程通知的窗口建立事件时,engine_handle_cmd的APP_CMD_INIT_WINDOW事件处理函数主要做了两件事:
1、调用engine_init_display初始化EGL;
2、调用cocos_init初始化引擎的主要角色。

这里进入到引擎初始化的前提是“engine->app->window != NULL”。而app->window的设置是在native_app_glue中进行的,大致流程是:
Main Thread:
onNativeWindowCreated
    -> android_app_set_window
    -> android_app->pendingWindow = window;
    -> android_app_write_cmd(android_app, APP_CMD_INIT_WINDOW);

Sub Thread:
process_cmd
    -> android_app_pre_exec_cmd
        -> android_app->window = android_app->pendingWindow;
        -> engine_handle_cmd(即app->onAppCmd回调),此时android_app->window != NULL

四、引擎初始化

前面说过,引擎初始化包括两部分:engine_init_display和cocos_init,我们分别来说说。

1、engine_init_display

//cocos/2d/platform/android/nativeactivity.cpp

static cocos_dimensions engine_init_display(struct engine* engine)
{
    cocos_dimensions r;
    … …

    EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY);

    eglInitialize(display, 0, 0);
    eglChooseConfig(display, attribs, &config, 1, &numConfigs);
    eglGetConfigAttrib(display, config, EGL_NATIVE_VISUAL_ID, &format);

    ANativeWindow_setBuffersGeometry(engine->app->window, 0, 0, format);

    surface = eglCreateWindowSurface(display, config, engine->app->window, NULL);

    const EGLint eglContextAttrs[] =
    {
        EGL_CONTEXT_CLIENT_VERSION, 2,
        EGL_NONE
    };

    context = eglCreateContext(display, config, NULL, eglContextAttrs);

    if (eglMakeCurrent(display, surface, surface, context) == EGL_FALSE) {
        LOGW("Unable to eglMakeCurrent");
        return r;
    }

    eglQuerySurface(display, surface, EGL_WIDTH, &w);
    eglQuerySurface(display, surface, EGL_HEIGHT, &h);

    engine->display = display;
    engine->context = context;
    engine->surface = surface;
    engine->width = w;
    engine->height = h;
    engine->state.angle = 0;

    r.w = w;
    r.h = h;

    return r;
}

这段代码应该是典型的EGL初始化流程,几乎每本有关EGL或opengl es的教程中都会有类似描述。个人对opengl以及EGL了解不多,从一些书籍或网络资料中大致得到如下一些理解:

首先,Android下每个Activity都会有对应窗口(Window)以及View,View就是显示在屏幕上的内容。NativeActivity初始化时设置了一个NativeContentView(View的子类):

// android/app/NativeActivity.java

static class NativeContentView extends View {
        NativeActivity mActivity;

        public NativeContentView(Context context) {
            super(context);
        }

        public NativeContentView(Context context,
                           AttributeSet attrs) {
            super(context, attrs);
        }
    }

protected void onCreate(Bundle savedInstanceState) {
        … …
        mNativeContentView = new NativeContentView(this);
        mNativeContentView.mActivity = this;
        setContentView(mNativeContentView);
        … …
}

但Cocos2d-x显然不会使用这个View,而是直接在窗口用opengl绘图。EGL大致有三个元素:Context、Display以及 Surface。它们之间的大致关系是:EGL通过Context指挥opengl在Surface画布(一种帧缓冲FrameBuffer)上绘制,绘 制完成后再Swap到窗口的Display显示器上去,这样我们就能看到绘制的图像了。2D游戏引擎的渲染器用的都是这个原理。关于上述EGL初始化的具 体调用含义这里就不赘述了,大家如要深入了解,可以找本OpenGL ES相关的书去看看。

2、cocos_init

static void cocos_init(cocos_dimensions d, struct android_app* app)
{
    LOGI("cocos_init(…)");
    pthread_t thisthread = pthread_self();
    LOGI("pthread_self() = %X", thisthread);

    cocos2d::FileUtilsAndroid::setassetmanager(app->activity->assetManager);

    auto director = cocos2d::Director::getInstance();
    auto glview = director->getOpenGLView();
    if (!glview)
    {
        glview = cocos2d::GLView::create("Android app");
        glview->setFrameSize(d.w, d.h);
        director->setOpenGLView(glview);

        cocos_android_app_init(app);

        cocos2d::Application::getInstance()->run();
    }
    else
    {
        cocos2d::GL::invalidateStateCache();
        cocos2d::ShaderCache::getInstance()->reloadDefaultShaders();
        cocos2d::DrawPrimitives::init();
        cocos2d::VolatileTextureMgr::reloadAllTextures();

        cocos2d::EventCustom foregroundEvent(EVENT_COME_TO_FOREGROUND);
        director->getEventDispatcher()->dispatchEvent(&foregroundEvent);
        director->setGLDefaultValues();
    }
}

分析过Cocos2d-x 2.x版本引擎结构的朋友对这段代码一定比较眼熟,没错,在2.x版本中这段代码是放在游戏项目的proj.android/jni/下的,在jni方法Java_org_cocos2dx_lib_Cocos2dxRenderer_nativeInit中我们可以看到类似代码。

在cocos_init中我们看到了Cocos2d-x游戏引擎的一个重要角色Director的创建和初始化:

auto director = cocos2d::Director::getInstance();

//cocos/2d/CCDirector.cpp

bool Director::init(void)
{
    setDefaultValues();

    … …

    _openGLView = nullptr;

    _contentScaleFactor = 1.0f;

    // scheduler
    _scheduler = new Scheduler();
    // action manager
    _actionManager = new ActionManager();
    _scheduler->scheduleUpdate(_actionManager,
                   Scheduler::PRIORITY_SYSTEM, false);

    _eventDispatcher = new EventDispatcher();
    _eventAfterDraw = new EventCustom(EVENT_AFTER_DRAW);
    _eventAfterDraw->setUserData(this);
    _eventAfterVisit = new EventCustom(EVENT_AFTER_VISIT);
    _eventAfterVisit->setUserData(this);
    _eventAfterUpdate = new EventCustom(EVENT_AFTER_UPDATE);
    _eventAfterUpdate->setUserData(this);
    _eventProjectionChanged = new EventCustom(EVENT_PROJECTION_CHANGED);
    _eventProjectionChanged->setUserData(this);

    //init TextureCache
    initTextureCache();

    _renderer = new Renderer;
    _console = new Console;

    return true;
}

诸多引擎基础设施都是在Director::init中被初始化的,这里最重要的就是Renderer了,这个就是Cocos2d-x 3.0新实现的渲染器。

Director初始化后,_openGLView == NULL,后续cocos_init调用cocos2d::GLView::create("Android app")创建GLView,并set到Director中。这个View似乎更多是用来辅助处理屏幕适配以及触屏事件处理的。

cocos_init最后调用了cocos_android_app_init(app),这个函数实现在你的游戏工程中,以cpp-empty- test为例,在tests/cpp-empty-test/proj.android/jni/main.cpp中我们看到了该函数的实现:

void cocos_android_app_init (struct android_app* app) {
    LOGD("cocos_android_app_init");
    AppDelegate *pAppDelegate = new AppDelegate();
}

我们已经进入游戏业务逻辑层了。和Cocos2d-x 2.x版本一样,Classes/AppDelegate.cpp中的 AppDelegate::applicationDidFinishLaunching依旧是我们初始化我们游戏业务逻辑层的入口。而这一入口函数是在 cocos_init中的cocos2d::Application::getInstance()->run()调用时被调用的。

// /cocos/2d/platform/android/CCApplication.cpp
int Application::run()
{
    // Initialize instance and cocos2d.
    if (! applicationDidFinishLaunching())
    {
        return 0;
    }

    return -1;
}

至此,我们又回到了熟悉的游戏业务逻辑层,也就是你的游戏project中。

五、到底发生了哪些重要变化

之前听说Cocos2d-x 3.0引擎的一个重要改造就是尽可能利用多线程,利用硬件的多核来提升游戏渲染性能。这给我的错觉是Renderer Thread完全独立出去,只负责渲染。但实际发布的版本似乎并不是这么回事。Cocos2d-x 2.x版本是两个线程,3.0版本依旧是两个线程,从cpp-empty-test运行的logcat日志也能看出来:

04-21 07:36:52.779  1522  1522 D dalvikvm: Late-enabling CheckJNI
04-21 07:36:52.783  1522  1522 I dalvikvm: Enabling JNI app bug workarounds for target SDK version 9…
04-21 07:36:52.783   561   573 I ActivityManager: Start proc org.cocos2dx.cpp_empty_test for activity org.cocos2dx.cpp_empty_test/.            Cocos2dxActivity: pid=1522 uid=10056 gids={50056}
04-21 07:36:53.047  1522  1535 D libEGL  : loaded /system/lib/egl/libEGL_genymotion.so
04-21 07:36:53.047  1522  1535 D         : HostConnection::get() New Host Connection established 0xb918a7c8, tid 1535
04-21 07:36:53.071  1522  1535 D libEGL  : loaded /system/lib/egl/libGLESv1_CM_genymotion.so
04-21 07:36:53.083  1522  1535 D libEGL  : loaded /system/lib/egl/libGLESv2_genymotion.so
04-21 07:36:53.143  1522  1535 D JniHelper: JniHelper::setJavaVM(0xb903a730), pthread_self() = B9180250
04-21 07:36:53.155  1522  1535 I cocos2dx/nativeactivity.cpp: cocos_init(…)
… …
04-21 07:36:53.395  1522  1535 D main    : cocos_android_app_init
04-21 07:36:53.419  1522  1535 D CCFileUtilsAndroid.cpp: relative path = ipadhd/CloseNormal.png
04-21 07:36:53.427  1522  1535 D CCFileUtilsAndroid.cpp: relative path = ipadhd/CloseSelected.png
04-21 07:36:53.439  1522  1535 D cocos2d-x debug info: cocos2d: fullPathForFilename: No file found at Arial. Possible missing file.
04-21 07:36:53.447  1522  1535 D dalvikvm: GC_FOR_ALLOC freed 64K, 4% free 3455K/3584K, paused 7ms, total 7ms
04-21 07:36:53.467  1522  1535 D CCFileUtilsAndroid.cpp: relative path = ipadhd/HelloWorld.png
04-21 07:36:54.003  1522  1535 I cocos2dx/nativeactivity.cpp: engine_draw_frame(…)
04-21 07:36:54.003  1522  1535 I cocos2dx/nativeactivity.cpp: pthread_self() = B9180250
04-21 07:36:54.003  1522  1535 I cocos2dx/nativeactivity.cpp: engine_draw_frame : just called cocos' mainLoop()
04-21 07:36:54.051  1522  1535 I cocos2dx/nativeactivity.cpp: android_main : engine.animating
04-21 07:36:54.051  1522  1535 I cocos2dx/nativeactivity.cpp: engine_draw_frame(…)
04-21 07:36:54.051  1522  1535 I cocos2dx/nativeactivity.cpp: pthread_self() = B9180250

… …
04-21 07:36:56.507  1522  1535 I Process : Sending signal. PID: 1522 SIG: 9

可以看出NativeActivity所在的主线程号为1522,但绝大多数工作都在1535这个渲染线程,也就是native_app_glue库中创 建的那个线程。Scene Graph管理和Renderer::render依旧都在该Thread内完成,这似乎也很难有效并充分的利用起多核的效能啊。cpp-empty- test在我的genymotion模拟器上跑时,帧数始终在50帧左右。

不过Renderer的确是重写的,并且将2.x版本中Scene Graph的管理与渲染之间的耦合解耦开来。每帧按Scene Graph Visit Node时并不真正执行渲染,而只是构造DrawCommand,并插入到Renderer的DrawCommand队列中:

// draw

void Sprite::draw(Renderer *renderer, const kmMat4 &transform, bool transformUpdated)
{
    // Don't do calculate the culling if the transform was not updated
    _insideBounds = transformUpdated ? isInsideBounds() : _insideBounds;

    if(_insideBounds)
    {
        _quadCommand.init(_globalZOrder, _texture->getName(), _shaderProgram, _blendFunc, &_quad, 1, transform);
        renderer->addCommand(&_quadCommand);
#if CC_SPRITE_DEBUG_DRAW
        _customDebugDrawCommand.init(_globalZOrder);
        _customDebugDrawCommand.func = CC_CALLBACK_0(Sprite::drawDebugData, this);
        renderer->addCommand(&_customDebugDrawCommand);
#endif //CC_SPRITE_DEBUG_DRAW
    }
}

在Director::drawScene尾部我们能看到真正的渲染动作render()被调用:

void Director::drawScene()
{
    … …
    // draw the scene
    if (_runningScene)
    {
        _runningScene->visit(_renderer, identity, false);
        _eventDispatcher->dispatchEvent(_eventAfterVisit);
    }

    _renderer->render();
    _eventDispatcher->dispatchEvent(_eventAfterDraw);

    kmGLPopMatrix();

    _totalFrames++;

    // swap buffers
    if (_openGLView)
    {
        _openGLView->swapBuffers();
    }

    if (_displayStats)
    {
        calculateMPF();
    }
}

这里我们看到了 _openGLView->swapBuffers(),但该方法的具体实现为空,真正swapBuffers调用在外层:

static void engine_draw_frame(struct engine* engine)
{
    LOG_RENDER_DEBUG("engine_draw_frame(…)");
    pthread_t thisthread = pthread_self();
    LOG_RENDER_DEBUG("pthread_self() = %X", thisthread);

    if (engine->display == NULL) {
        // No display.
        LOGW("engine_draw_frame : No display.");
        return;
    }

    dispatch_pending_runnables();
    cocos2d::Director::getInstance()->mainLoop();
    LOG_RENDER_DEBUG("engine_draw_frame : just called cocos' mainLoop()");

    /* // Just fill the screen with a color. */
    /* glClearColor(((float)engine->state.x)/engine->width, engine->state.angle, */
    /*         ((float)engine->state.y)/engine->height, 1); */
    /* glClear(GL_COLOR_BUFFER_BIT); */

    if (s_pfEditTextCallback && editboxText)
    {
        s_pfEditTextCallback(editboxText, s_ctx);
        free(editboxText);
        editboxText = NULL;
    }

    eglSwapBuffers(engine->display, engine->surface);
}

还记得android_main中的“死循环”么?那个死循环在每一帧都会调用engine_draw_frame方法,而这恰是整个Cocos2d-x 3.0引擎的驱动中心

通过汇集各个Node的DrawCommand而不是直接Draw,新渲染器可以做一些优化,比如Batch Renderer等。这在上一版本引擎中较难实现,或者只能显式的通过CCSpriteBatchNode实现。更多的好处可以参考官方说明,或待日后使 用引擎时挖掘。

六、其他

Cocos2d-x 3.0引擎的C++部分采用了C++ 11标准中的语法,因此如果你要编译Linux版本游戏,你需要升级你的gcc编译器到4.7以上版本。但如果只构建Android 游戏,Android NDK(r9c以后版本)早为我们准备好了arm和x86平台的4.8版本的g++编译器了。

Cocos2d-x 3.0的内存管理依旧沿用内存计数机制,如果你理解了2.x版本的内存管理,理解3.0版本应该不会有太大问题。

七、参考资料
 
  – Android NDK源码(r9c)
  -  Cocos2d-x 3.0rc1源码
  – Android SDK源码(4.4.2_r1)
  – 《Android Native Development Kit Cookbook — A step-by-step tutorial with more than 60 concise recipes on Android NDK development skills》。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats