1. 程式人生 > >cocos2d-x的場景類和生命週期

cocos2d-x的場景類和生命週期

在上一篇的文章已經通過程式碼分析了場景的跳轉是在主迴圈中setNextScene進行呼叫的,那麼在跳轉時便會開始呼叫生命週期函式。主要由以下四個函式組成

onEnter、onEnterTransitionDidFinish、onExitTransitionDidStart、onExit,這四個分別定義在CCNode的節點類中。所以很明顯,CCScene場景類是繼承自於CCNode。

CCScene的內容非常的簡單,它本質上就是一個非常普通的節點類,然後再把其他的節點全部掛載在這個節點下進行渲染和控制,也就是說它一個裝載了遊戲內容的一個大容器。

class CC_DLL CCScene : public CCNode
{
public:
    /**
     *  @js ctor
     */
    CCScene();
    /**
     *  @js NA
     *  @lua NA
     */
    virtual ~CCScene();
    bool init();

    static CCScene *create(void);
};
CCScene::CCScene()
{
    m_bIgnoreAnchorPointForPosition = true;
    setAnchorPoint(ccp(0.5f, 0.5f));
}

CCScene::~CCScene()
{
}

bool CCScene::init()
{
    bool bRet = false;
     do 
     {
         CCDirector * pDirector;
         CC_BREAK_IF( ! (pDirector = CCDirector::sharedDirector()) );
         this->setContentSize(pDirector->getWinSize());//初始化大小為螢幕視窗大小
         // success
         bRet = true;
     } while (0);
     return bRet;
}

CCScene *CCScene::create()
{
    CCScene *pRet = new CCScene();
    if (pRet && pRet->init())
    {
        pRet->autorelease();
        return pRet;
    }
    else
    {
        CC_SAFE_DELETE(pRet);
        return NULL;
    }
}
這個類確實是很簡單,沒有多少內容,但它確實承載遊戲內容的一個宿主,佔據極大的一塊記憶體空間,所以在跳轉時要非常的清楚記憶體的控制。

因為在場景切換是不論是呼叫那種切換函式(pushScene或replaceScene),程式中都會有儲存兩個場景的記憶體開銷,因為切換函式都是不會先銷燬上一個場景,再建立新場景的。

void CCDirector::replaceScene(CCScene *pScene)
{
    CCAssert(m_pRunningScene, "Use runWithScene: instead to start the director");
    CCAssert(pScene != NULL, "the scene should not be null");

    unsigned int index = m_pobScenesStack->count();
    //替換場景時要清除通知,isSendCleanupToScene方法的註釋中有提到
    m_bSendCleanupToScene = true;
    //將棧頂中的場景替換成即將要執行的場景,這樣便銷燬了上一個場景的資源(裡面有一個release),但在這之前程式中始終會佔有兩個場景的記憶體空間
    m_pobScenesStack->replaceObjectAtIndex(index - 1, pScene);
    //將此棧賦給m_pNextScene,注意m_pNextScene是一個弱引用(這樣在主迴圈中便會呼叫切換場景的方法setScene)
    m_pNextScene = pScene;
}

void CCDirector::pushScene(CCScene *pScene)
{
	//斷言判斷場景是否為空
    CCAssert(pScene, "the scene should not null");
    //入棧時不需要通知,isSendCleanupToScene方法的註釋中有提到
    m_bSendCleanupToScene = false;
    //加入棧中的佇列中,由一個數組來維護
    m_pobScenesStack->addObject(pScene);
    //將此棧賦給m_pNextScene,注意m_pNextScene是一個弱引用(這樣在主迴圈中便會呼叫切換場景的方法setScene)
    m_pNextScene = pScene;
}

void CCDirector::popScene(void)
{
    CCAssert(m_pRunningScene != NULL, "running scene should not null");
    //出棧——刪除陣列中的最後一個元素
    m_pobScenesStack->removeLastObject();
    //獲取當前場景棧的數量
    unsigned int c = m_pobScenesStack->count();
    //如果當前棧沒有場景,這退出主迴圈,結束遊戲
    if (c == 0)
    {
        end();
    }
    else
    {
        m_bSendCleanupToScene = true;//通知清除訊息
        m_pNextScene = (CCScene*)m_pobScenesStack->objectAtIndex(c - 1);//將最後一個場景賦給m_pNextScene(也就是出棧時的倒數第二個場景),注意m_pNextScene是一個弱引用(這樣在主迴圈中便會呼叫切換場景的方法setScene)
    }
}
通過上面的註釋可以很清楚的看到在切換場景時引擎所做的事情,也解釋了為什麼跳轉時推薦使用replaceScene(因為replaceScene會release掉被替換的場景,但這裡的release還不會觸發引擎去呼叫被替換場景的解構函式,主要是在切換場景函式中setNextScene中也有個retian,所以真正觸發是在setNextScene的release中,這樣做便能保證被替換的場景的生命週期函式可以完整的被執行,也就是onExitTransitionDidStart和onExit方法)。但上面的那個問題依舊還是會存在(如果兩個場景都佔有非常大的記憶體空間的話,這個問題便會十分嚴重),所以比較好的做法便是建立一個過渡場景,雖然也是個場景,但極低的記憶體資源,因為它與遊戲的內容無關,只是用來過渡而已,這時便要配合場景的生命週期來實現。

具體的思路如下:

正在執行中的場景標記為sc1。

過渡場景標記為sc2。

即將要執行的場景標記為sc3.

1. 建立一個sc2,建立完後呼叫replaceScene切換,切換完後主迴圈便會檢測到需要執行場景切換操作。

2. 在場景切換操作中會涉及到場景的生命週期函式的呼叫。從之前的程式碼可以看到其過程如下:

1)sc1呼叫onExitTransitionDidStart,然後再呼叫onExit,這時可以在這裡進行清除sc1的操作。這樣便把sc1的記憶體資源回收了。

2)sc2呼叫onEnter,然後再呼叫onEnterTransitionDidFinish,這時可以再onEneter中呼叫sc3的建立,然後再onEnterTransitionDidFinish中呼叫replaceScene進行場景切換。這樣就能把即將要執行的場景入棧。

這裡還要注意下是否是通過CCTransitionScene跳轉的情況(大部分都是通過CCTransitionScene來跳轉的)

 對於上面來說是是沒有CCTransitionScene或其子類來進行跳轉的,其過程便是直接的sc1->onExitTransitionDidStart,sc1->onExit,sc1->onEnter,sc1->onEnterTransitionDidFinish。但對於通過CCTransitionScene來跳轉的確又是不一樣的,因為這種情況是通過CCTransitionScene來控制目標場景的生命週期函式的呼叫的。其過程還是用程式碼來說明:

首先先觀察建立CCTransitionScene時的操作

CCTransitionScene * CCTransitionScene::create(float t, CCScene *scene)
{
    CCTransitionScene * pScene = new CCTransitionScene();
    if(pScene && pScene->initWithDuration(t,scene))
    {
        pScene->autorelease();
        return pScene;
    }
    CC_SAFE_DELETE(pScene);
    return NULL;
}

bool CCTransitionScene::initWithDuration(float t, CCScene *scene)
{
    CCAssert( scene != NULL, "Argument scene must be non-nil");

    if (CCScene::init())
    {
        m_fDuration = t;

        // retain
        m_pInScene = scene;//儲存目標場景的指標
        m_pInScene->retain();
        m_pOutScene = CCDirector::sharedDirector()->getRunningScene();//獲取即將被結束的場景的指標
        if (m_pOutScene == NULL)//沒有執行的場景,則建立一個空的(第一次運行遊戲時便會有此情況)
        {
            m_pOutScene = CCScene::create();
            m_pOutScene->init();
        }
        m_pOutScene->retain();

        CCAssert( m_pInScene != m_pOutScene, "Incoming scene must be different from the outgoing scene" );
        
        sceneOrder();//設定繪製順序,不同子類有不同的繪製順序

        return true;
    }
    else
    {
        return false;
    }
}

可以看見裡CCTransitionScene握有正在執行中的場景和將要執行的場景。接下來再看切換場景方法。
void CCDirector::setNextScene(void)
{
	//當第一次執行時runningIsTransition肯定為假,因為是主函式直接pushScene進來的一個場景,是沒有被CCTransitionScene所管理的
    bool runningIsTransition = dynamic_cast<CCTransitionScene*>(m_pRunningScene) != NULL;
	//這裡newIsTransition的為true,因為即將要執行的場景是通過CCTransitionScene包含進來的
    bool newIsTransition = dynamic_cast<CCTransitionScene*>(m_pNextScene) != NULL;
	//跳過此if
     if (! newIsTransition)//如果不是跳轉進來的,而是直接切換的則直接清空上一個場景的資源
     {
         if (m_pRunningScene)//這裡要先判斷m_pRunningScene是否為空,因為第一次載入場景時m_pRunningScene肯定是為NULL
         {
             m_pRunningScene->onExitTransitionDidStart();//如果用CCTransitionScene跳轉時會進入到CCTransitionScene的onExitTransitionDidStart中
             m_pRunningScene->onExit();//如果用CCTransitionScene跳轉時會進入到CCTransitionScene的onExit中
         }
 
         // issue #709. the root node (scene) should receive the cleanup message too
         // otherwise it might be leaked.
         if (m_bSendCleanupToScene && m_pRunningScene)//如果清除場景需要收到清除訊息,則呼叫cleanup方法
         {
             m_pRunningScene->cleanup();
         }
     }

    if (m_pRunningScene)//釋放上一個場景的資源
    {
        m_pRunningScene->release();
    }
    m_pRunningScene = m_pNextScene;//將要執行的場景賦給表示正在執行的場景的指標(CCTransitionScene的指標)
    m_pNextScene->retain();
    m_pNextScene = NULL;//清掉m_pNextScene,直到有新場景入棧

    //由於這裡之的runningIsTransition為false,所以會進入以下兩個函式的呼叫,這時的m_pRunningScene已經被CCTransitionScene所包含了
	//所以下面的onEnter和onEnterTransitionDidFinish實際上是呼叫CCTransitionScene的onEnter和onEnterTransitionDidFinish,這樣便間接的呼叫了目標場景的生命週期函式。
    if ((! runningIsTransition) && m_pRunningScene)//進入新場景的生命週期
    {
        m_pRunningScene->onEnter();
        m_pRunningScene->onEnterTransitionDidFinish();
    }
}
這時已經呼叫了CCTransitionScene的onEnter和onEnterTransitionDidFinish方法(CCTransitionScene並沒有重寫onEnterTransitionDidFinish方法,也就是這裡只通過onEnter來控制,然後再一次跳轉時會呼叫onExit來控制)了。
void CCTransitionScene::onEnter()
{
    CCScene::onEnter();
    
    // disable events while transitions
    CCDirector::sharedDirector()->getTouchDispatcher()->setDispatchEvents(false);
    
    // outScene should not receive the onEnter callback
    // only the onExitTransitionDidStart
    //呼叫舊場景的onExitTransitionDidStart方法
    m_pOutScene->onExitTransitionDidStart();
    //呼叫新場景的onEnter
    m_pInScene->onEnter();
}
這樣整個生命週期函式便明朗了,現在為止,其呼叫的過程如下:

舊場景的onEnter和onExitTransitionDidStart先被呼叫,然後再切換時,會呼叫舊場景的onExitTransitionDidStart,然呼叫新場景的onEnter。

這裡要清楚一點的是CCTransitionScene只是個過渡的場景,並不是真正的目標場景,自己也會有生命週期,也就是說當執行完以上過程後,還沒有執行對真正的目標場景的跳轉。但不幸的是,CCTransitionScene本身並沒有執行最後的跳轉,而是轉交給了它的子類去實現跳轉(這樣便能實現多種多樣的跳轉效果了,這裡以CCTransitionFade來舉例),所以當以以下方式來跳轉時,是不會發生真正的跳轉的,也就是跳轉失敗,依舊停留在舊場景。

	CCTransitionScene* t = CCTransitionScene::create(1.2f, TestScene::scene());
	CCDirector::sharedDirector()->replaceScene(t);

所以這裡一定要CCTransitionScene的子類來包裝(不同的子類,不同的跳轉效果),進入到CCTransitionFade的onEnter方法
void CCTransitionFade :: onEnter()
{
	//呼叫父類的onEnter
    CCTransitionScene::onEnter();

    CCLayerColor* l = CCLayerColor::create(m_tColor);
    m_pInScene->setVisible(false);

    addChild(l, 2, kSceneFade);
    CCNode* f = getChildByTag(kSceneFade);

    CCActionInterval* a = (CCActionInterval *)CCSequence::create
        (
            CCFadeIn::create(m_fDuration/2),
            CCCallFunc::create(this, callfunc_selector(CCTransitionScene::hideOutShowIn)),//CCCallFunc::create:self selector:@selector(hideOutShowIn)],
            CCFadeOut::create(m_fDuration/2),
            //注意這個回撥函式
            CCCallFunc::create(this, callfunc_selector(CCTransitionScene::finish)), //:self selector:@selector(finish)],
            NULL
        );
    f->runAction(a);
}

也就是說CCTransitionFade在執行完效果後最後會呼叫CCTransitionScene::finish,再看這個方法的實現
void CCTransitionScene::finish()
{
    // clean up     
     m_pInScene->setVisible(true);
     m_pInScene->setPosition(ccp(0,0));
     m_pInScene->setScale(1.0f);
     m_pInScene->setRotation(0.0f);
     m_pInScene->getCamera()->restore();
 
     m_pOutScene->setVisible(false);
     m_pOutScene->setPosition(ccp(0,0));
     m_pOutScene->setScale(1.0f);
     m_pOutScene->setRotation(0.0f);
     m_pOutScene->getCamera()->restore();

    //[self schedule:@selector(setNewScene:) interval:0];
     //新場景的跳轉
    this->schedule(schedule_selector(CCTransitionScene::setNewScene), 0);

}

void CCTransitionScene::setNewScene(float dt)
{    
    CC_UNUSED_PARAM(dt);

    this->unschedule(schedule_selector(CCTransitionScene::setNewScene));
    
    // Before replacing, save the "send cleanup to scene"
    CCDirector *director = CCDirector::sharedDirector();
    m_bIsSendCleanupToScene = director->isSendCleanupToScene();
    //這裡又執行了一次replaceScene的呼叫,這樣便觸發了對真正的即將要執行的場景的跳轉,同時也將CCTransitionScene的資源進行了回收
    director->replaceScene(m_pInScene);
    
    // issue #267
    m_pOutScene->setVisible(true);
}

上面已經註釋了最後跳轉的過程,那麼當replaceScene被執行時,切換函式肯定又執行了不一樣的流程。
void CCDirector::setNextScene(void)
{
	//這時的runningIsTransition為true,因為當前執行的場景確實是CCTransitionScene
    bool runningIsTransition = dynamic_cast<CCTransitionScene*>(m_pRunningScene) != NULL;
	//這裡newIsTransition的為false,因為m_pNextScene已經被重置為目標場景了,而不是被CCTransitionScene所包含的場景
    bool newIsTransition = dynamic_cast<CCTransitionScene*>(m_pNextScene) != NULL;
	
     if (! newIsTransition)//進入以下程式碼,完成生命週期的呼叫
     {
		 //因為當前執行的場景是CCTransitionScene
		 //所以下面的onExitTransitionDidStart和onExit實際上是呼叫CCTransitionScene的onExitTransitionDidStart和onExit,這樣便間接的完成了整個生命週期函式的呼叫
         if (m_pRunningScene)//這裡要先判斷m_pRunningScene是否為空,因為第一次載入場景時m_pRunningScene肯定是為NULL
         {
             m_pRunningScene->onExitTransitionDidStart();//如果用CCTransitionScene跳轉時會進入到CCTransitionScene的onExitTransitionDidStart中
             m_pRunningScene->onExit();//如果用CCTransitionScene跳轉時會進入到CCTransitionScene的onExit中
         }
 
         // issue #709. the root node (scene) should receive the cleanup message too
         // otherwise it might be leaked.
         if (m_bSendCleanupToScene && m_pRunningScene)//如果清除場景需要收到清除訊息,則呼叫cleanup方法
         {
             m_pRunningScene->cleanup();
         }
     }

    if (m_pRunningScene)//釋放上一個場景的資源,也就是CCTransitionScene
    {
        m_pRunningScene->release();
    }
    m_pRunningScene = m_pNextScene;//將要執行的場景賦給表示正在執行的場景的指標(CCTransitionScene的指標)
    m_pNextScene->retain();
    m_pNextScene = NULL;//清掉m_pNextScene,直到有新場景入棧

    //由於這裡之的runningIsTransition為true,所以會不會進入以下執行
    if ((! runningIsTransition) && m_pRunningScene)
    {
        m_pRunningScene->onEnter();
        m_pRunningScene->onEnterTransitionDidFinish();
    }
}

這樣便將最後的執行呼叫交給了CCTransitionScene的onExitTransitionDidStart(CCTransitionScene同樣也沒有重寫該方法,同樣值通過onExit來控制)和onExit。
void CCTransitionScene::onExit()
{
    CCScene::onExit();
    
    // enable events while transitions
    CCDirector::sharedDirector()->getTouchDispatcher()->setDispatchEvents(true);
    //呼叫舊場景的onExit方法
    m_pOutScene->onExit();

    // m_pInScene should not receive the onEnter callback
    // only the onEnterTransitionDidFinish
    //呼叫新場景的onEnterTransitionDidFinish
    m_pInScene->onEnterTransitionDidFinish();
}

所以現在生命週期函式又執行了舊場景的onExit和新場景的onEnterTransitionDidFinish方法,

總結以上過程便是最初始舊場景的onEnter,然後舊場景的onExitTransitionDidStart,然後舊場景的onExitTransitionDidStart,然後新場景的onEnter,然後舊場景的onExit,最後是新場景的onEnterTransitionDidFinish。如果又有場景通過CCTransitionScene的子類進行跳轉的話又會重複以上的一個過程。

最後測試以上的結果:

首先是不通過CCTransitionScene來跳轉。

void HelloWorld::menuCloseCallback(CCObject* pSender)
{
    // "close" menu item clicked
    //CCDirector::sharedDirector()->end();
	//CCTransitionScene* t = CCTransitionFade::create(1.2f, TestScene::scene());
	CCDirector::sharedDirector()->replaceScene(TestScene::scene());
}
打印出來的結果是:

Old Scene: onEnter
Old Scene: onEnterTransitionDidFinish
replaceScene is use(replaceScene 方法被呼叫)
Old Scene: onExitTransitionDidStart
Old Scene: onExit
New Scene: onEnter
New Scene: onEnterTransitionDidFinish
New Scene: onExitTransitionDidStart
New Scene: onExit

其實是通過CCTransitionScene來跳轉。

void HelloWorld::menuCloseCallback(CCObject* pSender)
{
    // "close" menu item clicked
    //CCDirector::sharedDirector()->end();
	CCTransitionScene* t = CCTransitionFade::create(1.2f, TestScene::scene());
	CCDirector::sharedDirector()->replaceScene(t);
}
打印出來的結果是:

Old Scene: onEnter
Old Scene: onEnterTransitionDidFinish
replaceScene is use(replaceScene 方法被呼叫)
Old Scene: onExitTransitionDidStart
New Scene: onEnter
replaceScene is use(replaceScene 方法被呼叫)
Old Scene: onExit
New Scene: onEnterTransitionDidFinish
New Scene: onExitTransitionDidStart
New Scene: onExit

結果與分析無誤。