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),程式中都會有儲存兩個場景的記憶體開銷,因為切換函式都是不會先銷燬上一個場景,再建立新場景的。
通過上面的註釋可以很清楚的看到在切換場景時引擎所做的事情,也解釋了為什麼跳轉時推薦使用replaceScene(因為replaceScene會release掉被替換的場景,但這裡的release還不會觸發引擎去呼叫被替換場景的解構函式,主要是在切換場景函式中setNextScene中也有個retian,所以真正觸發是在setNextScene的release中,這樣做便能保證被替換的場景的生命週期函式可以完整的被執行,也就是onExitTransitionDidStart和onExit方法)。但上面的那個問題依舊還是會存在(如果兩個場景都佔有非常大的記憶體空間的話,這個問題便會十分嚴重),所以比較好的做法便是建立一個過渡場景,雖然也是個場景,但極低的記憶體資源,因為它與遊戲的內容無關,只是用來過渡而已,這時便要配合場景的生命週期來實現。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) } }
具體的思路如下:
正在執行中的場景標記為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
結果與分析無誤。