世界足球的那个“王”还会出现吗?

No Comments

准球王梅西最终没能将巴西世界杯的决赛赛场变成自己到加冕地,潘帕斯雄鹰阿根廷连续第三届世界杯被德意志战车践踏,让我这个老阿迷痛心不已。

足球是一种信仰,在足球这个信仰的世界里有神,更要有王。但现代足球趋向整体的战术体系让类似贝利、马拉多纳那样的“王”的出现日益困难。足球界、球迷们 实际上都期望新“王”的诞生,这样才能带来更多的信仰满足感和成就感。因此每当有天赋异秉的球员出现时,大家都会给予足够的关注目光。梅西就是从2005 年世青赛一直被关注到今天上午的决赛赛场的。但就是像梅西这样50年不遇的足球天才也没能最终加冕为像马拉多纳那样的“王”,世界足球的“王”依旧缺失!

我们不禁要问,足球界的那个“王”还会出现吗?现实似乎给了我们比较悲观的答案。虽说很多人认为球王已不再需要世界杯去证明,但传统认知的惯性还是会影响诸多球迷:新球王就应该像贝利和马拉多纳那样拿下大力神杯。这里提到的“王”依旧遵循着传统的认知。

1、整体足球无法诞生“球王”

足球百年历史上公认的球王只有两个:贝利和马拉多纳。两人都来自南美洲,两人都在各自的世界杯舞台上有着被公认是个人英雄式的表演,这让全世界球迷顶礼膜 拜。但在今天整体足球逐渐成为主流趋势的情况下,我们再也很难见到想马拉多纳那样以一己之力带领球队拿到世界杯的表演了。2010年的西班牙,2014年 的德国都是整体足球、团队足球的典型代表,他们虽然夺冠了,但我们无法从中选出一个马拉多纳式的人物,冠军的成绩依靠的是整体的流畅运转,就像我们评价此 次德国队的那样,球队更像一个机器,每个螺丝,每个细节都是关键。

从此次世界杯的表现来看,要说不够“整体”的,还真的只有阿根廷了,这届梅西的表现其实已经十分接近马拉多纳当年的作用了,没能成王十分可惜。巴西队和德国、西班牙比起来,整体性也有不足,似乎具有诞生球王的潜质。但巴西、阿根廷在未来真的能诞生新王吗?我们继续分析。

2、巴西、阿根廷足球的"衰落"

2002年巴西在亚洲夺冠,让人们看到南美足球似乎在恢复统治力。但随着德国、西班牙对青训体系的计划、投入以及严格执行,西班牙、德国从2002年后诞 生了一大批年轻有天赋的足球青年,在2010、2014年,西班牙和德国先后捧杯就是一流青训计划带来的结果。反观这一时段的巴西和阿根廷队,人才青黄不 接。巴西中前场再无3R组合的豪华,阿根廷更加悲惨,不仅后卫趋向三流,进攻组织型中场也销声匿迹,唯独锋线还能拿得出手。更让南美球迷担心的是,巴西、 阿根廷本土联赛的水准日渐下滑,从近几次世俱杯的欧美对抗结果即可看出。巴西、阿根廷绝对不缺少天才型球员,缺少的则是像德国那样的长远规划和强有力的执 行。

另外巴阿两国在球员培养和输送方面开始“拜金”,什么样的球员受欧洲球队欢迎就培养什么位置的球员。哪个俱乐部给钱多,就把球员卖给哪个俱乐部。这导致大 量天才球员登陆欧洲的第一站往往是俄罗斯这样的三流联赛,让这些球员失去了向一流球员学习经验的机会,天赋逐渐被磨灭,经验也都是三流的,没法登上一流的 赛场上,逐渐变得平庸。另外南美球员由于分布在不同国家联赛,导致打法很难统一,一到国家队磨合起来十分困难,间接削弱了国家队的战斗力。

3、欧洲足球是“反球王”模式的

欧洲依旧是世界足球的中心,这有利于欧洲国家的球员在一支球队内磨合和统一风格。比如2010的西班牙,以巴萨为班底;本届的德国以拜仁为班底。核心球员 在俱乐部里配合默契娴熟,到了国家队基本不需要磨合就可以发挥出100%战力,让整体足球体现的淋漓尽致。但这种技术领先、团队合作的欧洲足球是反球王模 式的,这在相当大的程度上阻止了像阿根廷、巴西这样具有“王”诞生条件的国家队站在世界杯的最高领奖台上。

综上三点,世界足球“新球王”诞生真的不再乐观。也许我们依旧能看到像齐达内这样的神,但短时间内很难再看到马拉多纳这样的王的出现。梅西在这届世界杯上 已经尽力了,但依旧无法称王,我实在难以想象得到未来几十年中还能有像梅西这样天赋异秉的足球天才的出现。巴西、阿根廷足球体系的衰落也让王的出现更为难 上加难。

Cocos2d-x屏幕适配之Sprite绘制原理

8 Comments

手机(智能终端)游戏绝大多数为全屏(Full Screen)显示,这样开发人员在制作游戏时势必要考虑不同手机(智能终端)屏幕大小、宽高比的不同给游戏画面带来的影响,并且要将这种影响降低到最 小,努力使用不同终端的游戏玩家拥有几乎相同的游戏画面体验。为此各种游戏引擎在屏幕适配方面都给出了自己的方案,Cocos2d-x也不例外。 在Cocos2d-x官网Wiki上特地撰写了一篇讲解Cocos2d-x多屏幕适配原理的文章“Detailed explanation of Cocos2d-x Multi-resolution adaptation”。

这里我们以Cocos2d-x引擎(基于2.2.2版本)自带的Sample项目HelloCpp(cocos2d-x-2.2.2/samples/Cpp/HelloCpp)为例,直观的看看这个方案带来的好 处。首先,我们对HelloCpp项目做些许改造:
    – 注释掉AppDelegate.cpp中applicationDidFinishLaunching下的pEGLView->setDesignResolutionSize(designResolutionSize.width, designResolutionSize.height, kResolutionNoBorder);
    – 仅使用Resource/iphone下的资源,即仅searchPath.push_back(smallResource.directory); 这里我们有一张480×320分辨率大小PNG文件。
    – 通过改变proj.linux/main.cpp中的eglView->setFrameSize(960, 640);来改变屏幕参数。(用linux工程模拟甚为方便,编译和运行占用资源小,极为迅捷,效果与Android平台是等 效的)

我们对比一下以下三种条件下的游戏Demo显示结果:
    1) 屏幕大小480×320,未做任何屏幕适配工作,不调用pEGLView->setDesignResolutionSize。
    2) 屏幕大小960×640,未做任何屏幕适配工作,不调用pEGLView->setDesignResolutionSize。
    3) 屏幕大小同为960×640,按照上面Cocos2d-x屏幕适配指南Wiki中的做法,调用pEGLView->setDesignResolutionSize(480, 320);

如我们所料,我们得到三个截然不同的结果。

第一种情况,我们所得到的游戏屏幕截图如下:

第二种情况,我们所得到的游戏屏幕截图如下:

第三种情况,我们所得到的游戏屏幕截图如下:

第一种情况是最理想的情况,屏幕大小与背景图片大小相同,如我们所愿,屏幕与背景图片吻合的天衣无缝。
第二种情况显然是模拟我们初次遇到问题的场景。屏幕Size扩大为原先的二倍,在资源没有变化的情况下,我们发现480×320大小的背景图片没 有铺满屏幕,仅仅是居中显示,并在四周露出较多”黑边“,这显然不是我们想要的。
第三种情况,也就是我们按照官方屏幕适配方案调整后得到的结果,在资源依旧不变的情况下,我们得到了相对令人满意的结果:背景图片恰如其分的铺满 整个屏幕,比例正确。这样我们用一套资源就可以同时适配两个屏幕了:480×320、960×640。这两种终端的玩家至少不会对我们的游戏心生 抱怨之情^_^。

当然在遇到第二种情况的时候,你也大可再准备一套新资源,比如一张960×640的背景图片。在480×320手机上,使用480×320的图 片;在960×640的手机上,使用960×640的背景图片。但这种方法的弊端至少有三:
    – 包大了:游戏的安装包Size急剧变大。
    – 活儿多了:因适配屏幕种类太多而制作大量的图片。
    – 新屏幕出来咋办:如果某个厂家突然于某天出品一款手机,其分辨率与以往市面上的所有手机均不同,那你的游戏因没有对应的资源,肯定无法很好适配该手机,导 致较差用户体验。

为此,适配屏幕唯一的出路似乎只有按照官方推荐的方案进行了,当然适当结合有限种类的资源也许可以更好的提升游戏体验。

如果仅仅从游戏制作角度来看,我们找到了可以适配屏幕的方法就可以了,没有必要刨根问底。甚至当有人问起来:为何 setDesignResolutionSize后,背景图片就可以充满屏幕了呢?我们可以回答:“引擎对精灵进行了缩放,就是这样”。但对于上 面的背景精灵来说,真的是我们理解的普通意义上的“精灵缩放(Scale)吗?本着“知其然,也要知其所以然”的精神,这里对引擎如何对 Sprite进行绘制进行了一番研究,我还真发现了一些与我之前理解差异较大的“深奥”原理,这里与大家一起分享一下。

一、绘制参数初始化

我们还是从代码开始,了解一下引擎绘制参数的初始化工作是如何做的、在哪里做的,为后续的分析做些铺垫。这里以Cocos2d-x 2.2.2 Android平台为例。关于Cocos2d-x 2.2.2 Android平台的引擎粗线条启动流程分析,可以参考《Hello,Cocos2d-x》这篇文章。看完这篇文章,你就会知道我们这次应该从Java_org_cocos2dx_lib_Cocos2dxRenderer_nativeInit开 始。

// samples/Cpp/HelloCpp/proj.android/jni/hellocpp/main.cpp
void Java_org_cocos2dx_lib_Cocos2dxRenderer_nativeInit(
               JNIEnv*  env, jobject thiz, jint w, jint h)
{
    if (!CCDirector::sharedDirector()->getOpenGLView())
    {
        CCEGLView *view = CCEGLView::sharedOpenGLView();
        view->setFrameSize(w, h);

        AppDelegate *pAppDelegate = new AppDelegate();
        CCApplication::sharedApplication()->run();
    }
    … …
}

这里是引擎部分初始化的起点:CCDirector和CCEGLView先后完成创建与初始化。接下来我们分别看一下这两个过程,我们主要关 注与绘制参数设置相关的内容:

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

    … …
    m_obWinSizeInPoints = CCSizeZero;

    m_pobOpenGLView = NULL;

    m_fContentScaleFactor = 1.0f;
    … …
    return true;
}

void CCDirector::setDefaultValues(void)
{
    CCConfiguration *conf =
     CCConfiguration::sharedConfiguration();
    … …
    // GL projection
    const char *projection =
        conf->getCString("cocos2d.x.gl.projection",
                         "3d");
    if( strcmp(projection, "3d") == 0 )
        m_eProjection = kCCDirectorProjection3D;
    … …
}

由于conf中没有配置“cocos2d.x.gl.projection”,因此projection使用了 getCString传入的默认值:"3d",m_eProjection则被赋值为kCCDirectorProjection3D。

CCEGLView的创建更为简单:

CCEGLView::CCEGLView()
{
    initExtensions();
}

但背后真正发挥关键作用的是其父类CCEGLViewProtocol。

CCEGLViewProtocol::CCEGLViewProtocol()
: m_pDelegate(NULL)
, m_fScaleX(1.0f)
, m_fScaleY(1.0f)
, m_eResolutionPolicy(kResolutionUnKnown)
{
}

这里我们看到了三个重要的字段:m_fScaleX、m_fScaleY以及m_eResolutionPolicy,这三个字段对于后续屏 幕适配起到至关重要的作用。

nativeInit中的view->SetFrameSize(w, h)用于设置的屏幕物理分辨率,如果你的手机是960×640分辨率的,那FrameSize就是960×640。

void CCEGLViewProtocol::setFrameSize(float width,
                                     float height)
{
    m_obDesignResolutionSize
      = m_obScreenSize
      = CCSizeMake(width, height);
}
初始情况下,CCEGLViewProtocol将“设计分辨率”m_obDesignResolutionSize也设置为与 FrameSize or m_obScreenSize同等大小。

我们回到游戏逻辑层代码AppDelegate.cpp,我们知道游戏逻辑的入口在这里,最初的参数初始化是在为Director设置 GLView实例时进行的:

bool AppDelegate::applicationDidFinishLaunching() {
    // initialize director
    CCDirector* pDirector = CCDirector::sharedDirector();
    CCEGLView* pEGLView = CCEGLView::sharedOpenGLView();

    pDirector->setOpenGLView(pEGLView);
    CCSize frameSize = pEGLView->getFrameSize();
    … …
}

void CCDirector::setOpenGLView(CCEGLView *pobOpenGLView)
{
        m_pobOpenGLView = pobOpenGLView;

        // set size
        m_obWinSizeInPoints =
           m_pobOpenGLView->getDesignResolutionSize();
        … …

        if (m_pobOpenGLView)
        {
            setGLDefaultValues();
        }

        CHECK_GL_ERROR_DEBUG();
        … …
    }
}

由于尚未调用setDesignResolutionSize,因此m_obWinSizeInPoints的值与FrameSize大小相 同。

setGLDefaultValues最为关键,这是我们第一次遇到该函数,该方法用于初始化一些OpenGL的参数,建立好后续 OpenGL操作时所需要的各种数据结构。

void CCDirector::setGLDefaultValues(void)
{
    … …
    setAlphaBlending(true);
    setDepthTest(false);
    setProjection(m_eProjection);
    // set other opengl default values
    glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
}

glClearColor(0.0f, 0.0f, 0.0f, 1.0f);设置初始颜色为黑色,alpha为1.0f,即完全不透明。setProjection是实际上绘制参数设置的核心。

void CCDirector::setProjection(ccDirectorProjection kProjection)
{
    CCSize size = m_obWinSizeInPoints;

    setViewport();
   
    switch (kProjection)
    {
    case kCCDirectorProjection3D:
        {
            float zeye = this->getZEye();

            kmMat4 matrixPerspective, matrixLookup;

            kmGLMatrixMode(KM_GL_PROJECTION);
            kmGLLoadIdentity();

            … …

            // issue #1334
            kmMat4PerspectiveProjection( &matrixPerspective,
                   60,
                  (GLfloat)size.width/size.height,
                   0.1f, zeye*2);

            kmGLMultMatrix(&matrixPerspective);

            kmGLMatrixMode(KM_GL_MODELVIEW);
            kmGLLoadIdentity();
            kmVec3 eye, center, up;
            kmVec3Fill( &eye, size.width/2,
                   size.height/2, zeye );
            kmVec3Fill( &center, size.width/2,
                   size.height/2, 0.0f );
            kmVec3Fill( &up, 0.0f, 1.0f, 0.0f);
            kmMat4LookAt(&matrixLookup, &eye,
                         &center, &up);
            kmGLMultMatrix(&matrixLookup);
        }
        break;
        … …
    }

    m_eProjection = kProjection;
    ccSetProjectionMatrixDirty();
}

由于前面m_eProjection已经被赋值为kCCDirectorProjection3D,因此我们只分析 kCCDirectorProjection3D这个case分支。该函数大致进行设置的顺序是:设置视口变换(ViewPort)、设置投影变换矩阵和 设置模型视图变换矩阵。我们分别来看:

 * 设置视口(ViewPort)

void CCDirector::setViewport()
{
    if (m_pobOpenGLView)
    {
        m_pobOpenGLView->setViewPortInPoints(0, 0,
              m_obWinSizeInPoints.width,
              m_obWinSizeInPoints.height);
    }
}

void CCEGLViewProtocol::setViewPortInPoints(float x ,
                     float y , float w , float h)
{
    glViewport((GLint)(x * m_fScaleX
               + m_obViewPortRect.origin.x),
               (GLint)(y * m_fScaleY
               + m_obViewPortRect.origin.y),
               (GLsizei)(w * m_fScaleX),
               (GLsizei)(h * m_fScaleY));
}

这是我们遇到的第一个OpenGL概念:设置视口变换,关于视口变换究竟起到什么作用,后续会细说。

 * 设置“投影变换”矩阵参数

 kmMat4PerspectiveProjection( &matrixPerspective, 60,
        (GLfloat)size.width/size.height, 0.1f, zeye*2);
 kmGLMultMatrix(&matrixPerspective);

 * 设置“模型视图变换”矩阵参数

 kmVec3 eye, center, up;
 kmVec3Fill( &eye, size.width/2,
             size.height/2, zeye );
 kmVec3Fill( &center, size.width/2,
             size.height/2, 0.0f );
 kmVec3Fill( &up, 0.0f, 1.0f, 0.0f);
 kmMat4LookAt(&matrixLookup, &eye,
             &center, &up);

至此,引擎的绘制参数初始化设置就OK了,在你调用setDesignResolutionSize之前,这些参数不会被改变。

二、kazmath

Cocos2d-x引擎最底层采用OpenGL ES 2.0进行图形绘制,这样要想搞清楚前面的问题缘由,对OpenGL那一套技术体系至少要有一些直观认识才行。在这之前,我们还要先了解一些 Cocos2d-x深度使用的kazmath库。根据《Cocos2d-x高级开发教程》书 中说: “因为在Cocos2d-x 2.0采用的OpenGL ES 2.0中,而那些OpenGL ES 1.0函数已经不可使用了。但OpenGL ES 2.0已经放弃了固定的渲染流水线,取而代之的是自定义的各种着色器,在这种情况下变换操作通常需要由开发者来维护。所幸引擎也引入了一套第三方库 Kazmath,它使得我们几乎可以按照原来OpenGL ES 1.0所采用的方式进行开发”。

至此,我们大致知道了Kazmath库是用来辅助我们按照OpenGL ES 1.0的方式管理变换矩阵以及做变换操作的,接下来我们一起来看看kazmath库的结构吧:

//cocos2d-x-2.2.2/cocos2dx/kazmath/src/GL/matrix.c

km_mat4_stack modelview_matrix_stack;
km_mat4_stack projection_matrix_stack;
km_mat4_stack texture_matrix_stack;
km_mat4_stack* current_stack = NULL;
static unsigned char initialized = 0;

以上是Cocos2d-x整个引擎生命周期内会用到的与opengl变换矩阵相关的一些全局变量。

kazmath声明了三个变换矩阵的栈,modelview_matrix_stack(模型视图矩阵栈)、 projection_matrix_stack(投影矩阵栈)以及texture_matrix_stack(纹理矩阵栈)。不过Cocos2d-x引 擎只用到了前两个变化矩阵栈。current_stack指向当前所使用的那个变换矩阵栈。

这些栈的初始化在lazyInitialize中:

void lazyInitialize()
{

    if (!initialized) {
        kmMat4 identity; //Temporary identity matrix

        //Initialize all 3 stacks
        //modelview_matrix_stack =
            (km_mat4_stack*) malloc(sizeof(km_mat4_stack));
        km_mat4_stack_initialize(&modelview_matrix_stack);

        //projection_matrix_stack =
            (km_mat4_stack*) malloc(sizeof(km_mat4_stack));
        km_mat4_stack_initialize(&projection_matrix_stack);

        //texture_matrix_stack =
            (km_mat4_stack*) malloc(sizeof(km_mat4_stack));
        km_mat4_stack_initialize(&texture_matrix_stack);

        current_stack = &modelview_matrix_stack;
        initialized = 1;

        kmMat4Identity(&identity);

        //Make sure that each stack has the identity matrix
        km_mat4_stack_push(&modelview_matrix_stack, &identity);
        km_mat4_stack_push(&projection_matrix_stack, &identity);
        km_mat4_stack_push(&texture_matrix_stack, &identity);
    }
}

kmMat4Identify用于初始化“单位矩阵(Indentify Matrix)”,所谓"单位矩阵",指的是对脚线上元素都为1的矩阵。从kmMat4Identify的实现,我们也可以看出这一点:

kmMat4* const kmMat4Identity(kmMat4* pOut)
{
    memset(pOut->mat, 0, sizeof(float) * 16);
    pOut->mat[0] = pOut->mat[5]
     = pOut->mat[10]
     = pOut->mat[15] = 1.0f;

    return pOut;
}

最后,lazyInitialize函数将单位矩阵分别圧入(km_mat4_stack_push)不同的matrix stack。

再回顾一下CCDirector::setProjection,该函数通过kazmath先后设置了 projection_matrix_stack和modelview_matrix_stack的top元素。

   kmGLMatrixMode(KM_GL_PROJECTION);
   kmGLLoadIdentity();
   kmMat4PerspectiveProjection( &matrixPerspective, 60,
     (GLfloat)size.width/size.height, 0.1f, zeye*2);
   kmGLMultMatrix(&matrixPerspective);
  
   kmGLMatrixMode(KM_GL_MODELVIEW);
   kmGLLoadIdentity();
   kmVec3 eye, center, up;
   kmVec3Fill( &eye, size.width/2,
               size.height/2, zeye );
   kmVec3Fill( &center, size.width/2,
               size.height/2, 0.0f );
   kmVec3Fill( &up, 0.0f, 1.0f, 0.0f);
   kmMat4LookAt(&matrixLookup, &eye,
               &center, &up);
   kmGLMultMatrix(&matrixLookup);

三、精灵绘制

由《Hello,Cocos2d-x》一文我们知道,一旦引擎初始化完毕,就开始了每帧图像的绘制工作,Render Thread在一个“死循环”中反复调用CCDirector的drawScene方法 (CCDisplayLinkDirector::mainLoop中调用了drawScene):

void CCDirector::drawScene(void)
{
    … …
    glClear(GL_COLOR_BUFFER_BIT
           | GL_DEPTH_BUFFER_BIT);
    … …
    kmGLPushMatrix();

    // draw the scene
    if (m_pRunningScene)
    {
        m_pRunningScene->visit();
    }
    … …
    kmGLPopMatrix();
    … …
}

Cocos2d-x采用“渲染树”的方式进行绘制,即先从场景(Scene)的顶层根节点开始,深度优先的递归绘制Child Node。而整个绘制的顶层节点是CCScene。绘制从m_pRunningScene->visit()真正开始。visit是Scene、 Layer、Sprite的共同父类CCNode实现的方法:

void CCNode::visit()
{
    if (!m_bVisible)
    {
        return;
    }
    kmGLPushMatrix();
    … …
    this->transform();
    … …
   
    if(m_pChildren &&
       m_pChildren->count() > 0)
    {
        sortAllChildren();
        // draw children zOrder < 0
        … ..
        // self draw
        this->draw();

        // draw other children nodes
        … …
    } else {
        this->draw();
    }
    … …
    kmGLPopMatrix();
}
   
Visit大致做了这么几件事:
    – 向当前OpenGL变换矩阵栈Push元素
    – 用当前OpenGL变换矩阵栈栈顶元素的变换参数做节点变换
    – 递归绘制zOrder < 0 的子节点
    – 绘制自己
    – 递归绘制其他子节点
    – 从当前OpenGL变换矩阵栈Pop元素

如果你想知道为什么父节点缩放(Scale)、旋转(Rotate)、扭曲(Skew)后,子节点也会跟着父节点同样缩放(Scale)、旋 转(Rotate)、扭曲?其原理就在这里的transform方法中:

void CCNode::transform()
{
    kmMat4 transfrom4x4;

    // Convert 3×3 into 4×4 matrix
    CCAffineTransform tmpAffine
       = this->nodeToParentTransform();
    CGAffineToGL(&tmpAffine,
                 transfrom4x4.mat);

    // Update Z vertex manually
    transfrom4x4.mat[14] = m_fVertexZ;

    kmGLMultMatrix( &transfrom4x4 );
    … …
}

在进入tranform以前,Cocos2d-x做了啥?对了,kmGLPushMatrix():

void kmGLPushMatrix(void)
{
    kmMat4 top;

    lazyInitialize();

    //Duplicate the top of the stack (i.e the current matrix)
    kmMat4Assign(&top, current_stack->top);
    km_mat4_stack_push(current_stack, &top);
}

在引擎初始化后,我们的current_stack是模型视图矩阵栈modelview_matrix_stack。所有设置的初始参数都保 存在该栈的栈顶元素中。在每次Node绘制前,Node都会创建自己的变换矩阵,但这个矩阵不是凭空创造的,从kmGLPushMatrix 可以看出,在当前Node将新创建的矩阵元素圧栈前,它复制了原栈顶元素,也就携带有父节点所有的初始变换信息,也就是说在 km_mat4_stack_push后,栈顶放置的元素其实是原栈顶元素的复制品,而后续所有操作都是基于这个复制品的。这样一来,如果父 节点做了缩放或旋转或扭曲,那这些信息都会作为初始信息作为子节点变换的基础,后续子节点自身的变换参数也都是在这个基础上做出的,最终的矩 阵是transform方法中的kmGLMultMatrix后得出的。真正的矩阵变换计算都在nodeToParentTransform 中,不过要想看懂这个函数,需要对OpenGL有更深入的了解才行,这里略过^_^。

真正绘制Node的方法是CCNode::draw的override方法。CCNode::draw是一个空函数,各个子类 override该方法进行各自的绘制。以CCSprite::draw为例:

void CCSprite::draw(void)
{
    CC_NODE_DRAW_SETUP();

    ccGLBlendFunc( m_sBlendFunc.src, m_sBlendFunc.dst );

    ccGLBindTexture2D( m_pobTexture->getName() );
    ccGLEnableVertexAttribs( kCCVertexAttribFlag_PosColorTex );

#define kQuadSize sizeof(m_sQuad.bl)
    long offset = (long)&m_sQuad;

    // vertex
    int diff = offsetof( ccV3F_C4B_T2F, vertices);
    glVertexAttribPointer(kCCVertexAttrib_Position, 3,
     GL_FLOAT, GL_FALSE, kQuadSize, (void*) (offset + diff));

    // texCoods
    diff = offsetof( ccV3F_C4B_T2F, texCoords);
    glVertexAttribPointer(kCCVertexAttrib_TexCoords, 2,
      GL_FLOAT, GL_FALSE, kQuadSize, (void*)(offset + diff));

    // color
    diff = offsetof( ccV3F_C4B_T2F, colors);
    glVertexAttribPointer(kCCVertexAttrib_Color, 4,
           GL_UNSIGNED_BYTE, GL_TRUE,
           kQuadSize, (void*)(offset + diff));

    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
    … …
}

这里的draw是一个典型的OpenGL绘制工序。CC_NODE_DRAW_SETUP()将之前的经过若干准备而得到的最终各类变换矩阵 整合并传给OpenGL:

/** @def CC_NODE_DRAW_SETUP
 Helpful macro that setups the GL server state,
 the correct GL program and sets the Model View
 Projection matrix
 @since v2.0
 */
#define CC_NODE_DRAW_SETUP() \
do { \
    ccGLEnable(m_eGLServerState); \
    CCAssert(getShaderProgram(), "No shader program set for this node"); \
    { \
        getShaderProgram()->use(); \
        getShaderProgram()->setUniformsForBuiltins(); \
    } \
} while(0)

void CCGLProgram::setUniformsForBuiltins()
{
    kmMat4 matrixP;
    kmMat4 matrixMV;
    kmMat4 matrixMVP;

    kmGLGetMatrix(KM_GL_PROJECTION, &matrixP);
    kmGLGetMatrix(KM_GL_MODELVIEW, &matrixMV);

    kmMat4Multiply(&matrixMVP, &matrixP, &matrixMV);

    setUniformLocationWithMatrix4fv(m_uUniforms[kCCUniformPMatrix],
                                    matrixP.mat, 1);
    setUniformLocationWithMatrix4fv(m_uUniforms[kCCUniformMVMatrix],
                                    matrixMV.mat, 1);
    setUniformLocationWithMatrix4fv(m_uUniforms[kCCUniformMVPMatrix],
                                    matrixMVP.mat, 1);
    … …
}

经过计算顶点、绑定纹理等步骤后,最终由glDrawArrays完成Node绘制。

四、m_fScaleX和m_fScaleY都是1.0,背景精灵为何被放大?

根据上面的分析,我们了解到“子节点将跟随父节点的缩放而缩放”。据此,我们来分析一下前面提到的屏幕适配例子中的第三种情况,即屏幕大小为 960×640,按照Cocos2d-x屏幕适配指南Wiki中的做法,调用 pEGLView->setDesignResolutionSize(480, 320)。在该情况中,我们得到的结果是480×320大小的背景图片充满了大小为960×640的屏幕窗口,这给我们的直观印象就是背景图片被放大了一 倍。下面我们就尝试用上面的分析来解释一下这个现象。

在这个例子中,渲染树结构如下:
   CCScene
        – CCLayer
            – CCSprite – 背景图精灵

按照之前的理论,背景图精灵自身或父类应该有缩放的设置,比如m_fScaleX = 2.0之类的设置,于是我在代码中输出了Scene、Layer以及Sprite的m_fScaleX和m_fScaleY值。但出乎预料的是,这些 Node子类的两个轴向缩放值都保持了默认值,即1.0f。在代码里翻了半天,也的确没有找到改写Scene、Layer或Sprite Scale的地方。又一想:代码中调用了setDesignResolutionSize,这样CCEGLView的m_fScaleX = m_fScaleY = 2.0f,难道是CCEGLView的m_fScale传递给了CCScene等Node子类,但事实总是残酷的,代表这一联系的代码也始终未被我所找 到,看来继续纠结m_fScale的值设置是无法搞清楚真正原因,应该换换思路了。这里背景图的放大不应该是Node scale值设置的问题,也就是说关键环节不应该在绘制流程,而是在之前的OpenGL变换矩阵参数设置,看来不再深入学习点OpenGL知识,这个问题 就很难搞定了,于是开始翻看《OpenGL编程指南7th》(号称OpenGL红宝书)和《OpenGL超级宝典》(号称OpenGL蓝宝 书)。虽然我的阅读是粗粒度的,但还是收获到了一些答案。

五、OpenGL基础

OpenGL是帮助我们将三维世界的物体转换到二维屏幕上的一组接口。在新技术尚未出现之前,我们的屏幕永远是二维的,即便是现在的3D电影 也是双眼视角二维图像叠加的结果。我们知道“将大象装进冰箱总共分三 步”,将一个三维模型转换到二维屏幕上,OpenGL也规定了相对流水线般的步骤。

OpenGL三维图形的显示流程

三维图形显示流程中,涉及到OpenGL的一个重要操作,那就是“变换(Transformation)”,主要的变换包括模型视图变换 (model-view transformation)、投影变换(projection transformation)以及视口变换(ViewPort transformation)。我们经常用相机模拟来对比OpenGL解决这一问题的过程以及相关概念。

回顾一下我们自己用相机拍照的步骤吧。

第零步,选景。景就是所谓的三维模型或三维物体,或简称模型(Model),就是我们要显示到屏幕上的物体;
第一步,确定相机位置。让相机以一定的距离、高度、角度对准模型。在这里,相机的位置变换,对应OpenGL的“视图变换或叫视点变换 (View Transformation)”。在这一步里(对应上面图中的第二步),我们还可以调整三维物体的相对位置、角度与相机的距离,这就是模型变换 (Modeling Transformation),两种变换达成的效果是相同的,因此总称模型视图变换(Model-View Transformation)。
第二步,选镜头,并调焦。确定图像投影在胶片上的范围以及景深等。这一步叫投影变换(Projection Transformation)。
第三步,冲洗照片。拍摄好的图像放在底片上,但我们需要选择冲洗后最终是放在6寸相纸还是20寸相纸上,显然在不同大小相纸上,图像的显示效 果不同(比如大小)。这个过程叫视口变换(Viewport Transformation)。

三维空间的物体都是用三维坐标描述的,谈到坐标就离不开坐标系,OpenGL中的坐标系就有多种,我们最常用的就是世界坐标系。

世界坐标系是以屏幕中心为原点(0, 0, 0),你面对屏幕,你的右边是x正轴,上面是y正轴,屏幕指向你的为z正轴。无论如何变换,世界坐标系都不动。我们在Cocos2d-x中设置 初始参数时,参数的单位多为世界坐标系中的单位。

视点变换时会涉及到视点坐标系,但这个变换由opengl接口来负责,我们不用过多关心。

绘图坐标系(局部坐标系),当前绘图坐标系是绘制物体时的坐标系。程序刚初始化时,世界坐标系和当前绘图坐标系是重合的,当用 glTranslatef()等变换函数做移动和旋转时,都是改变的当前绘图坐标系,改变的位置都是当前绘图坐标系相对自己的x,y,z轴所做的 改变,改变以后,再绘图时,都是在当前绘图坐标系进行绘图,所有的函数参数也都是相对当前绘图坐标系来讲的。

屏幕坐标系,即终端屏幕上的坐标系,与世界坐标系有不同,它以屏幕左上角的点为原点,向右是x正轴,向下是y正轴,屏幕指向你的为z正轴。

注意视口(Viewport)的设置是以实际屏幕坐标定义了窗口中的区域,长度宽度都是以实际像素为单位。当然引擎在精灵绘图时用 的是绘图坐标系,我们理解原点在左下角即可。

六、Cocos2d-x各种变换矩阵的初始参数设置

前面说过,Cocos2d-x在CCDirector::setProjection中完成了对变换矩阵的初始参数设置,我们逐一来看看这些设置对模型映射后的二维图像有何影响,这也是理解篇头几个问题的关键环节。

  * 投影变换
   
    前面提到过,投影变换相当于调节相机镜头。OpenGL中提供了两种投影方式,一种是正射投影,另一种是透视投影。Cocos2d-x使用的是透视投影 (Perspective Projection)。透视投影是实际人们观察事物的真实反馈,即离视点近的物体大,离视点远的物体小,远到极点即为消失,成为灭点。Cocos2d- x使用的是kmMat4PerspectiveProjection,对应OpenGL中的gluPerspective,该方法创建一个对称透视视景体 (View Volumn),见下图:

gluPerspective的函数原型如下:void gluPerspective(GLdouble fovy,GLdouble aspect,GLdouble zNear, GLdouble zFar);

    参数fovy定义视野在X-Z平面的角度,范围是[0.0, 180.0],也就是上图中的“视角”;
    参数aspect是投影平面宽度与高度的比率;
    参数zNear和Far分别是近远裁剪面沿Z负轴到视点的距离,它们总为正值。
  
Cocos2d-x中是这么设置投影变换矩阵的:

  float zeye = this->getZEye();
  kmMat4PerspectiveProjection( &matrixPerspective, 60, (GLfloat)size.width/size.height, 0.1f, zeye*2);

  float CCDirector::getZEye(void)
  {
    return (m_obWinSizeInPoints.height / 1.1566f);
  }

从参数上来看,
    视角 = 60度
    宽高比 = 设计分辨率的宽高比,
    近平面 = 距离视点0.1f,几乎与视点重合
    远平面 = 距离视点zeye * 2距离。
    视点位置 = 设计分辨率.height / 1.1566f

投影是用来对模型进行截取的,只有在投影变换所建立的平头截体(Frustum,投影的近、远两个截面以及其他四个面构成的立体体)内的模型部分才会被最终映射和显示。我们用下面的图来直观了解一下各个参数在三维空间的概念吧。

显然引擎如此设置投影矩阵的参数是有考虑的:
首先就是投影平头截体的宽高比 = 设计分辨率的宽高比,这样设置使得一切符合设计分辨率宽高比的模型都可以被理想截取。
其次,视角60度,zEye的在Z轴正方向距离世界原点的距离 = (m_obWinSizeInPoints.height / 1.1566f),这里的1.1566f是怎么来的呢?我们沿着X轴负方向向zy平面投影,得到下图:

看这个图,让我想起了初中几何,通过60度的视角,我们可以推断由eye、XZ截断上平面与Y轴的交点、XZ截断下平面与Y轴的交点组成一个等边三角形, 现在我们已知在Zy平面投影中视点与原点的距离为m_obWinSizeInPoints.height / 1.1566f, 我们还知道夹角是60度,我们求一下投影在(z=0,XY平面)的截面高度h。

cos30 = (m_obWinSizeInPoints.height / 1.1566f)/ h
h = (m_obWinSizeInPoints.height / 1.1566f)/cos30 = m_obWinSizeInPoints.height;

我们计算出来的结果是 h = m_obWinSizeInPoints.height = 设计分辨率中的高度分量。这意味这什么呢?Cocos2d-x是2D游戏渲染引擎,针对该引擎的模型的z坐标都是0,因此模型实际上就在xy平面内,也就 是说eye与原点的距离恰好就是eye与模型的距离,而模型可显示区域的最大高度也就是h,即m_obWinSizeInPoints.height。这 个结论会在后续问题分析时发挥作用。

注意虽然这里知道eye在Z轴正方向距离世界原点的距离,但eye的(x, y)坐标在投影设置后依旧无法确认,我们需要在设置模型视图变换时得到eye的(x, y)坐标。

  * 视图变换

    kmGLMatrixMode(KM_GL_MODELVIEW);
    kmGLLoadIdentity();
    kmVec3 eye, center, up;
    kmVec3Fill( &eye, size.width/2, size.height/2, zeye );
    kmVec3Fill( &center, size.width/2, size.height/2, 0.0f );
    kmVec3Fill( &up, 0.0f, 1.0f, 0.0f);
    kmMat4LookAt(&matrixLookup, &eye, &center, &up);
    kmGLMultMatrix(&matrixLookup);

OpenGL原生的视图变换参数设置方法是gluLookAt,在kazmath中对应的方法为kmMat4LookAt。gluLookAt的函数原型是:

    void gluLookAt(GLdouble eyex, GLdouble exey, GLdouble eyez,
       GLdouble centrex, GLdouble centrey, GLdouble centrez,
       GLdouble upx, GLdouble upy, GLdouble upz);

eye的坐标(eyex, eyey, eyez), Cocos2d-x中是这么设置的kmVec3Fill( &eye, size.width/2, size.height/2, zeye )。可以看出eye在xy平面的投影恰好是以屏幕分辨率构成的矩形的中心。

centre坐标,表示的是视线方向,该方向矢量是由eye坐标、centre坐标共同构成的,由eye指向center。Cocos2d-x的设置 kmVec3Fill( &center, size.width/2, size.height/2, 0.0f )。x, y坐标与eye的相同,因此视线平行于Z轴。

最后的up参数可以理解为头顶方向,这里设置为Y轴方向。

可以看出,eye就在投影区的中心,由于投影区的高度为size.height(投影变换时分析得到的),这样根据投影矩阵设置的宽高比,得出该投影区的宽度也恰为size.width。

七、再分析

有了以上关于Cocos2d-x引擎的了解,我们再回过头来用OpenGL的变换原理对篇头的三种情况做分析。

 1) 屏幕大小480×320,未做任何屏幕适配工作,不调用pEGLView->setDesignResolutionSize。结果:背景图充满窗口。

    在这种情况下,各个OpenGL变换矩阵参数值如下:
        eye视点坐标(240, 160, 320/1.1566f);
        投影变换矩阵在xy平面的截面区域恰好是480×320;
        背景图锚点位置(240, 160, 0);

    在这种情况下,截面区域恰与背景图重合,显示在屏幕上后,背景图恰充满窗口,见下图:

   
   
 2) 屏幕大小960×640,未做任何屏幕适配工作,不调用pEGLView->setDesignResolutionSize。结果:背景图未充满窗口,四周有较大黑边。
 
    在这种情况下,各个OpenGL变换矩阵参数值如下:
        eye视点坐标(480, 320, 480/1.1566f);
        投影变换矩阵在xy平面的截面区域是960×640;
        而背景图锚点位置(480, 320, 0);

    因此背景图(480×320)未能完整充满截面区域(960×640),背景图周围将有较大黑边,见下图:
   
     

 3) 屏幕大小同为960×640,按照上面Cocos2d-x屏幕适配指南Wiki中的做法,调用pEGLView->setDesignResolutionSize(480, 320)。结果:背景图放大为原来2倍,充满屏幕窗口。

    在这种情况下,各个OpenGL变换矩阵参数值如下:
        eye视点坐标(240, 160, 320/1.1566f);
        投影变换矩阵在xy平面的截面区域是480×320;
        而背景图锚点位置(240, 160, 0);

    在这种情况下,截面区域恰与背景图重合。但这里需要注意的是现在屏幕是960×640,而截面区域仅仅是480×320,为何映射后,背景图充满屏幕了呢?这里就不能不提到视口的作用了。

    前面说过视口相当于相片,现在我们拍摄出的图片是480×320的,但我们选择的底片Viewport却是960×640的,怎么办,在视口转换 时,OpenGL自动将480×320的图片映射到960×640的底片上,相当于对图像进行的放大。而960×640的视口恰好与屏幕窗口大小一致且坐 标重叠,于是我们就在屏幕上看到了一个铺满屏幕的背景图,见下图:

   

 4) 我们再来说两个有关视口的例子

    以第三种情况为基础,我们修改一下引擎代码,看看视口的作用。
   
    我们手工将CCDirector::setViewport()中的:
        m_pobOpenGLView->setViewPortInPoints(0, 0, m_obWinSizeInPoints.width, m_obWinSizeInPoints.height);
    改为:
        m_pobOpenGLView->setViewPortInPoints(0, 0, m_obWinSizeInPoints.width/2, m_obWinSizeInPoints.height/2);

    这样修改后,Viewport从point(0,0), rect (960×640)变成了point(0,0), rect (480×320)。也就是说用照相机拍出的景物大小是480×320,底片也是480×320,但屏幕是960×640,我们可以将屏幕理解为相框,把 一张480×320的照片,放到960×640大小的相框里,相片只能占据相框的四分之一。这个例子的最终屏幕显示结果见下图:

   

    前面的例子中背景图片size均小于屏幕大小,我们再来举一个资源图片大于屏幕大小的例子,看看经过一系列变换会得到什么样的结果。
   
    首先将CCDirector::setViewport()中的代码恢复原先状态。然后我们准备一张1024×768(>屏幕的960×640)的 背景图片"HelloWorld-1024×768.jpg",修改HelloWorldScene.cpp,将:
    CCSprite* pSprite = CCSprite::create("HelloWorld.png");
    修改为:
    CCSprite* pSprite = CCSprite::create("HelloWorld-1024×768.png");

    注释掉AppDelegate.cpp中的pEGLView->setDesignResolutionSize调用,这样更直观。

    这样修改后,各参数如下:
        eye视点坐标(480, 320, 640/1.1566f);
        投影变换矩阵在xy平面的截面区域是960×640;
        而背景图锚点位置(480, 320, 0);
        Viewport point(0,0), rect (960×640)
   
    由于背景资源图片太大(1024×768),大于我们的投影截面区域960×640,因此模型真正能显示的部分仅仅是投影截面区域中的那960×640范围内的图片。于是显示结果如下:

   

    矩阵变换过程如下:

   

    投影截面区域与视口区域重叠,这里就不再赘述了。

八、CCDirector::m_fContentScaleFactor

决定图像在屏幕上的最终显示结果的因素还有一个,那就是CCDirector::m_fContentScaleFactor。在最初的HelloCpp例子中,我们能看到这样的代码:

    if (frameSize.height > mediumResource.size.height)
    {
        searchPath.push_back(largeResource.directory);
        pDirector->setContentScaleFactor(
          MIN(largeResource.size.height/designResolutionSize.height,
              largeResource.size.width/designResolutionSize.width));
    }
    … …

    可以看出这个contentScaleFactor存储的是资源分辨率与设计分辨率的比值。我们还是用例子来看看该元素对显示的影响。我们在第一种情况的基础上验证。

    第一种情况:屏幕480×320,未调用setDesignResolutionSize,资源大小480×320。结果:图片充满屏幕。

    现在我们增加并使用一个新资源:HelloWorld-960×640.png,这个图片大小960×640,是屏幕大小的二倍,根据上面的分析,我们很容易猜测到最终结果是:只有图片中央区域(480×320)可以显示出来,其余部分被投影矩阵截掉。

    现在我们使用setContentScaleFactor,在AppDelegate.cpp中做如下调用:

    pDirector->setContentScaleFactor(MIN(960/480, 640/320));

    这样我们得到的m_fContentScaleFactor = 2。而我们编译运行后得到的结果是:图片铺满整个屏幕。为什么会这样呢?

    我们在代码中搜索contentScaleFactor,我们找到一些宏和调用:

   
#define CC_CONTENT_SCALE_FACTOR() CCDirector::sharedDirector()->getContentScaleFactor()

CCSize CCTexture2D::getContentSize()
{

    CCSize ret;
    ret.width = m_tContentSize.width / CC_CONTENT_SCALE_FACTOR();
    ret.height = m_tContentSize.height / CC_CONTENT_SCALE_FACTOR();

    return ret;
}

#define CC_RECT_PIXELS_TO_POINTS(__rect_in_pixels__)                                                                        \
    CCRectMake( (__rect_in_pixels__).origin.x / CC_CONTENT_SCALE_FACTOR(), (__rect_in_pixels__).origin.y / CC_CONTENT_SCALE_FACTOR(),    \
            (__rect_in_pixels__).size.width / CC_CONTENT_SCALE_FACTOR(), (__rect_in_pixels__).size.height / CC_CONTENT_SCALE_FACTOR() )

… …

bool CCSprite::initWithTexture(CCTexture2D *pTexture)
{
    CCAssert(pTexture != NULL, "Invalid texture for sprite");

    CCRect rect = CCRectZero;
    rect.size = pTexture->getContentSize();

    return initWithTexture(pTexture, rect);
}

    这些代码都在告诉我们,如果m_fContentScaleFactor = 2,那代码会对Sprite的纹理进行缩放,让上面得到的数据是经过contentScaleFactor变换的,我们可以认为我们所用的实际资源大小是 原资源的1/m_fContentScaleFactor即可。

Cocos2d-x 3.0rc0集成Google AdMob SDK

No Comments

话说Cocos2d-x 3.0上一周迫不及待地发布了正式版,本是一件值得庆幸的事情。但由于不可解决的技术问题,引擎无奈将Android平台的NativeActivity 实现重新回退到了Cocos2d-x 2.2.x版本的实现方案。由于之前已经将 GameDemo移植到了Cocos2d-x 3.0rc0版,直观感受到了NativeActivity方案带来的游戏操作体验上的提升(触屏事件的响应),因此心里总是“挂念”引擎的 NativeActivity方案。按照Cocos2d-x官方人士的说法,只要NDK的问题修复,NativeActivity还是未来引擎在 Android平台上的第一选择。我的理解,将来Cocos2d-x的某个版本中还会恢复NativeActivity的实现方案。

上次只是将GameDemo的核心逻辑移植到了Cocos2d-x 3.0rc0上,但一些外部SDK集成尚未做完,这两天闲遐时开始着手研究如何做,而首先要移植的就是Google AdMob SDK。关于Cocos2d-x 3.0rc0集成Google AdMob SDK,网上已经有了技术方案原型,最初应该是一个外国开发者提出的方案,后来被CocoaChina cocos2d-x社区版主翻译成了中文教程,这里基本上也是参考的这个方案,只是做了局部细化和进一步说明,希望能帮助大家进一步解惑。

一、功能说明

GameDemo游戏分为三个场景:WelcomeScene、GameScene以及EndScene。集成AdMob SDK的功能要求如下:

    – 当游戏进入到WelcomeScene时,AdMob SDK完成初始化,发出Ad Request,当收到Ad的时候才会在屏幕上方显示带有Ad的窗口;
    – 当点击Start进入到GameScene时,隐藏Ad窗口,以不干扰玩家的游戏操作为优先;
    – 当游戏Over进入到EndScene的时候,恢复显示Ad窗口;
    – 当玩家点击“Retry”回到GameScene时,隐藏Ad窗口。

二、方案原理

这个方案就是通过android.widget.PopupWindow实现广告窗口悬浮在当前窗口之上。按照Android官方Doc中的说 明,PopupWindow用于实现一个弹出框,它可以使用任意布局的View作为其内容,这个弹出框是悬浮在当前activity之上的。

至于PopupWindow为何能显示在当前Activity之上,可以查看PopupWindow的源码,大致思路就是PopupWindow通过 new时传入的context(当前Activity)获得了当前WindowManager,并将AdView作为子View添加到该窗口中。这里简要列出一些关键源码,大家可粗略理解一下:

    public PopupWindow(Context context, AttributeSet attrs,
         int defStyleAttr, int defStyleRes) {
        mContext = context;
        mWindowManager = (WindowManager)context
               .getSystemService(Context.WINDOW_SERVICE);
        …. …
    }

    public void setContentView(View contentView) {
        if (isShowing()) {
            return;
        }

        mContentView = contentView;

        if (mContext == null && mContentView != null) {
            mContext = mContentView.getContext();
        }

        if (mWindowManager == null && mContentView != null) {
            mWindowManager = (WindowManager) mContext
                  .getSystemService(Context.WINDOW_SERVICE);
        }
    }

    public void showAtLocation(View parent, int gravity, int x, int y) {
        showAtLocation(parent.getWindowToken(), gravity, x, y);
    }

    public void showAtLocation(IBinder token, int gravity, int x, int y) {
        if (isShowing() || mContentView == null) {
            return;
        }

        unregisterForScrollChanged();

        mIsShowing = true;
        mIsDropdown = false;

        WindowManager.LayoutParams p = createPopupLayout(token);
        p.windowAnimations = computeAnimationResource();
      
        preparePopup(p);
        if (gravity == Gravity.NO_GRAVITY) {
            gravity = Gravity.TOP | Gravity.START;
        }
        p.gravity = gravity;
        p.x = x;
        p.y = y;
        if (mHeightMode < 0) p.height = mLastHeight = mHeightMode;
        if (mWidthMode < 0) p.width = mLastWidth = mWidthMode;
        invokePopup(p);
    }
   
    private void invokePopup(WindowManager.LayoutParams p) {
        if (mContext != null) {
            p.packageName = mContext.getPackageName();
        }
        mPopupView.setFitsSystemWindows(mLayoutInsetDecor);
        setLayoutDirectionFromAnchor();
        mWindowManager.addView(mPopupView, p);
    }

UI线程负责更新窗口,因此popupWindow的创建与操作都应该通过runOnUiThread传递给UI线程执行。

游戏逻辑的C++层代码通过Jni控制PopupWindow的Show和dismiss。别忘了C++层的代码是在渲染线程执行的哦,这也是为何要用handler和runOnUIThread的原因。

在Cocos2d-x 2.2.2版本集成AdMob时,AdMob只是在收到ad后才显示出广告Banner,但是在cocos2d-x 3.0rc0中,当广告加载未成功时,android.widget.PopupWindow会显示一个小黑框,特难看,因此我们需要自己来控制。

三、集成步骤

【AdMob准备】

  到Google AdMob注册一个帐号,如果你有Google帐号,那直接开通AdMob(需填写更加详细的信息)。
  下载Google AdMob SDK,之前一直用GoogleAdMobAdsSdk-6.4.1.jar,这里还以这个版本为例。但Google官方已经不推荐这个版本了,你可以下 载Google Play Services版本的Mobile Ads API,但代码与这里会稍有不同。将下载的jar包放到GameDemo/proj.android/libs中。
 
【修改AndroidManifest.xml】

  在<Application>标签里添加:

  <activity
     android:name="com.google.ads.AdActivity"
     android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|uiMode|screenSize|smallestScreenSize" />
  <meta-data
     android:name="ADMOB_PUBLISHER_ID"
     android:value="YOUR_PUBLISHER_ID_VALUE" />

  权限方面至少包含如下两个:
   <uses-permission android:name="android.permission.INTERNET"/>
   <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>

【Java层代码集成】

我们只需要修改GameDemoActivity.java这个文件。

import android.app.NativeActivity;
import android.os.Bundle;
import android.util.Log;

import android.os.Handler;
import android.os.Message;
import android.view.Gravity;
import android.view.View;
import android.view.Gravity;
import android.view.ViewGroup.LayoutParams;
import android.view.ViewGroup.MarginLayoutParams;
import android.view.WindowManager;
import android.widget.LinearLayout;

import android.widget.PopupWindow;
import com.google.ads.AdRequest;
import com.google.ads.AdSize;
import com.google.ads.AdView;
import com.google.ads.AdListener;
import com.google.ads.Ad;

public class GameDemoActivity extends NativeActivity{
    private static GameDemoActivity context;

    private AdView adView;
    private PopupWindow popUpWindow;
    private LinearLayout popupWindowLayout;
    private LinearLayout mainActivityLayout;
    private boolean hasAdReceived;

    private static Handler adHandler = new Handler() {
        public void handleMessage(Message msg) {
            if (!context.hasAdReceived)
                return;

            switch (msg.what) {
                case 0:
                    if (View.VISIBLE ==
                        context.adView.getVisibility()) {
                        context.adView.setVisibility(View.GONE);
                        context.popUpWindow.dismiss();
                    }
                    break;
                case 1:
                    if (View.VISIBLE !=
                        context.adView.getVisibility()) {
                        context.adView.setVisibility(View.VISIBLE);
                        context.popUpWindow.showAtLocation(
                           context.mainActivityLayout, Gravity.TOP, 0, 0);
                    }
                    break;
            }
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
        context = this;
        adView = new AdView(this, AdSize.BANNER, "a1533d6de900e31");
        adView.setAdListener(new AdmobListener());
        //初始时,广告View隐藏起来
        adView.setVisibility(View.GONE);
    }

    public static void setupAds(){
        context.initAdPopupWindow();
    }

    public void initAdPopupWindow() {
        if (adView != null) {
            context.runOnUiThread(new Runnable() {
                 @Override
                 public void run() {
                     MarginLayoutParams params = new MarginLayoutParams(
                                     LayoutParams.WRAP_CONTENT,
                                     LayoutParams.WRAP_CONTENT);
                     params.setMargins(0, 0, 0, 0);

                     popupWindowLayout = new LinearLayout(context);
                     popupWindowLayout.setPadding(-5, -5, -5, -5);
                     popupWindowLayout.setOrientation(LinearLayout.VERTICAL);
                     popupWindowLayout.addView(adView, params);

                     popUpWindow = new PopupWindow(context);
                     popUpWindow.setWidth(320);
                     popUpWindow.setHeight(50);
                     popUpWindow.setWindowLayoutMode(LayoutParams.WRAP_CONTENT,
                                               LayoutParams.WRAP_CONTENT);
                     popUpWindow.setClippingEnabled(false);
                     popUpWindow.setContentView(popupWindowLayout);

                     mainActivityLayout = new LinearLayout(context);
                     context.setContentView(mainActivityLayout, params);

                     context.hasAdReceived = false;

                     AdRequest adRequest = new AdRequest();
                     //测试Admob时使用测试Device,发布版需要去掉这行代码
                     adRequest.addTestDevice("CCE4220B2509A406B515E7C9A205AEE1");
                     context.adView.loadAd(adRequest);

                     popUpWindow.update();
                }
            });
        }
    }

    //GoogleAdMobAdsSdk-6.4.1版本中的AdListener是interface,因此
    //我们需要override所有接口,但只有onReceiveAd是我们关心的。
    private class AdmobListener implements AdListener {
        @Override
        public void onReceiveAd(Ad ad) {
            //只有第一次成功接收Ad后,我们后续才显示广告窗口,否则
            //popupWindow会显示为一个小黑框,特难看。
            Log.d("GameDemo", "onReceiveAd");
            if (!hasAdReceived){
                hasAdReceived = true;               
            }
        }

        @Override
        public void onFailedToReceiveAd(Ad ad,
                            AdRequest.ErrorCode error) {
            Log.d("GameDemo",
              "failed to receive ad (" + error+ ")");
        }

        @Override
        public void onPresentScreen(Ad ad) {
            Log.d("GameDemo", "onPresentScreen");
        }

        @Override
        public void onDismissScreen(Ad ad) {
            Log.d("GameDemo", "onDismisScreen");
        }

        @Override
        public void onLeaveApplication(Ad ad) {
            Log.d("GameDemo", "onLeaveApp");
        }
    }

    public static void setAdVisible(boolean b) {
       Message msg = new Message();
        if (b) {
            msg.what = 1;
        } else {
            msg.what = 0;
        }
        adHandler.sendMessage(msg);
    }
}

【C++层代码集成】

在AppDelegate.cpp中添加setupAds方法:

void AppDelegate::setupAds()
{
#if (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID)
    JniMethodInfo t;
    if (JniHelper::getStaticMethodInfo(t, "com/tonybai/game/GameDemoActivity", "setupAds", "()V")) {
        t.env->CallStaticVoidMethod(t.classID, t.methodID);
        if (t.env->ExceptionOccurred()) {
            t.env->ExceptionDescribe();
            t.env->ExceptionClear();
            return;
        }
        t.env->DeleteLocalRef(t.classID);
    }
#endif
}

在WelcomeScene的init方法中调用setupAds:

bool WelcomeScene::init()
{
    bool bRet = false;
    do {
       … …
       AppDelegate *app = (AppDelegate*)Application::getInstance();
       app->setupAds();

       bRet=true;
    } while(0);

    return bRet;
}

在点击Start按钮时,隐藏Ad:

void WelcomeScene::menuStartCallback(Ref* pSender)
{
    AppDelegate *app = (AppDelegate*)Application::getInstance();

    app->setAdVisible(false);
    GameScene *gameScene = GameScene::create();
    Director::getInstance()->replaceScene(gameScene);
}

集成步骤到此就结束了,编译ok就可以部署到模拟器上运行测试一番了。

四、使用Cocos2d-x 3.0rc0引擎遇到的两个问题

【问题1】用cocos2d-x 3.0rc0的cocos编译高版本引擎生成的工程遇到的问题

 cocos2d-x 3.0rc0生成的proj.android/build-cfg.json与高版本(rc1~正式版)  cocos生成的工程的build-cfg.json稍有不同,用cocos2d-x 3.0rc0版cocos编译高版本cocos生成的project,会提示build_android组件无法找到“copy_to_assets”。 因此需要手动修改proj.android/build-cfg.json,将其中的:

    "copy_resources": [
        {
            "from": "../Resources",
            "to": ""
        }
    ],

改为:

   "copy_to_assets" :[
            "../Resources/"
            ],

【问题2】 W/dalvikvm(1194): dvmFindClassByName rejecting 'org/cocos2dx/lib/Cocos2dxHelper'

使用cocos2d-x 3.0rc0编译的项目,总是出现如下问题,但似乎这个错误还不影响程序的运行:

04-29 09:44:34.968: D/JniHelper(1194): JniHelper::setJavaVM(0xb8835730), pthread_self() = B897B518
04-29 09:44:34.968: W/dalvikvm(1194): dvmFindClassByName rejecting 'org/cocos2dx/lib/Cocos2dxHelper'

Google了一下,这个问题很多童鞋都遇到了,但给出的solution却不甚令我满意,于是我就求甚解的细致挖掘了一下,终于找到了一个让我还算满意 的答案。我们分析一下这两条日志,rejecting那条日志总是伴随setJavaVM后面出现的。可引擎什么时候setJavaVM了呢?显然是在 native_app_glue创建的子进程中,引擎需要attachCurrentThread,获得JniEnv时才做的。于是我们打开cocos2d-x-3.0rc0/cocos/2d/platform/android/nativeactivity.cpp一看究竟。

在nativeactivity.cpp中有两处"org/cocos2dx/lib/Cocos2dxHelper",我们先看engine_handle_cmd中的那处:

static void engine_handle_cmd(struct android_app* app, int32_t cmd)
{
        …. ….
        case APP_CMD_INIT_WINDOW:
            LOG_RENDER_DEBUG("android_main : 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;
    … …
}

初步可以断定,那两条日志就是执行到这里输出的,但为何dvmFindClassByName方法会rejecting “org/cocos2dx/lib/Cocos2dxHelper”这个类名呢?我们还得翻看dalvik虚拟机源码:

 /dalvik/vm/native/InternalNative.cpp

/*
 * Find a class by name, initializing it if requested.
 */
ClassObject* dvmFindClassByName(StringObject* nameObj, Object* loader,
        bool doInit)
{
    ClassObject* clazz = NULL;
    char* name = NULL;
    char* descriptor = NULL;
    if (nameObj == NULL) {
        dvmThrowNullPointerException("name == null");
        goto bail;
    }
    name = dvmCreateCstrFromString(nameObj);
    /*
     * We need to validate and convert the name (from x.y.z to x/y/z). This
     * is especially handy for array types, since we want to avoid
     * auto-generating bogus array classes.
     */
    if (!dexIsValidClassName(name, true)) {
        ALOGW("dvmFindClassByName rejecting '%s'", name);
        dvmThrowClassNotFoundException(name);
        goto bail;
    }
    … …
}

我们的确找到了输出rejecting日志的地方,通过注释我们可以看到这个方法是用来验证名字对象,并将x.y.z形式的名字转换成x/y/z的。但引 擎中传入的就是“x/y/z”格式,因此这个方法输出了错误日志。我尝试将上面engine_handle_cmd中的"org/cocos2dx/lib/Cocos2dxHelper"改成"org.cocos2dx.lib.Cocos2dxHelper",错误日志果然消失了。

不过目前仍不能解释一点:为何在其他位置,比如在前面的AppDelegate::setupAds中,使用x/y/z格式的jni调用参数却没有输出错误日志!难道两个位置dalvik虚拟机使用的是不同的名字对象查找和加载方法?这个目前尚无定论。

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

7 Comments

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

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

Cocos2d-x 3.0rc2集成ShareSDK

1 Comment

给自己的手机游戏增加些社交分享功能,有助于游戏宣传和提升知名度,是一种不错的社交营销手段。国内这方面的第三方插件有不少,比如ShareSDK友 盟分享组件Baidu分享组件等,之前在研究2.2.2版本时,集成了ShareSDK这个组件,这次迁移到Cocos2d-x 3.0rc2依旧选择集成ShareSDK,这里就来说说集成的过程,遇到的一些问题以及解决方法。这里仅以Android平台游戏集成为例。

一、功能描述、SDK版本和帐号准备

功能大致是这样的:在游戏中设置一个按钮,点击这个按钮,弹出知名社交平台的分享图标集窗口,用户选择分享目标后,相关信息分享到对应的社交平台。分享结果通知通过Toast显示在屏幕的下方。

这次依旧使用ShareSDK for Android 2.3.7版本(ShareSDK-Android-2.3.7),Cocos2d-x的版本为3.0rc2

集成前,你需要有一个基于Cocos2d-x 3.0rc2的可运行的Android平台游戏project,我们的集成就基于该project,这里我们的project名为GameDemo,GameDemo的源码结构大致是:

GameDemo/
    – Classes/
    – proj.android/
    – Resources/
    – cocos2d/
    – CMakeLists.txt
    – … …

使用ShareSDK前,你需要在各大主流社交平台(微信微博)申请开发者帐号以及游戏接入权限(app_key、app_secret)等,当然在ShareSDK站点也应该有自己的帐号和应用AppKey,这些申请的审核需要几个工作日,甚至更长。

二、ShareSDK集成步骤

按照ShareSDK官方manual说法,Cocos2d-x集成ShareSDK有三种方式,之前在Cocos2d-x 2.2.2引擎中采用的是专用组件集成的方式,该组件(C2DXShareSDKSample)可以在这里下载(该组件近期已经fix了我之前发现的bug)。

1.  jar包集成

这次我们主要做微博、微信的社交分享,因此只需要微博、微信相关jar包。在C2DXShareSDKSample/proj.android/libs下,我们找到以下几个jar包:

  -rw-rw-r– 1 tonybai tonybai  97K  4月  8 18:10 mframework.jar
  -rw-rw-r– 1 tonybai tonybai 112K  4月  8 17:39 ShareSDK-Core-2.3.7.jar
  -rw-rw-r– 1 tonybai tonybai  19K  4月  8 17:39 ShareSDK-SinaWeibo-2.3.7.jar
  -rw-rw-r– 1 tonybai tonybai 4.3K  4月  8 17:39 ShareSDK-Wechat-2.3.7.jar
  -rw-rw-r– 1 tonybai tonybai  29K  4月  8 17:39 ShareSDK-Wechat-Core-2.3.7.jar
  -rw-rw-r– 1 tonybai tonybai 4.6K  4月  8 17:39 ShareSDK-Wechat-Favorite-2.3.7.jar
  -rw-rw-r– 1 tonybai tonybai 4.4K  4月  8 17:39 ShareSDK-Wechat-Moments-2.3.7.jar

把这些jar包文件Copy到GameDemo/proj.android/libs下。

2. 配置文件与资源部分集成

修改GameDemo/proj.android/AndroidManifest.xml文件,在application标签下,添加如下Activity标签:

        <activity
            android:name="cn.sharesdk.framework.ShareSDKUIShell"
            android:configChanges="keyboardHidden|orientation|screenSize"
            android:screenOrientation="portrait"
            android:theme="@android:style/Theme.Translucent.NoTitleBar"
            android:windowSoftInputMode="stateHidden|adjustResize" >
    </activity>
    <activity
            android:name=".wxapi.WXEntryActivity"
            android:configChanges="keyboardHidden|orientation|screenSize"
            android:exported="true"
            android:screenOrientation="portrait"
            android:theme="@android:style/Theme.Translucent.NoTitleBar" />

将C2DXShareSDKSample/proj.android/res下的如下目录中的文件复制到GameDemo/proj.android/res下:

   drawable-hdpi/  drawable-ldpi/  drawable-mdpi/ 
   drawable-xhdpi/  layout/  values/  values-en/

注意,类似icon.png这种文件就不要复制了,自己做一下判断就好。

3. C++部分代码集成

将C2DXShareSDKSample/Classes下的C2DXShareSDK文件夹Copy到GameDemo/Classes下面。

由于Cocos2d-x 3.0rc2的类命名发生了变化,我们需要对C2DXShareSDK中使用到的引擎中的类名以及方法名进行修改。但实际上Cocos2d-x 3.0rc2考虑到了一些兼容性的问题,大部分名字通过cocos2d/cocos/deprecated/CCDeprecated.h中定义的typedef得以保留,虽然这些名字已经被建议deprecated了。rc2中CCObject被改名为Ref了,这个我们需要手工在C2DXShareSDK进行修改。

另外ShareSDK组件在实现时大量使用了CCDictionaryCCArrayCCString,而这三个类在Cocos2d-x 3.0rc2中均被deprecated了,但我们依然可以使用,所以我们可以不做修改。但以后随着cocos2d-x版本的演进,这些类很可能被彻底移除出引擎,我们就需要重新使用其替代品进行实现了。

此外我们还需要手工修改一下C2DXShareSDK/Android/JSON/CCJSONConverter.cpp文件中的getObjJson方 法,因为rc2中CCDictionary、CCString、CCArray这些类的真实名称都已经换成了__Dictionary、__String 和__Array,CCDictionary、CCString、CCArray只是些typedef,因此要像下面这样做些修改(如果你是集成 cocos2d-x 2.x.x版本,则无需做下面修改):

cJSON * CCJSONConverter::getObjJson(Ref * obj)
{
    std::string s = typeid(*obj).name();
    if(s.find("__Dictionary")!=std::string::npos){
        cJSON * json = cJSON_CreateObject();
        convertDictionaryToJson((CCDictionary *)obj, json);
        return json;
    }else if(s.find("__Array")!=std::string::npos){
        cJSON * json = cJSON_CreateArray();
        convertArrayToJson((CCArray *)obj, json);
        return json;
    }else if(s.find("__String")!=std::string::npos){
        CCString * s = (CCString *)obj;
        cJSON * json = cJSON_CreateString(s->getCString());
        return json;
    }else if(s.find("CCNumber")!=std::string::npos){
        CCNumber * n = (CCNumber *)obj;
        cJSON * json = cJSON_CreateNumber(n->getDoubleValue());
        return json;
    }else if(s.find("CCNull")!=std::string::npos){
        cJSON * json = cJSON_CreateNull();
        return json;
    }
    CCLog("CCJSONConverter encountered an unrecognized type");
    return NULL;
}

CCNumber和CCNull是ShareSDK组件自己实现的类名,这里无需修改。

接下来我们需要在AppDelegate.cpp中对ShareSDK做初始化了:

bool AppDelegate::applicationDidFinishLaunching() {
    … …
    initShareSDK();
    … ..
}

void AppDelegate::initShareSDK()
{
    // sina weibo
    CCDictionary *sinaConfigDict = CCDictionary::create();
    sinaConfigDict->setObject(CCString::create("YOUR_WEIBO_APPKEY"), "app_key");
    sinaConfigDict->setObject(CCString::create("YOUR_WEBIO_APPSECRET"), "app_secret");
    sinaConfigDict->setObject(CCString::create("http://www.sharesdk.cn"), "redirect_uri");
    C2DXShareSDK::setPlatformConfig(C2DXPlatTypeSinaWeibo, sinaConfigDict);

    // wechat
    CCDictionary *wcConfigDict = CCDictionary::create();
    wcConfigDict->setObject(CCString::create("YOUR_WECHAT_APPID"), "app_id");
    C2DXShareSDK::setPlatformConfig(C2DXPlatTypeWeixiSession, wcConfigDict);
    C2DXShareSDK::setPlatformConfig(C2DXPlatTypeWeixiTimeline, wcConfigDict);
    C2DXShareSDK::setPlatformConfig(C2DXPlatTypeWeixiFav, wcConfigDict);

    C2DXShareSDK::open(CCString::create("YOUR_SHARESDK_APPKEY"), false);
}

在Share按钮的事件回调函数中调用ShareSDK的接口进行社交平台分享:

void GameScene::menuShareCallback(Ref* sender)
{
    Dictionary *content = Dictionary::create();

    content->setObject(String::create("ShareSDK for Cocos2d-x 3.0rc2社交分享测试。")
                        , "content");
    content->setObject(String::create("ShareSDK分享测试"), "title");
    content->setObject(String::create("http://tonybai.com"), "titleUrl");
    content->setObject(String::create("http://tonybai.com"), "url");
    content->setObject(String::create("Tony Bai"), "site");
    content->setObject(String::create("http://tonybai.com"), "siteUrl");
    content->setObject(String::createWithFormat("%s", YOUR_LOCAL_IMAGE_PATH)
                       , "image");
    content->setObject(String::createWithFormat("%d", C2DXContentTypeNews)
                       , "type");

    C2DXShareSDK::showShareMenu(NULL, content, CCPointMake(100, 100),
                          C2DXMenuArrowDirectionLeft, shareResultHandler);
}

void shareResultHandler(C2DXResponseState state,
                        C2DXPlatType platType,
                        Dictionary *shareInfo,
                        Dictionary *error)
{
    AppDelegate *app = (AppDelegate*)Application::getInstance();
    switch (state) {
        case C2DXResponseStateSuccess:
            CCLog("Share Ok");
            app->showShareResultToast("分享成功");
            break;
        case C2DXResponseStateFail:
            app->showShareResultToast("分享失败");
            CCLog("Share Failed");
            break;
        default:
            break;
    }
}

showShareResultToast实现如下:

void AppDelegate::showShareResultToast(const char *msg)
{
    JniMethodInfo t;
    if (JniHelper::getStaticMethodInfo(t, "YOUR_ACTIVITY_NAME",
        "showShareResultToast", "(Ljava/lang/String;)V")) {
        jstring jmsg = t.env->NewStringUTF(msg);
        t.env->CallStaticVoidMethod(t.classID, t.methodID, jmsg);
        if (t.env->ExceptionOccurred()) {
            t.env->ExceptionDescribe();
            t.env->ExceptionClear();
            return;
        }
        t.env->DeleteLocalRef(t.classID);
    }
}

4. Java部分代码集成

在GameDemo/proj.android/src下面建立cn/sharesdk路径,将C2DXShareSDKSample /proj.android/src/cn/sharesdk下的onekeyshare和ShareSDKUtils.java Copy到GameDemo/proj.android/src/cn/sharesdk下面。

将ShareSDK-Android-2.3.7.zip解压后的ShareSDK for Android/Src/wxapi Copy到GameDemo/proj.android/src/com.tonybai.game/下。

修改GameDemo/proj.android/src/com.tonybai.game/GameDemoActivity.java文件:

import android.widget.Toast;
import cn.sharesdk.ShareSDKUtils;

public class GameDemoActivity extends Cocos2dxActivity {

    private static Context context;

    private static Handler notifyHandler = new Handler() {
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case 1:
                    String message = (String) msg.obj;
                    Toast.makeText(context, message,
                      Toast.LENGTH_SHORT).show();
                    break;
                default:
                    break;
            }
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        context = this;
        ShareSDKUtils.prepare();
        ShareSDKUtils.initSDK("YOUR_SHARESDK_APPKEY", true);
    }

    public static void showShareResultToast(String result) {
        Message msg = new Message();
        msg.what = 1;
        msg.obj = result;
        notifyHandler.sendMessage(msg);
    }

    @Override
    public void onDestroy() {
        ShareSDKUtils.stopSDK();
        super.onDestroy();
    }
}

三、问题与解决方法

按照上面的集成方法修改后,通过cocos编译app,在模拟器运行GameDemo,点击Share,理论上屏幕下方会出现ShareSDK的分享窗口,选择“新浪微博”图标,会打开“图文分享”内容窗口,点击窗口右上角的“分享”即可。

问题1】“图文分享”窗口内容可编辑,并且总是弹出软键盘,影响体验。
 
 期望:内容不可编辑,默认不弹出软键盘
 解决方法:
      打开proj.android/src/cn/sharesdk/onekeyshare/EditPage.java,做如下修改:

      将窗口的软输入方式默认改为SOFT_INPUT_STATE_HIDDEN。

      public void setActivity(Activity activity) {
        super.setActivity(activity);
        if (dialogMode) {
            activity.setTheme(android.R.style.Theme_Dialog);
            activity.requestWindowFeature(Window.FEATURE_NO_TITLE);
        }
        activity.getWindow().setSoftInputMode(
                   //WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
                   WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN);//default: hidden
    }

    在initPageView中增加一行:etContent.setKeyListener(null)。让窗口内容无法修改。
    private void initPageView() {
         … …
        // 文字输入区域
        etContent = new EditText(getContext());
        etContent.setGravity(Gravity.LEFT | Gravity.TOP);
        etContent.setBackgroundDrawable(null);
        etContent.setText(String.valueOf(reqData.get("text")));
        etContent.setKeyListener(null);//make the edittext uneditable
        etContent.setLayoutParams(lpEt);
        … …
    }

【问题2】向微博分享,点击“分享”后,过一会程序异常停止。

 原因分析:
        通过调试观察,发现ShareSDK在解析从Weibo收到的Json包时出现内存违法访问。具体位置是在解析一个数组对象时出现的问题。 ShareSDK用CCArray来存储Json中的数组对象。该问题在cocos2d-x 2.2.2版本中不会出现,但在cocos2d-x 3.0rc2版本中会出现。经代码对比发现,3.0rc2版本中的CCArray的实现与2.2.2 CCArray实现有很大不同,似乎是做了较大重构,暂不能确定是否是3.0rc2版本中CCArray实现的bug。

 解决方法:由于后续的分享结果通知成功与否只需要根据分享的状态来决定,因此我们只需解析出"status"、“action”和“platform” 这三个CCNumber类型字段的值即可。CCArray类型的对象我们并不需要,因此我们只需绕过对Array类型字段的解析和存储即可,修改如下:

// Classes/C2DXShareSDK/Android/JSON/CCJSONConverter.cpp

void CCJSONConverter::convertJsonToDictionary(cJSON *json, CCDictionary *dictionary)
{
    dictionary->removeAllObjects();
    cJSON * j = json->child;
    while (j) {
        if (j->type == cJSON_Number) {
            Ref * obj = getJsonObj(j);
            dictionary->setObject(obj, j->string);
        }
        j = j->next;
    }
}

四、其他

在使用ShareSDK做社交分享时,注意下面两个现象:
1) 第一次进行微博或微信分享时,会打开授权页面,授权后才能分享成功;
2) 微信分享窗口只有在手机联网状态下才能打开。如果手机无法联网,那微信好友、朋友圈和收藏分享将无法打开分享窗口,也不会有什么提示。

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

No Comments

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

No Comments

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》。

Older Entries