ShareSDK Cocos2d-x专用组件的一个Bug

No Comments

近期研究了一下Game App做社交分享,最后选择了ShareSDK来集成,不仅是因为ShareSDK支持国内外主流社交平台,更重要的是ShareSDK提供了专门的 cocos2d-x集成方案,有专门的文档代码Demo供开发者参考。

文档中提到了三种集成方式:纯Java方式、plugin-x方式以及Cocos2d-x专用组件方式,这里选择了ShareSDK Cocos2d-x专用组件(v2.3.7版本)的方式。按照文档中描述的步骤进行的相对顺利,在各个社交平台的appkey生效后,我们对demo app进行了测试,居然发现app经常随机性的崩溃,有时甚至是每次都崩溃,经过深入分析,发现这是ShareSDK Cocos2d-x专用组件的一个严重Bug,下面详细说明一下Bug的产生原因以及Fix方法。

一、App崩溃的场景和代码位置

发生崩溃的场景如下:
    App Demo中有一个"Share"按钮,点击该按钮,App Demo向已经授权的社交平台分享一些Test Content,而App Demo就在收到分享结果应答时发生了崩溃。

代码位置大致如下:

void AppDemo::onShareClick(CCObject* sender)
{
    … …
    C2DXShareSDK::showShareMenu(NULL, content,
                                CCPointMake(100, 100),
                                C2DXMenuArrowDirectionLeft,
                                shareResultHandler);
}

void shareResultHandler(C2DXResponseState state, C2DXPlatType platType,
                        CCDictionary *shareInfo, CCDictionary *error)
{
    switch (state) {
        case C2DXResponseStateSuccess:
            CCLog("Share Ok");
            break;
        case C2DXResponseStateFail:
            CCLog("Share Failed");
            break;
        default:
            break;
    }
}

崩溃的位置大致就在回调shareResultHandler前后的某个位 置,比较随机。

二、现象分析

通过查看Eclipse logcat窗口的调试日志,我们发现一些规律,一些在“Share Ok后的崩溃打印出如下日志:

04-16 01:28:33.890: D/cocos2d-x debug info(1748): Share Ok
04-16 01:28:34.090: D/cocos2d-x debug info(1748): Assert failed: reference count should greater than 0
04-16 01:28:34.090: E/cocos2d-x assert(1748): /home1/tonybai/android-dev/cocos2d-x-2.2.2/samples/Cpp/temp/AppDemo/proj.android/../../../../../cocos2dx/cocoa/CCObject.cpp function:release line:81
04-16 01:28:34.130: A/libc(1748): Fatal signal 11 (SIGSEGV) at 0×00000003 (code=1), thread 1829 (Thread-122)

猜测一下,似乎是某个CCObject在真正Release前已经被释放了,然后后续被引用时触发内存非法访问。Cocos2d-x采用的是内存 计数的内存管理机制,在我的《Cocos2d-x内存管理-绕不过去的坎》一文中有描述。了解Cocos2d-x的内存管理机制是理解这个Bug 的前提条件。

三、原因分析

看来不得不挖掘一下ShareSDK组件的代码了。AppDemo中ShareSDK组件的代码分为两个部分:AppDemo/Classes /C2DXShareSDK和AppDemo/proj.android/src/cn/sharesdk。前者是C++代码,后面则是Java 代码,两者通过jni调用联系在一起。我们重点来找出分享应答返回来时的关键联系。

集成ShareSDK的Cocos2d-x程序会在主Activity的onCreate方法中调用ShareSDKUtils.prepare();

我们来看看prepare方法的实现:

//AppDemo/proj.android/src/cn/sharesdk/ShareSDKUtils.java

public class ShareSDKUtils {
    private static boolean DEBUG = true;
    private static Context context;
    private static PlatformActionListener paListaner;
    private static Hashon hashon;
    … …
   
    public static void prepare() {
        UIHandler.prepare();
        context = Cocos2dxActivity.getContext().getApplicationContext();
        hashon = new Hashon();
        final Callback cb = new Callback() {
            public boolean handleMessage(Message msg) {
                onJavaCallback((String) msg.obj);
                return false;
            }
        };

        paListaner = new PlatformActionListener() {
            public void onComplete(Platform platform, int action, HashMap<String, Object> res) {
                if (DEBUG) {
                    System.out.println("onComplete");
                    System.out.println(res == null ? "" : res.toString());
                }
                HashMap<String, Object> map = new HashMap<String, Object>();
                map.put("platform", ShareSDK.platformNameToId(platform.getName()));
                map.put("action", action);
                map.put("status", 1); // Success = 1, Fail = 2, Cancel = 3
                map.put("res", res);
                Message msg = new Message();
                msg.obj = hashon.fromHashMap(map);
                UIHandler.sendMessage(msg, cb);
            }

    … …
}

可以看出监听Complete事件的listener将message的处理都交给了cb,而cb调用了onJavaCallback方法。

onJavaCallback方法是jni导出的方法,它的实现在 AppDemo/Classes/C2DXShareSDK/Android/ShareSDKUtils.cpp里面。

JNIEXPORT void JNICALL Java_cn_sharesdk_ShareSDKUtils_onJavaCallback
  (JNIEnv * env, jclass thiz, jstring resp) {
    CCJSONConverter* json = CCJSONConverter::sharedConverter();
    const char* ccResp = env->GetStringUTFChars(resp, JNI_FALSE);
    CCLog("ccResp = %s", ccResp);
    CCDictionary* dic = json->dictionaryFrom(ccResp);
    env->ReleaseStringUTFChars(resp, ccResp);
    CCNumber* status = (CCNumber*) dic->objectForKey("status"); // Success = 1, Fail = 2, Cancel = 3
    CCNumber* action = (CCNumber*) dic->objectForKey("action"); //  1 = ACTION_AUTHORIZING,  8 = ACTION_USER_INFOR,9 = ACTION_SHARE
    CCNumber* platform = (CCNumber*) dic->objectForKey("platform");
    CCDictionary* res = (CCDictionary*) dic->objectForKey("res");
    // TODO add codes here
    if(1 == status->getIntValue()){
        callBackComplete(action->getIntValue(), platform->getIntValue(), res);
    }else if(2 == status->getIntValue()){
        callBackError(action->getIntValue(), platform->getIntValue(), res);
    }else{
        callBackCancel(action->getIntValue(), platform->getIntValue(), res);
    }

    dic->autorelease();
}

这就是两块代码的关键联系。而问题似乎就出在onJavaCallback方 法里,因为我们看到了该方法中使用了Cocos2d-x的数据结构类。

我们来看一下onJavaCallback方法是在哪个线程里执行的。Cocos2d-x App至少有两个线程,一个UI Thread(Activity),一个Render Thread。显然onJavaCallback是在UI Thread中被执行的。但是我们知道Cocos2d-x的AutoreleasePool是在Render Thread中管理的,并在帧切换时进行释放操作的。

我们似乎闻到了问题的味道。Cocos2d-x基本上算是一个"单线程"游戏架构,所有的渲染操作、渲染树节点逻辑管理、绝大多数游戏逻辑都在 Render Thread中进行,UI Thread更多的是接收系统事件,并传递给Render Thread处理。Cocos2d-x的内存管理在这样的“单线程”背景下是没有大问题的,都是串行操作,不存在thread racing的情况。但一旦另外一个线程也调用内存管理接口进行对象内存操作时,问题就出现了,Cocos2d-x的内存池管理不是线程安全的。

我们回到上面代码,重点看一下json转dic的方法,该方法将分享应答字符串转换为内部的dictionary结构:

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

CCDictionary * CCJSONConverter::dictionaryFrom(const char *str)
{
    cJSON * json = cJSON_Parse(str);
    if (!json || json->type!=cJSON_Object) {
        if (json) {
            cJSON_Delete(json);
        }
        return NULL;
    }
    CCAssert(json && json->type==cJSON_Object, "CCJSONConverter:wrong json format");
    CCDictionary * dictionary = CCDictionary::create();
    convertJsonToDictionary(json, dictionary);
    cJSON_Delete(json);
    return dictionary;
}

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

CCObject * CCJSONConverter::getJsonObj(cJSON * json)
{
    switch (json->type) {
        case cJSON_Object:
        {
            CCDictionary * dictionary = CCDictionary::create();           
            convertJsonToDictionary(json, dictionary);
            return dictionary;
        }
        case cJSON_Array:
        {
            CCArray * array = CCArray::create();
            convertJsonToArray(json, array);
            return array;
        }
        case cJSON_String:
        {
            CCString * string = CCString::create(json->valuestring);
            return string;
        }
        case cJSON_Number:
        {
            CCNumber * number = CCNumber::create(json->valuedouble);
            return number;
        }
        case cJSON_True:
        {
            CCNumber * boolean = CCNumber::create(1);
            return boolean;
        }
        case cJSON_False:
       {
            CCNumber * boolean = CCNumber::create(0);
            return boolean;
        }
        case cJSON_NULL:
        {
            CCNull * null = CCNull::create();
            return null;
        }
        default:
        {
            CCLog("CCJSONConverter encountered an unrecognized type");
            return NULL;
        }
    }
}

可以看出整个解析过程,都直接用的是传统的Cocos2d-x对象构造方法:create。在每个对象的create中,代码都会调用该对象的 autorelease方法。而这个方法本身就是线程不安全的,且即便autorelease调用ok,在下一帧切换时,这些对象将都会被release 掉,如果在UI Thread中再引用这些对象的地址,那势必造成内存的非法访问,而引发程序崩溃。

四、Fix方法

可能有朋友会问,create后,我retain一下可否?答案是否。因此create的创建不是线程安全的,create和retain两个调 用之间存在时间差,而在这段时间内,该对象就有可能被render thread释放掉。

Fix方法很简单,就是在UI Thread中不使用Cocos2d-x的内存管理机制,我们用传统的new来替代create,并将 Java_cn_sharesdk_ShareSDKUtils_onJavaCallback最后的autorelease改为release,这样就 不用劳烦Render Thread来帮我们释放内存了。CCDictionary的destructor调用时还会将Dictionarny内部所有Element自动释放 掉。

Cocos2d-x内存管理-绕不过去的坎

No Comments

Cocos2d-x引擎的核心是用C++编写的,那对于所有使用该引擎的游戏开发人员来说,内存管理是一道绕不过去的坎。

关于Cocos2d-x内存管理,网上已经有了许多参考资料,有些资料写的颇为详实,因为在内存管理这块我不想多费笔墨,只是更多的将思路描述清 楚。

一、对象内存引用计数

Cocos2d-x内存管理的基本原理就是对象内存引用计数,Cocos2d-x将内存引用计数的实现放在了顶层父类CCObject中,这里将涉及引用计数的CCObject的成员和方法摘录出来:

class CC_DLL CCObject : public CCCopying
{
public:
   … …
protected:
    // count of references
    unsigned int        m_uReference;
    // count of autorelease
    unsigned int        m_uAutoReleaseCount;
public:
    void release(void);
    void retain(void);
    CCObject* autorelease(void);
    … ….
}

CCObject::CCObject(void)
: m_nLuaID(0)
, m_uReference(1) // when the object is created, the reference count of it is 1
, m_uAutoReleaseCount(0)
{
  … …
}

void CCObject::release(void)
{
    CCAssert(m_uReference > 0, "reference count should greater than 0");
    –m_uReference;

    if (m_uReference == 0)
    {
        delete this;
    }
}

void CCObject::retain(void)
{
    CCAssert(m_uReference > 0, "reference count should greater than 0");

    ++m_uReference;
}

CCObject* CCObject::autorelease(void)
{
    CCPoolManager::sharedPoolManager()->addObject(this);
    return this;
}

先不考虑autorelease与m_uAutoReleaseCount(后续细说)。计数的核心字段是m_uReference,可以看到:

* 当一个Object初始化(被new出来时),m_uReference = 1;
* 当调用该Object的retain方法时,m_uReference++;
* 当调用该Object的release方法时,m_uReference–,若m_uReference减后为0,则delete该Object。

二、手工对象内存管理

在上述对象内存引用计数的原理下,我们得出以下Cocos2d-x下手工对象内存管理的基本模式:

CCObject *obj = new CCObject();
obj->init();
…. …
obj->release();

在Cocos2d-x中CCDirector就是一个手工内存管理的典型:

CCDirector* CCDirector::sharedDirector(void)
{
    if (!s_SharedDirector)
    {
        s_SharedDirector = new CCDisplayLinkDirector();
        s_SharedDirector->init();

    }

    return s_SharedDirector;
}

void CCDirector::purgeDirector()
{
    … …
    // delete CCDirector
    release();
}

三、自动对象内存管理

所谓的“自动对象内存管理”,指的就是哪些不再需要的object将由Cocos2d-x引擎替你释放掉,而无需你手工再调用Release方法。

自动对象内存管理显然也要遵循内存引用计数规则,只有当object的计数变为0时,才会释放掉对象的内存。

自动对象内存管理的典型模式如下:

CCYourClass *CCYourClass::create()
{
    CCYourClass*pRet = new CCYourClass();
    if (pRet && pRet->init())
    {
        pRet->autorelease();
        return pRet;
    }
    else
    {
        CC_SAFE_DELETE(pRet);
        return NULL;
    }
}

一般我们通过一个单例模式创建对象,与手工模式不同的地方在于init后多了一个autorelease调用。这里再把autorelease调用的实现摘录一遍:

CCObject* CCObject::autorelease(void)
{
    CCPoolManager::sharedPoolManager()->addObject(this);
    return this;
}

追溯addObject方法:

// cocoa/CCAutoreleasePool.cpp

void CCPoolManager::addObject(CCObject* pObject)
{
    getCurReleasePool()->addObject(pObject);
}

void CCAutoreleasePool::addObject(CCObject* pObject)
{
    m_pManagedObjectArray->addObject(pObject);

    CCAssert(pObject->m_uReference > 1, "reference count should be greater than 1");
    ++(pObject->m_uAutoReleaseCount);
    pObject->release(); // no ref count, in this case autorelease pool added.
}

// cocoa/CCArray.cpp
void CCArray::addObject(CCObject* object)                                                                                                   
{                                                                                                                                          
    ccArrayAppendObjectWithResize(data, object);                             
}  

// support/data_support/ccCArray.cpp
void ccArrayAppendObjectWithResize(ccArray *arr, CCObject* object)                                                                          
{                                                                                                                   
    ccArrayEnsureExtraCapacity(arr, 1);                                                              
    ccArrayAppendObject(arr, object);                                         
}

void ccArrayAppendObject(ccArray *arr, CCObject* object)
{
    CCAssert(object != NULL, "Invalid parameter!");
    object->retain();
    arr->arr[arr->num] = object;
    arr->num++;
}

调用层次挺深,涉及的类也众多,这里归纳总结一下。

Cocos2d-x的自动对象内存管理基于对象引用计数以及CCAutoreleasePool(自动释放池)。引用计数前面已经说过了,这里单说自动释放池。Cocos2d-x关于自动对象内存管理的基本类层次结构如下:

    CCPoolManager类 (自动释放池管理器)
        – CCArray*    m_pReleasePoolStack; (自动释放池栈,存放CCAutoreleasePool类实例)
           
    CCAutoreleasePool类
        – CCArray*    m_pManagedObjectArray;
(受管对象数组)

CCObject关于内存计数以及自动管理有两个字段:m_uReference和m_uAutoReleaseCount。前面在手工管理模式下,我只提及了m_uReference,是m_uAutoReleaseCount该亮相的时候了。我们沿着自动释放对象的创建步骤来看看不同阶段,这两个重要字段的值都是啥,代表的是啥含义:

CCYourClass*pRet = new CCYourClass();    m_uReference = 1; m_uAutoReleaseCount = 0;
pRet->init();                           m_uReference = 1; m_uAutoReleaseCount = 0;
pRet->autorelease();                    
    m_pManagedObjectArray->addObject(pObject);
m_uReference = 2; m_uAutoReleaseCount = 0;
    ++(pObject->m_uAutoReleaseCount);          m_uReference = 2; m_uAutoReleaseCount = 1;
    pObject->release();                        m_uReference = 1; m_uAutoReleaseCount = 1;

在调用autorelease之前,两个值与手工模式并无差别,在autorelease后,m_uReference值没有变,但m_uAutoReleaseCount被加1。

m_uAutoReleaseCount这个字段的名字很容易让人误解,以为是个计数器,但实际上绝大多数时刻它是一个标识的角色,以前版本代码中有一个布尔字段m_bManaged,似乎后来被m_uAutoReleaseCount替换掉了,因此m_uAutoReleaseCount兼有m_bManaged的含义, 也就是说该object是否在自动释放池的控制之下,如果在自动释放池的控制下,自动释放池会定期调用该object的release方法,直到该 object内存计数降为0,被真正释放。否则该object不能被自动释放池自动释放内寸,需手工release。这个理解非常重要,再后面我们能用到 这个理解。

四、自动释放时机

通过autorelease我们已经将object放入autoreleasePool中,那究竟何时对象会被释放呢?答案是每帧执行一次自动内存对象释放操作。

在“Hello,Cocos2d-x”一文中,我们讲过整个Cocos2d-x引擎的驱动机制在于GLThread的guardedRun函数,后者会 “死循环”式(实际帧绘制频率受到屏幕vertsym信号的影响)的调用Render的onDrawFrame方法实现,而最终程序会进入 CCDirector::mainLoop方法中,也就是说mainLoop的执行频率是每帧一次。我们再来看看mainLoop的实现:

void CCDisplayLinkDirector::mainLoop(void)
{
    if (m_bPurgeDirecotorInNextLoop)
    {
        m_bPurgeDirecotorInNextLoop = false;
        purgeDirector();
    }
    else if (! m_bInvalid)
     {
         drawScene();

         // release the objects
         CCPoolManager::sharedPoolManager()->pop();
     }
}

这次我们要关注的不是drawScene,而是 CCPoolManager::sharedPoolManager()->pop(),显然在游戏未退出 (m_bPurgeDirecotorInNextLoop决定)的条件下,CCPoolManager的pop方法每帧执行一次,这就是自动释放池执行 的起点。

void CCPoolManager::pop()
{
    if (! m_pCurReleasePool)
    {
        return;
    }

     int nCount = m_pReleasePoolStack->count();

    m_pCurReleasePool->clear();

      if(nCount > 1)
      {
        m_pReleasePoolStack->removeObjectAtIndex(nCount-1);
        m_pCurReleasePool = (CCAutoreleasePool*)m_pReleasePoolStack->objectAtIndex(nCount – 2);
    }
}

真正释放对象的方法是m_pCurReleasePool->clear()

void CCAutoreleasePool::clear()
{
    if(m_pManagedObjectArray->count() > 0)
    {
        CCObject* pObj = NULL;
        CCARRAY_FOREACH_REVERSE(m_pManagedObjectArray, pObj)
        {
            if(!pObj)
                break;

            –(pObj->m_uAutoReleaseCount);
        }

        m_pManagedObjectArray->removeAllObjects();
    }
}

void CCArray::removeAllObjects()     
{   
    ccArrayRemoveAllObjects(data);                    
}

void ccArrayRemoveAllObjects(ccArray *arr)                    
{                       
    while( arr->num > 0 )                      
    {                    
        (arr->arr[--arr->num])->release();               
    }                    
}

不出预料,当前自动释放池遍历每个“受控制”Object,–m_uAutoReleaseCount,并调用该object的release方法。

我们接着按释放流程来看看m_uAutoReleaseCount和m_uReference值的变化:

CCPoolManager::sharedPoolManager()->pop();  m_uReference = 0; m_uAutoReleaseCount = 0;

五、自动释放池的初始化

自动释放池本身是何时出现的呢?回顾一下Cocos2d-x引擎的初始化过程(android版),引擎初始化实在Render的onSurfaceCreated方法中进行的,我们不难追踪到以下代码:

//hellocpp/jni/hellocpp/main.cpp
Java_org_cocos2dx_lib_Cocos2dxRenderer_nativeInit {
   
    //这里CCDirector第一次被创建
    if (!CCDirector::sharedDirector()->getOpenGLView())
    {
        CCEGLView *view = CCEGLView::sharedOpenGLView();
        view->setFrameSize(w, h);

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

   
CCDirector* CCDirector::sharedDirector(void)
{
    if (!s_SharedDirector)
    {
        s_SharedDirector = new CCDisplayLinkDirector();
        s_SharedDirector->init();  
    }

    return s_SharedDirector;
}

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

    … …

    // create autorelease pool
    CCPoolManager::sharedPoolManager()->push();

    return true;
}

六、探寻Cocos2d-x内核对象的自动化内存释放

前面我们基本了解了Cocos2D-x的自动化内存释放原理。如果你之前翻看过一些Cocos2d-x的内核源码,你会发现很多内核对象都是通过单例模式create出来的,也就是说都使用了autorelease将自己放入自动化内存释放池中被管理。

比如我们在HelloCpp中看到过这样的代码:

//HelloWorldScene.cpp
bool HelloWorld::init() {
     …. ….
    // add "HelloWorld" splash screen"
    CCSprite* pSprite = CCSprite::create("HelloWorld.png");

    // position the sprite on the center of the screen
    pSprite->setPosition(ccp(visibleSize.width/2 + origin.x, visibleSize.height/2 + origin.y));

    // add the sprite as a child to this layer
    this->addChild(pSprite, 0);
    … …
}

CCSprite采用自动化内存管理模式create object(cocos2dx/sprite_nodes/CCSprite.cpp),之后将自己加入到HelloWorld这个CCLayer实例 中。按照上面的分析,create结束后,CCSprite object的m_uReference = 1; m_uAutoReleaseCount = 1。一旦如此,那么在下一帧时,该object就会被CCPoolManager释放掉。但我们在屏幕上依旧可以看到该Sprite的存在,这是怎么回事呢?

问题的关键就在this->addChild(pSprite, 0)这行代码中。addChild方法实现在CCLayer的父类CCNode中:

//  cocos2dx/base_nodes/CCNode.cpp
void CCNode::addChild(CCNode *child, int zOrder, int tag)
{
    … …
    if( ! m_pChildren )
    {
        this->childrenAlloc();
    }

    this->insertChild(child, zOrder);

    … …
}

void CCNode::insertChild(CCNode* child, int z)
{
    m_bReorderChildDirty = true;
    ccArrayAppendObjectWithResize(m_pChildren->data, child);
    child->_setZOrder(z);
}

void ccArrayAppendObjectWithResize(ccArray *arr, CCObject* object)
{
    ccArrayEnsureExtraCapacity(arr, 1);
    ccArrayAppendObject(arr, object);
}

void ccArrayAppendObject(ccArray *arr, CCObject* object)
{
    CCAssert(object != NULL, "Invalid parameter!");
    object->retain();
    arr->arr[arr->num] = object;
    arr->num++;
}

又是一系列方法调用,最终我们来到了ccArrayAppendObject方法中,看到了陌生而又眼熟的retain方法调用。

在本文开始我们介绍CCObject时,我们知道retain是CCObject的一个方法,用于增加m_uReference计数。而实际上retain还隐含着“保留”这层意思。

在完成this->addChild(pSprite, 0)调用后,CSprite object的m_uReference = 2; m_uAutoReleaseCount = 1,这很关键。

我们在脑子里再过一下自动释放池释放object的过程:–m_uReference, –m_uAutoReleaseCount。一帧之后,两个值变成了m_uReference = 1; m_uAutoReleaseCount = 0。还记得前面说过的m_uAutoReleaseCount的另外一个非计数含义么,那就是表示该object是否“受控”,现在值为0,显然不再受自动释放池的控制了,后续即便再执行100次内存自动释放,也不会影响到该object的存活。

后续要想释放这个“精灵”,我们还是需要手工调用release,或再调用其autorelease方法。

Hello, Cocos2d-x

No Comments

女儿从两岁半开始接触iPad,在这个年龄段也只有一些幼教类游戏适合她玩。虽然知道iPad玩久了对视力有伤害,但有时候还真拗不过果果,索性 也就让她玩一会儿。之前对智能终端上的东西不是很在意,也没啥兴趣,这大概与当年在大学时做Win32 GUI开发的糟糕经历多多少少有点关系。不过智能终端是大势所趋,历史的潮流不能违抗。虽然自己并非以Android/iOS编程为主业,但适当学习学习 总归没有坏处,万一作出一个像"Flappy Bird"的游戏,爆发一下,还是蛮Happy的。于是在开始学习实践之前给自己定了一个小目标:今年六一儿童节送给女儿一款自己制作的小游戏。

智能终端上的游戏目前风头正劲,试问哪个智能手机上没有几款企鹅公司出品的游戏呢!之前从未涉猎过游戏开发,但知道游戏开发前要挑选一款合适的游 戏引擎,自己从头开始敲代码的时代已经out了。在寻觅游戏引擎之前,我需要回答三道摆在我面前的选择题:

    1、2D引擎还是3D引擎?
    2、平台专用引擎还是跨平台引擎?
    3、收费引擎还是开源引擎?

作为入门级选手,2D游戏显然更适合上手一些,另外适合果果这个年龄段的幼教类的游戏也多以2D游戏居多。3D游戏本身也太难了,不仅要 Programming能力,还要3D建模能力,这些学习起来周期就太长了;一直是Ubuntu Fans,手头没有Mac Book,这样开发iOS程序变成一件糟心的事,在Ubuntu下搭建iOS App开发环境繁杂的很,即便是虚拟机也懒得尝试。但从游戏体验来看,还是在iPad上玩更好一些,因此最好引擎能跨平台,以便后续迁移到iOS上;开源 和用开源惯了,收费的引擎目前不在考虑范围之内。综上,我要寻找的是一款开源的、跨平台的Mobile 2D Game Engine。

于是我找到了Cocos2d-x!Cocos2d-x是Cocos2d-iphone的C++跨平台分支,由于是国人创立的,在国内有着较大的用 户群,引擎资料也较多,社区十分活跃。国内已经出版了多本有关Cocos2d-x的中文书籍,比如《Cocos2d-x高级开发教程:制作自己的 “捕鱼达人”》 、《Cocos2d-x权威指南》 等都还不错。更重要的是Cocos2d-x自带了丰富的例子,供初学者“临摹学习”,其中cocos2d-x-2.2.2/samples/Cpp /TestCpp这个例子几乎涵盖了该引擎的绝大多数功能。下面就开启Cocos2d-x的入门之旅(For Android)。

一、引擎安装

试验环境:
   Ubuntu 12.04.1 x86_64
   gcc 4.6.3
   javac 1.7.0_21
   java "1.7.0_21" HotSpot 64-bit Server VM
   adt-bundle-linux-x86_64-20131030.zip
   android-ndk-r9d-linux-x86_64.tar.bz2

Cocos2d-x官网目前提供2.2.2稳定版以及3.0beta2版的下载(当然你也可以下载到更老的版本)。由于3.0改变较大,资料不 多,且对编译器等版本的要求较高(需要支持C++ 11标准),因此这里依旧以2.2.2版本作为学习目标。Cocos2d-x-2.2.2下载后解压到某个目录:比如/home1/tonybai/android-dev/cocos2d-x-2.2.2。 如果仅是用Cocos2d-x开发Android版本游戏,则不需要做什么编译工作。Android Game Project会在Project build时自动用NDK的编译器编译C++代码,并与NDK链接。如果你想早点看看Cocos2d-x sample中的例子运行起来到底是什么样子的,你可以在Ubuntu下编译出Linux版本的游戏:在cocos2d-x-2.2.2下执行make-all-linux-project.sh即可。编译需要一段时间,编译成 功后,我们可以进入到“cocos2d-x-2.2.2/samples/Cpp/HelloCpp/proj.linux/bin/release” 下执行“HelloCpp”这个可执行文件,一个最简单的Cocos2d-x游戏就会展现在你的面前了。

Android sample project的构建稍微复杂些:

首先在Eclipse中添加libcocos2dx Library project from existed code(注意:不Copy到workspace,原地建立)。该Project的代码路径为cocos2d-x-2.2.2/cocos2dx/platform /android/java。在project.properties和AndroidManifest.xml适当修改你所使用的api版本, 以让编译通过。我这里用的是 target=android-19。

然后,设置NDK_ROOT环境变量(比如export NDK_ROOT='/home1/tonybai/android-dev/adt-bundle-linux-x86_64/android-ndk-r9c'), 供build_native.sh使用。

最后添加游戏project。在Eclipse中添加HelloCpp project from existed code,位置cocos2d-x-2.2.2/samples/Cpp/HelloCpp/proj.android(注 意:不Copy到Workspace中,原地建立)。在HelloCpp的project.properties中添加“android.library.reference.1=../../../../cocos2dx/platform/android /java”。同样别忘了在project.properties和AndroidManifest.xml适当修改你所使用 的api版本,以让编译通过。

如果一切顺利的话,你会在Console窗口看到“**** Build Finished ****”。Problems窗口显示“0 errors“。 启动Android模拟器,Run Application,同样的HelloCpp画面会呈现在模拟器上。

Cocos2d-x是建构在OpenGL技术之上的。对于Android平台而言,Android SDK已经完全封装了opengl es 1.1/2.0的API(android.opengl.*;javax.microedition.khronos.egl.*;javax.microedition.khronos.opengles.*), 引擎完全可以建立在这个之上,无需C++代码。但Cocos2d-x是一个跨平台的2D游戏引擎,核心选择了用C++代码实现(iOS提供的C绑 定,不提供Java绑定;Android则提供了Java和C绑定),因此 在开发Android平台的2D游戏时,引擎部分是SDK与NDK交相互应,比如GLThread的创建和管理用的是SDK的 GLSurfaceView和GLThread,但真正的Surface绘制部分则是回调Cocos2d-x用C++编写的绘制实现(链接NDK 中的库)。

二、Cocos2d-x Android工程代码组织结构

以samples/Cpp/HelloApp的Android工程为例,Android版的Cocos2d-x工程与普通android应用程序 差别 不大,核心部分只是多了一个jni目录和一个build_native.sh脚本文件。其中jni目录下存放的是Java和C++调用转换的“胶 水”代码;build_native.sh则是用于编译jni下C++代码以及 cocos2dx_static library代码的构建脚本。

HelloCpp的构建过程摘要如下:

**** Build of configuration Default for project HelloCpp ****

bash /home1/tonybai/android-dev/cocos2d-x-2.2.2/samples/Cpp/HelloCpp/proj.android/build_native.sh
NDK_ROOT = /home1/tonybai/android-dev/adt-bundle-linux-x86_64/android-ndk-r9c
COCOS2DX_ROOT = /home1/tonybai/android-dev/cocos2d-x-2.2.2/samples/Cpp/HelloCpp/proj.android/../../../..
APP_ROOT = /home1/tonybai/android-dev/cocos2d-x-2.2.2/samples/Cpp/HelloCpp/proj.android/..
APP_ANDROID_ROOT = /home1/tonybai/android-dev/cocos2d-x-2.2.2/samples/Cpp/HelloCpp/proj.android
+ /home1/tonybai/android-dev/adt-bundle-linux-x86_64/android-ndk-r9c/ndk-build -C /home1/tonybai/android-dev/cocos2d-x-2.2.2/samples/Cpp/HelloCpp/proj.androidNDK_MODULE_PATH=/home1/tonybai/android-dev/cocos2d-x-2.2.2/samples/Cpp/HelloCpp/proj.android/../../../..:/home1/tonybai/android-dev/cocos2d-x-2.2.2/samples/Cpp/HelloCpp/proj.android/../../../../cocos2dx/platform/third_party/android/prebuilt
Using prebuilt externals
Android NDK: WARNING:/home1/tonybai/android-dev/cocos2d-x-2.2.2/samples/Cpp/HelloCpp/proj.android/../../../../cocos2dx/Android.mk:cocos2dx_static: LOCAL_LDLIBS is always ignored for static libraries  
make: Entering directory `/home1/tonybai/android-dev/cocos2d-x-2.2.2/samples/Cpp/HelloCpp/proj.android'
[armeabi] Compile++ thumb: hellocpp_shared <= main.cpp
[armeabi] Compile++ thumb: hellocpp_shared <= AppDelegate.cpp
[armeabi] Compile++ thumb: hellocpp_shared <= HelloWorldScene.cpp
[armeabi] Compile++ thumb: cocos2dx_static <= CCConfiguration.cpp
[armeabi] Compile++ thumb: cocos2dx_static <= CCScheduler.cpp
 … …
[armeabi] Compile++ thumb: cocos2dx_static <= CCTouch.cpp
[armeabi] StaticLibrary  : libcocos2d.a
[armeabi] Compile thumb  : cpufeatures <= cpu-features.c
[armeabi] StaticLibrary  : libcpufeatures.a
[armeabi] SharedLibrary  : libhellocpp.so
[armeabi] Install        : libhellocpp.so => libs/armeabi/libhellocpp.so
make: Leaving directory `/home1/tonybai/android-dev/cocos2d-x-2.2.2/samples/Cpp/HelloCpp/proj.android'

**** Build Finished ****

指挥NDK编译的则是jni下的Android.mk文件,其角色类似于Makefile。

三、Cocos2d-x Android工程代码阅读

单独将如何阅读代码拿出来,是为了后面分析引擎的驱动流程做准备工作。学习类似Cocos2d-x这样的游戏引擎,仅仅停留在游戏逻辑层代码是不 能很好的把握引擎本质的,因此适当的挖掘引擎实现实际上对于理解和使用 引擎都是大有裨益的。

以一个Cocos2d-x Android工程为例,它的游戏逻辑代码以及涉及的引擎代码涵盖在一下路径下(还是以HelloCpp的Android工程为例):

    项目层:
        * cocos2d-x-2.2.2/samples/Cpp/HelloCpp/proj.android/src  主Activity的实现;
        * cocos2d-x-2.2.2/samples/Cpp/HelloCpp/proj.android/jni/hellocpp  Cocos2dxRenderer类的nativeInit实现,用于引出Application的入口;
        * cocos2d-x-2.2.2/samples/Cpp/HelloCpp/Classes 你的游戏逻辑,以C++代码形式呈现;
   
    引擎层:
        * cocos2d-x-2.2.2/cocos2dx/platform/android/java/src 引擎层对Android Activity、GLSurfaceView以及Render的封装
        * cocos2d-x-2.2.2/cocos2dx/platform/android/jni 对应上面封装的native method实现
        * cocos2d-x-2.2.2/cocos2dx、cocos2d-x-2.2.2/cocos2dx/platform、cocos2d-x- 2.2.2/cocos2dx/platform/android   cocos2dx引擎的核心实现(针对android平台)

后续的代码分析也将从这两个层次、六处位置出发。

四、从Activity开始

之前多少了解了一些Android App开发的知识,Android App都是始于Activity的。游戏也是App的一种,因此在Android平台上,Cocos2d-x游戏也是从Activity开始的。于是 Activity,确切的说是Cocos2dxActivity是我们这次引擎驱动机制分析的出发点。

回顾Android Activity的Lifecycle,Activity启动的顺序是:Activity.onCreate -> Activity.onStart() -> Activity.onResume()。接下来我们将按照 这条主线进行引擎驱动机制的分析。

HelloCpp.java中的HelloCpp这个Activity完全无所作为,仅仅是继承其父类Cocos2dxActivity的实现罢 了。

// HelloCpp.java
public class HelloCpp extends Cocos2dxActivity{
    protected void onCreate(Bundle savedInstanceState){
        super.onCreate(savedInstanceState);
    }
    … …
}

我们来看Cocos2dxActivity类。

// Cocos2dxActivity.java

@Override
protected void onCreate(final Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    sContext = this;
    this.mHandler = new Cocos2dxHandler(this);
    this.init();
    Cocos2dxHelper.init(this, this);

public void init() {
        // FrameLayout
        ViewGroup.LayoutParams framelayout_params =
            new ViewGroup.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT,
                                       ViewGroup.LayoutParams.FILL_PARENT);
        FrameLayout framelayout = new FrameLayout(this);
        framelayout.setLayoutParams(framelayout_params);

        … …
        // Cocos2dxGLSurfaceView
        this.mGLSurfaceView = this.onCreateView();

        // …add to FrameLayout
        framelayout.addView(this.mGLSurfaceView);
        … …
        this.mGLSurfaceView.setCocos2dxRenderer(new Cocos2dxRenderer());
        … …

        // Set framelayout as the content view
        setContentView(framelayout);
}

从上面代码可以看出,onCreate调用的init方法才是Cocos2dxActivity初始化的核心。在init方法 中,Cocos2dxActivity创建了一个Framelayout实例,并将该实例作为content View赋给了Cocos2dxActivity的实例。Framelayout实例也并不孤单,一个设置了Cocos2dxRenderer实例的 GLSurfaceView被Added to it。而Cocos2d-x引擎的初始化已经悄悄地在这几行代码间完成了,至于初始化的细节我们后续再做分析。

接下来是onResume方法,它的实现如下:

    @Override
    protected void onResume() {
        super.onResume();

        Cocos2dxHelper.onResume();
        this.mGLSurfaceView.onResume();
    }

onResume调用了View的onResume()。

// Cocos2dxGLSurfaceView:
    @Override
    public void onResume() {
        super.onResume();

        this.queueEvent(new Runnable() {
            @Override
            public void run() {
                Cocos2dxGLSurfaceView.this.mCocos2dxRenderer.handleOnResume();
            }
        });
    }

Cocos2dxGLSurfaceView将该事件打包放到队列里,扔给了另外一个线程去执行(后续会详细说明这个线程),对应的方法在 Cocos2dxRenderer class中。

    public void handleOnResume() {
        Cocos2dxRenderer.nativeOnResume();
    }

Render实际上调用的是native方法。

    JNIEXPORT void JNICALL Java_org_cocos2dx_lib_Cocos2dxRenderer_nativeOnResume() {
        if (CCDirector::sharedDirector()->getOpenGLView()) {
            CCApplication::sharedApplication()->applicationWillEnterForeground();
        }
    }

applicationWillEnterForeground方法在你的AppDelegate.cpp中;

void AppDelegate::applicationWillEnterForeground() {
    CCDirector::sharedDirector()->startAnimation();//

    // if you use SimpleAudioEngine, it must resume here
    // SimpleAudioEngine::sharedEngine()->resumeBackgroundMusic();
}

这里仅是重新获得了一下时间罢了。

五、Render Thread(渲染线程) - GLThread

游戏引擎要兼顾UI事件和屏幕帧刷新。Android的OpenGL应用采用了UI线程(Main Thread) +  渲染线程(Render Thread)的模式。Activity活在Main Thread(主线程)中,也叫做UI线程。该线程负责捕获与用户交互的信息和事件,并与渲染(Render)线程交互。比如当用户接听电话、切换到其他 程序时,渲染线程必须知道发生了 这些事件,并作出即时的处理,而这些事件及处理方式都是由主线程中的Activity以及其装载的View传递给渲染线程的。我们在Cocos2dx的框 架代码中看不到渲染线程的诞生过程,这是因为这一过程是在Android SDK层实现的。

我们回顾一下Cocos2dxActivity.init方法的关键代码:

    // Cocos2dxGLSurfaceView
    this.mGLSurfaceView = this.onCreateView();

    // …add to FrameLayout
    framelayout.addView(this.mGLSurfaceView);
    this.mGLSurfaceView.setCocos2dxRenderer(new Cocos2dxRenderer());
       
    // Set framelayout as the content view
    setContentView(framelayout);

Cocos2dxGLSurfaceView是 android.opengl.GLSurfaceView的子类。在android 上做原生opengl es 2.0编程的人应该都清楚GLSurfaceView的重要性。但渲染线程并非是在Cocos2dxGLSurfaceView实例化时被创建的,而是在 setRenderer的时候。

我们来看Cocos2dxGLSurfaceView.setCocos2dxRenderer的实现:

    public void setCocos2dxRenderer(final Cocos2dxRenderer renderer) {
        this.mCocos2dxRenderer = renderer;
        this.setRenderer(this.mCocos2dxRenderer);
    }

setRender是Cocos2dxGLSurfaceView父类GLSurfaceView实现的方法。在Android SDK GLSurfaceView.java文件中,我们看到:

       public void setRenderer(Renderer renderer) {
        checkRenderThreadState();
        if (mEGLConfigChooser == null) {
            mEGLConfigChooser = new SimpleEGLConfigChooser(true);
        }
        if (mEGLContextFactory == null) {
            mEGLContextFactory = new DefaultContextFactory();
        }
        if (mEGLWindowSurfaceFactory == null) {
            mEGLWindowSurfaceFactory = new DefaultWindowSurfaceFactory();
        }
        mRenderer = renderer;
        mGLThread = new GLThread(mThisWeakRef);
        mGLThread.start();

    }

GLThread的实例是在这里被创建并开始执行的。至于渲染线程都干了些什么,我们可以通过其run方法看到:

        @Override
        public void run() {
            setName("GLThread " + getId());
            if (LOG_THREADS) {
                Log.i("GLThread", "starting tid=" + getId());
            }

            try {
                guardedRun();
            } catch (InterruptedException e) {
                // fall thru and exit normally
            } finally {
                sGLThreadManager.threadExiting(this);
            }
        }

run方法并没有给我们带来太多有价值的东西,真正有价值的信息藏在guardedRun方法中。guardedRun是这个源文件中规模最为庞 大的方法,但抽取其核心结构后,我们发现它大致就是一个死循环,以下是摘要式的伪代码:

while (true) {
   synchronized (sGLThreadManager) {
       while (true) {
           …. …
           if (! mEventQueue.isEmpty()) {
               event = mEventQueue.remove(0);
               break;
           }
        }  
   }//end of synchronized (sGLThreadManager)

    if (event != null) {
       event.run();
       event = null;
       continue;
   }  

   if needed
       view.mRenderer.onSurfaceCreated(gl, mEglHelper.mEglConfig);

   if needed
       view.mRenderer.onSurfaceChanged(gl, w, h);

   if needed
       view.mRenderer.onDrawFrame(gl);
}

在这里我们看到了event、Renderer的三个回调方法onSurfaceCreated、onSurfaceChanged以及 onDrawFrame,后续我们会对这三个函数做详细分析的。

六、游戏逻辑的入口

在HelloCpp的Classes下有好多C++代码文件(涉及具体的游戏逻辑),在HelloCpp的android project jni目录下也有Jni胶水代码,那么这些代码是如何和引擎一起互动生效的呢?

上面讲到过,涉及到画面的一些渲染都是在GLThread中进行的,这涉及到onSurfaceCreated、 onSurfaceChanged以及onDrawFrame三个方法。我们看看 Cocos2dxRenderer.onSurfaceCreated方法的实现,该方法会在Surface被首次渲染时调用:

    public void onSurfaceCreated(final GL10 pGL10, final EGLConfig pEGLConfig) {
        Cocos2dxRenderer.nativeInit(this.mScreenWidth, this.mScreenHeight);
        this.mLastTickInNanoSeconds = System.nanoTime();
    }

该方法继续调用HelloCpp工程jni目录下的nativeInit代码:

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();
    }
    else
    {
        ccGLInvalidateStateCache();
        CCShaderCache::sharedShaderCache()->reloadDefaultShaders();
        ccDrawInit();
        CCTextureCache::reloadAllTextures();
        CCNotificationCenter::sharedNotificationCenter()->postNotification(EVENT_COME_TO_FOREGROUND, NULL);
        CCDirector::sharedDirector()->setGLDefaultValues();
    }
}

这似乎让我们看到了游戏逻辑的入口了:

    CCEGLView *view = CCEGLView::sharedOpenGLView();
    view->setFrameSize(w, h);

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

继续追踪CCApplication::run方法:

int CCApplication::run()
{
    // Initialize instance and cocos2d.
    if (! applicationDidFinishLaunching())
    {
        return 0;
    }

    return -1;
}

applicationDidFinishLaunching,没错这就是游戏逻辑的入口了。我们得回到Samples代码目录中去找到对应方法 的实现。

//cocos2d-x-2.2.2/samples/Cpp/HelloCpp/Classes/AppDelegate.cpp

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

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

    // turn on display FPS
    pDirector->setDisplayStats(true);

    // set FPS. the default value is 1.0/60 if you don't call this
    pDirector->setAnimationInterval(1.0 / 60);

    // create a scene. it's an autorelease object
    CCScene *pScene = HelloWorld::scene();

    // run
    pDirector->runWithScene(pScene);

    return true;
}

的确,在applicationDidFinishLaunching中我们做了很多引擎参 数的设置。接下来大管家CCDirector实例登场,并运行了HelloWorld Scene的实例。但这依旧是初始化的一部分,虽然方法名让人听起来像是某种持续连贯行为:

//cocos2d-x-2.2.2/cocos2dx/CCDirector.cpp

void CCDirector::runWithScene(CCScene *pScene)
{
    … …

    pushScene(pScene);
    startAnimation();
}

void CCDisplayLinkDirector::startAnimation(void)
{
    if (CCTime::gettimeofdayCocos2d(m_pLastUpdate, NULL) != 0)
    {
        CCLOG("cocos2d: DisplayLinkDirector: Error on gettimeofday");
    }

    m_bInvalid = false;
}

两个方法均只是初始化了某些数据成员变量,并未真正将引擎驱动起来。

七、驱动引擎

之所以游戏画面是运动的,那是因为屏幕以较高的帧数刷新的缘故,这样人眼就会看到连续的动作,就和电影的放映原理是一样的。在Cocos2d-x 引擎中这些驱动屏幕刷新的代码在哪里呢?

我们回顾一下之前谈到的GLThread线程,我们说过画面渲染的工作都是由它来完成的。GLThread的核心是guardedRun函数,该 函数以“死循环”的方式调用Cocos2dxRender.onDrawFrame方法对画面进行持续渲染。

我们来看看引擎实现的Cocos2dxRender.onDrawFrame方法:

public void onDrawFrame(final GL10 gl) {
        /*
         * FPS controlling algorithm is not accurate, and it will slow down FPS
         * on some devices. So comment FPS controlling code.
         */

        /*
        final long nowInNanoSeconds = System.nanoTime();
        final long interval = nowInNanoSeconds – this.mLastTickInNanoSeconds;
        */

        // should render a frame when onDrawFrame() is called or there is a
        // "ghost"
        Cocos2dxRenderer.nativeRender();

        /*
        // fps controlling
        if (interval < Cocos2dxRenderer.sAnimationInterval) {
            try {
                // because we render it before, so we should sleep twice time interval
                Thread.sleep((Cocos2dxRenderer.sAnimationInterval – interval) / Cocos2dxRenderer.NANOSECONDSPERMICROSECOND);
            } catch (final Exception e) {
            }
        }

        this.mLastTickInNanoSeconds = nowInNanoSeconds;
        */
    }

这个方法实现得比较奇怪,似乎修改过多次,但最后还是决定只保留了一个方法调用: Cocos2dxRenderer.nativeRender()。从注释掉的代码来看,似乎是想在这个方法中通过Thread.sleep来控制 Render Thread渲染的帧率。但由于控制的不理想,索性就不控制了,让guardedRun真正变成了dead loop。但从HelloCpp Sample运行时的状态显示,画面始终保持在60帧左右,让人十分诧异。据说Cocos2d-x 3.0版本重新设计了渲染这块的机制。(后记:在Android上虽然没有帧数控制,但真正的渲染帧率实际上还受到"垂直同步"信号 – vertical sync的影响。在游戏中,也许强劲的显卡迅速的绘制完一屏的图像,但是没有垂直同步信号的到达,显卡无法绘制下一屏,只有等vsync信号到达,才可以绘制。这样fps实际上要要受到操作系统刷新率值的制约)。

nativeRender从命名来看,这显然是一个C++编写的函数实现。我们只能到jni目录下寻找。

cocos2d-x-2.2.2/cocos2dx/platform/android/jni/ Java_org_cocos2dx_lib_Cocos2dxRenderer.cpp

    JNIEXPORT void JNICALL Java_org_cocos2dx_lib_Cocos2dxRenderer_nativeRender(JNIEnv* env) {
        cocos2d::CCDirector::sharedDirector()->mainLoop();
    }

nativeRender也很简洁,直接调用了CCDirector的mainLoop,也就是说每帧渲染过程中真正干活地是 CCDirector::mainLoop。到此我们终于找到了引擎渲染的驱动器:GLThead::guardedRun,以“死循环”的方式刷新着画面,让我们感受到“动”的魅力。

八、mainLoop

进一步我们来看看mainLoop所做的工作。mainLoop是CCDirector类的一个纯虚函数,CCDirector的子类CCDisplayLinkDirector真正实现了 它:

//CCDirector.cpp
void CCDisplayLinkDirector::mainLoop(void)
{
    if (m_bPurgeDirecotorInNextLoop)
    {
        m_bPurgeDirecotorInNextLoop = false;
        purgeDirector();
    }
    else if (! m_bInvalid)
     {
         drawScene();

         // release the objects
         CCPoolManager::sharedPoolManager()->pop();
     }
}

void CCDirector::drawScene(void)
{
    // calculate "global" dt
    calculateDeltaTime();

    //tick before glClear: issue #533
    if (! m_bPaused)
    {
        m_pScheduler->update(m_fDeltaTime);
    }

    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    /* to avoid flickr, nextScene MUST be here: after tick and before draw.
     XXX: Which bug is this one. It seems that it can't be reproduced with v0.9 */
    if (m_pNextScene)
    {
        setNextScene();
    }

    kmGLPushMatrix();

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

    // draw the notifications node
    if (m_pNotificationNode)
    {
        m_pNotificationNode->visit();
    }

    if (m_bDisplayStats)
    {
        showStats();
    }

    kmGLPopMatrix();

    m_uTotalFrames++;

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

    if (m_bDisplayStats)
    {
        calculateMPF();
    }
}

帧渲染由mainLoop调用的drawScene()完成,drawScene方法根据Scene下的渲染树,根据node的最新属性逐个渲染 node,并调整各个Node的调度定时器数据,细节这里就不详细说明了。

九、UI线程与GLThread的交互

用户的屏幕触控动作由UI线程捕捉到,该类事件需要传递给引擎,并由GLThread根据各个画面元素的最新状态重新绘制画面。UI线程负责处理用户交互 事件,并将特定的事件通知GLThread处理。UI线程通过Cocos2dxGLSurfaceView的queueEvent方法,将事件以及处理方 法传递给GLThread执行的。

Cocos2dxGLSurfaceView的queueEvent方法继承自其父类GLSurfaceView:

    public void queueEvent(Runnable r) {
        mGLThread.queueEvent(r);
    }

而GLThread的queueEvent方法实现如下:

public void queueEvent(Runnable r) {
    if (r == null) {
        throw new IllegalArgumentException("r must not be null");
    }  
    synchronized(sGLThreadManager) {
        mEventQueue.add(r);
        sGLThreadManager.notifyAll();

    }  
}

该方法将event互斥地放入EventQueue,并通知阻塞在Queue上的线程取货。

运行着的GLThread实例在guardedRun中会从event队列中取出runnable event并run的。
  
while (true) {
    synchronized (sGLThreadManager) {
        while (true) {
            if (mShouldExit) {
                return;
            }  

            if (! mEventQueue.isEmpty()) {
                event = mEventQueue.remove(0);
                break;
            }  
         …….
        }  
     }  

     … …
     if (event != null) {
        event.run();
        event = null;
        continue;
    }  
    …
}

Activity的各种事件Pause、Resume、Stop以及View的各种屏幕触控事件都是通过queueEvent传递给GLThread执行的,比如:View的onKeyDown方法:

    //Cocos2dxGLSurfaceView.java
    @Override
    public boolean onKeyDown(final int pKeyCode, final KeyEvent pKeyEvent) {
        switch (pKeyCode) {
            case KeyEvent.KEYCODE_BACK:
            case KeyEvent.KEYCODE_MENU:
                this.queueEvent(new Runnable() {
                    @Override
                    public void run() {
                        Cocos2dxGLSurfaceView.this.mCocos2dxRenderer.handleKeyDown(pKeyCode);
                    }
                });
                return true;
            default:
                return super.onKeyDown(pKeyCode, pKeyEvent);
        }
    }

十、小结

有了以上的对Cocos2d-x引擎的理解后,再编写游戏代码就更加游刃有余了,至少出现问题时,我们知道应该在哪里查找了。就像对汽车的发动机了如指掌 后,一旦发生动力故障,我们基本知道排除的方法。但对发动机了解的再透彻,也不能代表就能设计和生产出好车,游戏也是这样,对引擎了解是一码事,设计和实 现出好游戏是另外一码事。学习引擎只是编写游戏的起点而已。

说说执行力

No Comments

You are never to dictate what I can and can not do. The only two words I want to hear from you when I ask you to do something are "Yes" and "Sir"。(我能做什么不能做什么,你管不着。我吩咐你做事的时候,只想听到两个词,"是的"和"先生"。)
                                                                                   – 《纸牌屋》第一季

想必大家都基本认同:最有执行力的团队莫过于军队。军队有纪律约束,有荣誉感引导。在战时,违抗命令者是可以被直接拉出去枪毙的(影视剧中^_^)。但在 现实中你的团队里,你行么?别说枪毙,说了一句刺耳的批评的话,都可能招来各种不合作。大多数工作并非金饭碗,Google , FB的员工还经常跳来跳去呢,“此处不留爷,自有留爷处”才是硬道理。

那我们靠什么保证团队执行力呢?

靠NB人士?也许可行。但看看你的荷包,金子够么?NB人士属于金字塔顶端,数量稀少,价格昂贵,且多聚集在国内外知名名企。对于普通企业来说,操作起来有极大困难。
大把金钱奖励?我们不是土豪,没有挥金如土的气魄,钱不是不可以给,但要用在刀刃上。
精神鼓励?可以用,但不能一直用,否则下面就要说你是“精神病”了。

回归本源,执行力是建立在员工的职业操守上的。在这样一个前提下,我们至少要做好三件事来保证执行力。

1、明确责任
让员工确定、一定以及肯定的明确这件事/这些事的责任人就是他/她。做好了获奖,做少了、做错了、做砸了,他就是“罪魁祸首”。

2、设立检查点
为了帮助员工工作在正确的方向上,不走偏,不做错,不漏做,我们要帮助员工建立好检查点,明确告知检查点所有内容。在检查点上有针对性的检查也是员工的责任之一。

3、频繁反馈
团队负责人应该像老妈子似的不断的催促和获取反馈,以即时把握执行力的真实情况。

锡恩的4R执行力理论还强调了一个“即时奖励”,强化任务执行与后果之间的关系,让员工第一时间感受到良好执行带来的成果,获得成就感和认同。

接下来就要将以上三件事耐心的变成制度、变成流程,耐心地让员工按流程要求工作而不是按负责人的指令行事。

最后将以上流程自动化。通过系统分配任务,通过任务单将任务相关的信息整合在一起,为执行者提供帮助。通过这样的一个系统排除管理者的人肉提醒、减少在以 上环节中的人为疏漏(比如缺少检查点,忘记反馈),尽可能排除人的惰性、忘性等带来的种种执行问题。通过系统为任务执行作出评价,可作为员工绩效的参考。

通过以上的措施,还可以很好的应对“熟人文化”:不是按某人的指令,而是“制度规程”去做事,按照执行力系统的分配、提醒和通知去执行、去反馈。

这样一来,管理者也可以从繁重的“人肉管理”中脱离出来,既可以通过系统众览全局进度,保证任务的按时正确执行,也可以将精力更多的倾注在其他重要方面的工作中。

关于2014团队改善的考量

1 Comment

一个人的品行,不取决于这人如何享受胜利,而在于这人如何忍受失败。
                                                                       — 《纸牌屋》第一季

团队改善,不是那种很快见到成果或者效益的活儿。

但这件事你做不做呢?坦诚的说,今年我在这方面的“热情”真的不是那么高,肯定是不如前两年了,因为是时候更多地为自己的“前途”考虑考虑了。团队改善这 种活儿做好了还行,做坏了,那就成为“把柄”,成为劣迹。投入了资源,却不见成果。因为领导层可能从来都没有让你去做什么改变,也不关心你要做什么改变。 只要把领导认为要做的事情做好即可。做团队改善这事儿,纯属自己给自己加的“私活儿”:长路漫漫,遍地荆棘,费力还不一定讨好。

但身在其位,团队没有任何改善或进步不是我的风格,因此2014年,这事儿还要继续做,并且更难做。用现在一个时髦的词来形容,好比进入了“深水区”。

改善的初衷是总是好的,但在实际操作中也有可能带来负面的影响,比如因流程变化或工具切换导致的一时的效率下降。人都是不习惯于变化的,并且改变可能会触 动某些人的利益,因此阻力可想而知。从全局来看,这又是必须去做的事情。总之改善的道路坎坷,顶住压力是必须的。有些时候压力更多是来自于上面。当领导问 如果失败了谁负责,这时你应该毫不犹豫的说:我负责。没有别的选择,这就是改变带来的代价,”我不入地狱谁入地狱呢“。

我理想中的团队应该是一部精密的机器,开机后不用管的,自运行的,并生产出正确让人满意的成果。我的目标就是搭建出这样一部机器,在生产过程中尽量降低人 的因素的影响:比如惰性、忘性、马虎、态度不端正等对成果物质量的影响。我们还要在“关键环节”加入“自动”地检查,就像传统工厂的质检环节那样,这些 “检查”环节让低质量的成果无法通过。通过这些环节,你还可以全盘掌握产品的生产和质量情况。

而我就是幕后的那个“导演”。我发现自己越来越喜欢“导演”这一角色了。策划着这一切,看到一切都按照你的思路一步一步的进行下去的。导演决定了是否能拍出好片子,即便演员不一定都是大腕儿。

有同事建议我能针对一些改善措施和想法做些宣讲。我回绝了。因为大家都是那种喜欢看到结果的,在结果未出来之前,还是多做少说。这样也可以避免外界干扰你 的做事思路。另外没有两个团队面对的情况是一模一样的,也就没有一致的改善的方法,有些事情不能代劳。自己发现的问题才真实,才接地气。

坚持你认为正确的事情,坚定的做下去就是了,其他的都抛到脑后。

厨房里的领导课

No Comments

生活中永远不缺少大道理,缺的是一颗善于思考和发现它们的心。
                                                                        – Tony Bai

晚上回到家,家人端上来热腾腾的饭菜。吃了几口,感觉味道较为普通。盘子里那些被加工过的食材是昨天刚刚买到的,又好又新鲜。顿然一种可惜的赶脚油然而 生。为什么这么上好新鲜的食材经过家人的烹制就变得这么普通了呢,仅仅是变成了充饥之用。而这些食材在大厨手下却能妙笔生花,做出让人流连忘返的精美菜 肴。我不是很懂厨艺,但总觉的大厨烹制菜肴的过程与领导团队做一个项目或开发一款产品有着相似的内涵。小小厨房中蕴含着某些大道理,值得我在这里深思一番。

* 角色定义

既然将大厨烹制菜肴比作领导团队做事,那么我们先来熟悉一下厨房里的各个角色:

    厨房 – 工作间
    厨子 – Leader
    食材、调料 – 团队成员
    厨房用品 – 基础设施
    食客  – 使用者,客户
    营养、口感、档次 – 主要Feature
    味道 – 用户体验

    任务目标 – 制作一道色香味俱佳的菜肴。
 

* 好厨善选材

选材是作出上好菜肴的关键。好厨都是善选材的,就好比好领导知道应该选择什么样的组员加入团队。

主食材(团队主力)决定菜肴的基本品质,比如:档次、外观、营养、口味等。

选择食材要主次分明,相辅相成。有红花争艳,也要有绿叶陪衬,否则烧成的菜品就会内斗严重,主次不明,类别不清,让人迷惑。

选择食材切忌相声相克。一旦这样,很可能会彻底毁掉这道菜。即便做成,也可能给食客带来损害。

调料,好比美工、前端设计师,专攻用户体验。虽然主食材实现了菜肴的核心营养和口感(核心Feature),但如果没有调料的作用,就缺少了味道这一决定性的用户体验。没有好的用户体验,结果一样是失败,至少产品或结果算不上一流。

* 火候儿甚重要

大厨的另外一个特长就是对烹饪过程中火候的把握极其到位。对于不同菜肴,不同食材,大厨会选择不同的火候烹制,让食材保持最佳状态,适当吸收汤汁味道。用 错了火候,将是灾难性的。本该用文火慢炖入味的,却用了旺火,结果食不入味,对食客来说毫无吸引力;本来用旺火快速炒制的,却用了文武火,导致菜肴制作时 间较长,营养成分流失。滥用火候甚至可能将食材烧焦烧糊,使得菜肴制作彻底失败。

这就好比团队Leader对团队工作节奏、进度以及压力的控制,要让团队成员在适宜的环境下发挥出最大的潜力、进行最高效的工作。过大的压力,过紧的进度都可能会压跨团队,无法达成团队目标。

* 装备,装备,事半功倍

厨子烹制佳肴离不开厨房用具,虽说厨房用具的好坏对于菜肴的最终烹制结果不起决定性的作用,但精良全面的厨房用具会使得大厨烹制菜肴的过程事半功倍。

从古至今均有美酒佳肴,但非要论一下到底什么时候的菜更好吃,相信没人能给出结论,也许古代人做的菜更好吃也不一定。但能够确定的是现代大厨所使用的厨具 肯定要比古代人更加齐全和精良,这使得现代人可以快速制作出大量精美的菜肴,可以在一定程度上缩短了菜肴制作的周期。通过这些现代化的厨具,可以更加精确 量化菜肴的营养成分,也可以将菜肴的制作工序程式化,以供复用。

另外不同的厨师团队使用的装备各有不同,无所谓新老,找到最适宜的才是最好。

* 厨房里的创新

中华饮食文化源远流长,创新菜品层出不穷。从厨师的角度来看,这些创新无非是创新的食材、创新的调料以及创新的烹制方法。映射的团队管理来看,领导者应善于引进创新的人才,并敢于进行组织、管理以及过程方法创新,这样才能有创新的产品、创新的文化和氛围。

2013小结

No Comments

2013年的个人年终总结比以往来得晚了一些,至于原因,我也说不清楚,拖延症也罢,其他原因也罢,总之是晚了。

写年终小结已经有小几年了,风格一直如一,无非是老三样:工作得失、生活酸甜以及新年展望,今年也不利外。

* 工作篇

我们部门在所在行业里已经摸爬滚打了10多年了,经 历和见证了这个行业从诞生、增长、成熟到如今的衰退的整个过程。也正是由于处于行业的衰退期,2013年部门的运营十分艰难。十年对于任何一个行业来说, 可能都已经过了其巅峰期,真心不能再期望这个行业还能会有下一个高峰了,对于个人来说也是如此。转型、业务突破变成了领导常挂在嘴边的词汇,但做起来又何 其艰难。

2013年,我们的业务转型依旧是围绕着我们的“金主”,虽然他们的业务营收也受到了微信等OTT业务的极大影响,传统业务投资也在缩减。对于个人而言, 除了负责传统产品线,转型、业务突破也成了我的绩效目标的一部分。于是在2013年,写文档比写代码多了一点,出差比常年多了一点,周六周日的连续加班也 多出了一点。在这些尝试中,以5月份某运营商某信的重构项目最为让人印象深刻。为了这个合同额几个亿的项目,我们近30人连续奋战了一个多月编写技术建议 书和投标方案,过程辛苦但却颇感充实,最终我们拿到了两个第二、两个第三的成绩。也许这个结果对于公司来说算是一种失败,但对于我个人来说,我获得了些许 转型的信心,以至于在后续的几次投标资料编写过程中,面对较新的领域,我也可以镇定自若。

掐指算来,这一年我以咨询顾问的临时角色参与了8个大大小小项目的前期交流以及投标支持工作,其中六个标以失败或不了了之而告终,还有两个标尚未有最终结 果。对于这样的结果,我也只能表示无奈。虽然我心里也十分清楚,对于国内这类解决方案项目的投标,技术往往不是最重要的,况且对于这些新领域,我们的技术 储备还不够系统,积累较为浅薄,落地的也的确较少。但面对这样的局面,我们还能怎么做呢?我也期待新一年能得到一个新的答案。

当然2013的工作中不全是遗憾,年末之前新系统的上线算是为我的2013划上了一个还算不错的句号,毕竟这是我两年来为之付出最多,也是最重要的一个工作目标。另外2013年继续整理和总结自己的一些管理经验工作原则。在过程方面继续深入改善,尤其在代码质量方面。

在技术精深方面,今年没有太多进步。年初的时候曾探讨过如何在现有项目中使用一些成熟的开源技术和产品,比如memcachedzookeeper等, 为了保持手热,还尝试做些算法类的编码,这个在experiments库中有体现。在其他方面,可谓是“三无状态”:无技术书籍翻译、无技术杂志投稿、无 新开源项目发起。另外今年没有尝试去学习什么新语言,理由在此

在年末的绩效评审时,观察到一些现象:那些绩效最末尾的人,往往并非是自身不够努力,而是领导赋予的目标不明确,这会给下属带来更多的不安,多数下属也会因为工作目标的不明确,而表现出更为糟糕的绩效。

* 生活篇

我个人十分注重工作和生活的平衡,不知道这种理念对于一个革命尚未成功的人来说算不算正确。

今年写了56篇博文,只完成了计划值的3/4,算是可接受范围,博文质量有所提升,访问次数和评论反馈也多了许多。文章以技术理解偏多,深入的偏少。技术攻关还是留给年轻人去吧。另外就是经验总结和感悟偏多,这也许与工作年头多有关系吧。

读书方面,据豆瓣不完全统计一共读了61本,这照比去年要多出不少,想必是有了Kindle PaperWhite的缘故吧,使得碎片时间得到充分利用。技术、商业书籍依旧占较大比例,小说尤其是科幻小说也不少。同样是因为电子书,今年纸质书籍购 买减少了(痛定思痛后的决定),双十一、双十二以及圣诞促销均没有出手。不过豆瓣上想读的书单依旧还有上百本^_^,任重道远啊。

今年爱上了跑步,坚持到11月末,因出差和天气转深寒等原因,决定暂停一段时间,等春节后气温回升时再拾起这个好习惯,相信不是大问题。跑步的确让我的身体状况大为好转,至少感冒次数大为下降。                 

今年的一些家庭目标也多已实现,比如和老婆一起去香港、带孩子去海边玩等。数码装备也更新了一圈。

果果这个小家伙那叫一个茁壮成长啊。年中给她换了一个大的幼儿园,她也变得十分喜欢和小朋友在一起玩了,有时候还觉得在家里没有意思。每周果果还要上一节 她最喜欢的舞蹈课,我们的初衷就是让她多与小朋友老师接触,也不指望她能学出什么样子来,不过她学得倒是有模有样,十分认真。现在的果果简直就是一个小大 人,每天从早到晚说个不停,精力那叫一个充沛,有时候不得不强迫她去睡觉^_^。

* 新年展望

感觉这一年的进步有些差强人意,心底真心感觉自己的努力还是太少了,于是立下了“少睡觉,多干活”的目标。

新的一年,无论是个人还是工作,都要更多的思考如何将知识、技能和经验转化为更多价值,如何将业务经验、技术积累转化为合同。

新的一年,要主动适应转型,无论是工作上的还是个人方向上的,争取在这一年里能找到正确的方向,并成功入门。最好给自己做一个三年到五年的布局。

新的一年,尝试继续保持生活与工作的平衡,也许这将变成一种奢侈的期望。

新的一年,还有什么比全家健康快乐更重要的呢。

Older Entries