cocos2dx挖掘

轉載自:http://tonybai.com/2014/05/


三、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引擎的理解後,再編寫遊戲代碼就更加遊刃有餘了,至少出現問題時,我們知道應該在哪裏查找了。就像對汽車的發動機瞭如指掌 後,一旦發生動力故障,我們基本知道排除的方法。但對發動機瞭解的再透徹,也不能代表就能設計和生產出好車,遊戲也是這樣,對引擎瞭解是一碼事,設計和實 現出好遊戲是另外一碼事。學習引擎只是編寫遊戲的起點而已。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章