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(m_pRunningScene) != NULL;
	//这里newIsTransition的为true,因为即将要运行的场景是通过CCTransitionScene包含进来的
    bool newIsTransition = dynamic_cast(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(m_pRunningScene) != NULL;
	//这里newIsTransition的为false,因为m_pNextScene已经被重置为目标场景了,而不是被CCTransitionScene所包含的场景
    bool newIsTransition = dynamic_cast(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


结果与分析无误。

你可能感兴趣的:(cocos2d-x)