Cocos2d-X3.0 刨根问底(九)----- 场景切换(TransitionScene)源码分析

上一章我们分析了Scene与Layer相关类的源码,对Cocos2d-x的场景有了初步了解,这章我们来分析一下场景变换TransitionScene源码。

直接看TransitionScene的定义

 1 class CC_DLL TransitionScene : public Scene
 2 {
 3 public:
 4     /** Orientation Type used by some transitions
 5      */
 6     enum class Orientation
 7     {
 8         /// An horizontal orientation where the Left is nearer
 9         LEFT_OVER = 0,
10         /// An horizontal orientation where the Right is nearer
11         RIGHT_OVER = 1,
12         /// A vertical orientation where the Up is nearer
13         UP_OVER = 0,
14         /// A vertical orientation where the Bottom is nearer
15         DOWN_OVER = 1,
16     };
17     
18     /** creates a base transition with duration and incoming scene */
19     static TransitionScene * create(float t, Scene *scene);
20 
21     /** called after the transition finishes */
22     void finish(void);
23 
24     /** used by some transitions to hide the outer scene */
25     void hideOutShowIn(void);
26 
27     //
28     // Overrides
29     //
30     virtual void draw(Renderer *renderer, const kmMat4 &transform, bool transformUpdated) override;
31     virtual void onEnter() override;
32     virtual void onExit() override;
33     virtual void cleanup() override;
34     
35 CC_CONSTRUCTOR_ACCESS:
36     TransitionScene();
37     virtual ~TransitionScene();
38 
39     /** initializes a transition with duration and incoming scene */
40     bool initWithDuration(float t,Scene* scene);
41     
42 protected:
43     virtual void sceneOrder();
44     void setNewScene(float dt);
45 
46     Scene *_inScene;
47     Scene *_outScene;
48     float _duration;
49     bool _isInSceneOnTop;
50     bool _isSendCleanupToScene;
51 
52 private:
53     CC_DISALLOW_COPY_AND_ASSIGN(TransitionScene);
54 };

这个类并不大,从类的头信息继承关系上可以看出场景切换的类其实也是一个场景。

老套路,先从成员变量开始分析。

TransitionScene 类一共有五个成员变量这五个变量从变量命名上就已经能猜得差不多了。

1     Scene *_inScene;            // 场景切换 切入的场景指针
2     Scene *_outScene;           // 场景切换 切出的场景指针 
3     float _duration;            // 场景切换 消耗的时间 
4     bool _isInSceneOnTop;       // 场景切换 描述切入场景与切出场景的渲染层次关系,true 切入场景在切出场景的顶层 
5     bool _isSendCleanupToScene; // 场景切换 标记是否已经给切出场景发送了清理的命令。 

到这里,可以猜出TransitionScene 类到底是以一个什么样的过程来实现 场景切换的,

TransitionScene 是一个中介场景,它左手拿着要切入的场景(_inScene),右手拿着要切出的场景(_outScene),让这两个入出场景在自己身上以一种华丽的方式完成切场的过程,最后这个中介场景退出,把舞台交给新切入的场景。

 

上面我们猜测了一下TransitionScene实现场景切换的原理,我们便有了好奇,TransitionScene类实现切入场景与切出场景的具体过程是什么呢?我们猜测的切换原理是否正确呢?

 

带着问题我们继续看源码来找答案。

 

我们先从TransitionScene的创建方法开始。

 

TransitionScene的构造函数是个空函数忽略,我们看一下TransitionScene::Create这个静态方法开始分析。

 1 TransitionScene * TransitionScene::create(float t, Scene *scene)
 2 {
 3     TransitionScene * pScene = new TransitionScene();
 4     if(pScene && pScene->initWithDuration(t,scene))
 5     {
 6         pScene->autorelease();
 7         return pScene;
 8     }
 9     CC_SAFE_DELETE(pScene);
10     return nullptr;
11 }

看到create函数的结构 熟悉的不能再熟悉了,在Cocos2d-x基本年有Node类及子类的创建都是这个结构。

下面我们看一下initWithDuration这个初始化方法。

 1 bool TransitionScene::initWithDuration(float t, Scene *scene)
 2 {
 3     CCASSERT( scene != nullptr, "Argument scene must be non-nil");
 4 
 5     if (Scene::init()) 
 6     {
 7         _duration = t;
 8 
 9         // retain
10         _inScene = scene;
11         _inScene->retain();
12         _outScene = Director::getInstance()->getRunningScene();
13         if (_outScene == nullptr)
14         {
15             _outScene = Scene::create();
16         }
17         _outScene->retain();
18 
19         CCASSERT( _inScene != _outScene, "Incoming scene must be different from the outgoing scene" );
20         
21         sceneOrder();
22 
23         return true;
24     }
25     else
26     {
27         return false;
28     }
29 }

从这个函数的参数来判断,这个初始化函数的作用是,通过指定场景切换的持续时间与要切换的场景指针来初始化一个场景切换中介场景(TransitionScene)的实例。

TransitionScene实例初始过程

  1. 调用基类Scene初始化方法。Scene::init
  2. 将传入的持续时间参数进行赋值 _duration = t;
  3. 切入场景赋值 _inScene = scene;注意这里增加了一次对切入场景的引用,因为在中介场景引用了一次切入场景,所以增加了一次引用计数。
  4. 切出场景赋值 这里的切出场景当然就是指的当前Director类正在运行的场景 通过 Director::getInstance()->getRunningScene(); 得到当前正在运行的场景赋值给_outScene还有一个判断,如果没有正在运行的场景那么会创建一个空的场景做为切出场景,并且增加了一次对切也场景的引用记数。
  5. 调用了sceneOrder()函数,从这个函数的命名上来看是对场景进行了一次排序,具体都干了些啥事呢?下在对sceneOrder进行分析。
void TransitionScene::sceneOrder()
{
    _isInSceneOnTop = true;
}

sceneOrder 这是一个虚函数,里面的过程很简单,只是设置了_isInSceneOnTop这个标记,来指定切入场景与切出场景的层次关系,就是谁在谁的上面。

TransitionScene 的实例创建我们分析完了,下面来寻找场景切换的过程是怎么样实现的。

看一下TransitionScene 类头文件发现了以下几个函数。

    virtual void draw(Renderer *renderer, const kmMat4 &transform, bool transformUpdated) override;
    virtual void onEnter() override;
    virtual void onExit() override;
    virtual void cleanup() override;

从命名上初步分析这四个函数的作用分别是

draw 渲染方式

onEnter 中介场景进入舞台时调用

onExit 中介场景退出舞台时调用

clearup 清理场景方法。

 

一个一个分析先看onEnter

 

void TransitionScene::onEnter()
{
    Scene::onEnter();
    
    // disable events while transitions
    _eventDispatcher->setEnabled(false);
    
    // outScene should not receive the onEnter callback
    // only the onExitTransitionDidStart
    _outScene->onExitTransitionDidStart();
    
    _inScene->onEnter();
}

函数过程分析:

  1. 调用Scene基类的onEnter
  2. 将事件分发器设置为不可用状态,场景切换是一个动画过程,在这个过程中不处理事件,以免发生意想不到的错误。
  3. 调用切出场景的onExitTransitionDidStart方法。看过以前章节的朋友应该还有印象,这个方法是在Node基类里面定义的,函数意义为场景变化切出开始时的回调方法。中介场景进入舞台当然是当前场景离开的时候在这里调用这个方法合情合理
  4. 最后调用了切入场景的onEnter ,一点问题都没有,切出场景开始离开,切入场景进入舞台,这正是这个中介场景在左右手交换的过程。

再看onExit方法。

void TransitionScene::onExit()
{
    Scene::onExit();
    
    // enable events while transitions
    _eventDispatcher->setEnabled(true);
    _outScene->onExit();

    // _inScene should not receive the onEnter callback
    // only the onEnterTransitionDidFinish
    _inScene->onEnterTransitionDidFinish();
}

函数过程分析:

  1. 调用基类Scene的onExit方法。
  2. 恢复了事件分发器的可用状态。
  3. 调用了切出场景的离开回调
  4. 调用了切入场景的进入完成回调。

我们onEnter和onExit这两个方法来比较着看。

通过分析,我们可以了解切入切出场景 进入舞台到离开舞台的函数调用顺序,在这里小结一下。

场景进入舞台 ---->离开舞台。 (这里说的舞台可以理解成就是屏幕上可以显示的区域)

onEnter(进入舞台) ---> onEnterTransitionDidFinish(进入完成) ---> onExitTransitionDidStart(开始离开舞台) ---> onExit(离开)

了解了场景的进入舞台函数调用顺序,我们就可以理解中介场景的onExit与onEnter这两个函数都是干了些什么。

中介场景进入舞台的时候正是切出场景开始离开舞台与切入场景进入舞台的时候

中介场景离开始了舞台,切入场景完成了进入舞台,切出场景离开了舞台。

就在这个时候中介场景与切出场景都离开始了舞台,在舞台上就留下了切入的场景,至此完成了场景的切换。

接下来我们看一下另外两个虚函数

void TransitionScene::draw(Renderer *renderer, const kmMat4 &transform, bool transformUpdated)
{
    Scene::draw(renderer, transform, transformUpdated);

    if( _isInSceneOnTop ) {
        _outScene->visit(renderer, transform, transformUpdated);
        _inScene->visit(renderer, transform, transformUpdated);
    } else {
        _inScene->visit(renderer, transform, transformUpdated);
        _outScene->visit(renderer, transform, transformUpdated);
    }
}

draw方法没什么难度,就是根据_isInSceneOnTop这里记录的切入切出两个场景层次关系,来做分别的渲染。

void TransitionScene::cleanup()
{
    Scene::cleanup();

    if( _isSendCleanupToScene )
        _outScene->cleanup();
}

clearup方法也是先调用其它的clearup然后根据切出场景是否已经清除过_isSendCleanupToScene这个标记来对切出场景进行清理操作。

下面我们看一下TransitionScene还有哪些方法

void TransitionScene::finish()
{
    // clean up
    _inScene->setVisible(true);
    _inScene->setPosition(Point(0,0));
    _inScene->setScale(1.0f);
    _inScene->setRotation(0.0f);
    _inScene->setAdditionalTransform(nullptr);

    _outScene->setVisible(false);
    _outScene->setPosition(Point(0,0));
    _outScene->setScale(1.0f);
    _outScene->setRotation(0.0f);
    _outScene->setAdditionalTransform(nullptr);

    //[self schedule:@selector(setNewScene:) interval:0];
    this->schedule(schedule_selector(TransitionScene::setNewScene), 0);
}

这个finish方法,大家一看就能知道是场景切换完成时调用的方法

里面的函数过程也很简单,设置切入 场景为显示状态,切出场景不显示,并且将场景的旋转、缩放、位置都恢复成初始状态。

值得注意的是最后将一个回调函数加入到了定时调度器里 TransitionScene::setNewScene 这个回调间隔时间为0也就是在下一帧就会被调用。

下面看一下这个setNewScene这个函数是干什么的。

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

    this->unschedule(schedule_selector(TransitionScene::setNewScene));
    
    // Before replacing, save the "send cleanup to scene"
    Director *director = Director::getInstance();
    _isSendCleanupToScene = director->isSendCleanupToScene();
    
    director->replaceScene(_inScene);
    
    // issue #267
    _outScene->setVisible(true);
}

这个函数有一个参数但并没有使用,可能是预留的。

这个函数里面取到了Director类是清除场景的标记,并且赋值给了_isSendCleanupToScene这个变量。

注意这一行代码 director->replaceScene(_inScene); 

我们知道,Director管理着场景,同时只能有一个runningScene,这行代码实际上是真正的把舞台通过Director交给了我们要切入的场景(_inScene)

void TransitionScene::hideOutShowIn()
{
    _inScene->setVisible(true);
    _outScene->setVisible(false);
}

函数hideOutShowIn隐藏切出场景显示切入场景,没什么可多说的。

 

分析到这里,所有TransitionScene类里的方法我们都分析过了,可能大家有一些疑问

  1. 只分析到了一些场景切入舞台与场景离开舞台的回调,而没有看到那些动态的场景切换的过程。
  2. 中介场景是如何进入舞台的?如何使用中介场景来做场景切换呢?
  3. finish函数并没有被调用,那么这个finish好像名存实亡。

带着疑问,我们继续在源码中寻找答案。

 

我们看CCTransition.h这个头文件里面除了TransitionScene还定义了好多TransitionScene类的子类,下面我们选择一个类来分析一下

/** @brief TransitionRotoZoom:
Rotate and zoom out the outgoing scene, and then rotate and zoom in the incoming 
*/
class CC_DLL TransitionRotoZoom : public TransitionScene
{
public:
    static TransitionRotoZoom* create(float t, Scene* scene);

    //
    // Overrides
    //
    virtual void onEnter() override;

protected:
    TransitionRotoZoom();
    virtual ~TransitionRotoZoom();

private:
    CC_DISALLOW_COPY_AND_ASSIGN(TransitionRotoZoom);

};

看一下这个类的注释,切出场景缩放旋转着移出,切入场景旋转缩放着移入。

TransitionRotoZoom 类继承 TransitionScene类 并没有增加什么属性与方法,而是重载了onEnter方法,那么onEnter里面有什么变化呢?

跟进代码:

void TransitionRotoZoom:: onEnter()
{
    TransitionScene::onEnter();

    _inScene->setScale(0.001f);
    _outScene->setScale(1.0f);

    _inScene->setAnchorPoint(Point(0.5f, 0.5f));
    _outScene->setAnchorPoint(Point(0.5f, 0.5f));

    ActionInterval *rotozoom = (ActionInterval*)(Sequence::create
    (
        Spawn::create
        (
            ScaleBy::create(_duration/2, 0.001f),
            RotateBy::create(_duration/2, 360 * 2),
            nullptr
        ),
        DelayTime::create(_duration/2),
        nullptr
    ));

    _outScene->runAction(rotozoom);
    _inScene->runAction
    (
        Sequence::create
        (
            rotozoom->reverse(),
            CallFunc::create(CC_CALLBACK_0(TransitionScene::finish,this)),
            nullptr
        )
    );
}

看到了onEnter实现,是有这么点意思了,里面有提到动画,延时……. 开始分析。

onEnter函数过程:

  1. 调用基类的onEnter这里面连接了切入切出场景的onEnter与onExit相关的场景过程回调。
  2. 设置切入场景缩小到0.001倍大小  切出场景为原大小。
  3. 将切入,切出场景的锚点设置在场景的中心部位。
  4. 这里出现了一个新的类ActionInterval  从命名上可得知是一个动画的类,的这里设置了两个变化过程,一个是缩放上面的变化,一个是旋转上面的变化。切出场景运行了这个动作过程。
  5. 切入场景反运行了上面定义的动作过程rotozoom->reverse()
  6. 在创建动画序列对象时定义了一个回调函数CallFunc::create(CC_CALLBACK_0(TransitionScene::finish,this)),  哈哈,这里我们看到了 TransitionScene::finish 这个函数 在这里出现完全可以理解, 在切入场景动画播放完成的时候调用了中介场景的finish方法。上面讲过在finish方法里面最终通过director的replaceScene方法来将切入场景加入到了舞台。

注意:这里用了ActionInterval  等动画相关的类,在这里我们只要知道这些动画类的大概作用就可以了,后面的章节我们一个一个地分析。

小鱼写了一个Demo给大家展示一下 TransitionRotoZoom  切换场景的过程。

通过对 TransitionScene 类的派生类 TransitionRotoZoom 的分析,我们了解了TransitionScene类的动作过程,总结如下:

  1. TransitionScene 类是场景切换的基类,Cocos2d-x的场景效果都继承 这个类。但这个类没有任何切换场景的效果,只提供了onEnter onExit等几个函数接口,在子类里面重载这些接口函数来实现特定的场景切换效果。
  2. TransitionScene  系列类是一个场景的中介类,在这个类里会相继调用 切入切出场景的onEnter与onExit。
  3. 如果你自己定义场景切换的效果,不要忘记在效果结束后调用 TransitionScene::finish 方法.

现在上面的三个疑问基本都找到答案了。

这里还有一点可能有些读者会产生疑问。

小鱼在这里一起和大家再分析一下场景切换与Director类及引擎的关联。

先说一下TransitionScene  类的使用方法。

看以下代码。

    auto visibleSize = Director::getInstance()->getVisibleSize();
    Scene *myScene = Scene::create();
    Sprite * sp = Sprite::create("mv2.jpg");
    sp->setPosition( visibleSize.width / 2, visibleSize.height / 2 );
    myScene->addChild( sp );
    TransitionRotoZoom *tranScene = TransitionRotoZoom::create( 2.0, myScene );
    Director::getInstance()->replaceScene( tranScene );

 

这个例子是我上面gif图的代码片断,主要用到了 TransitionRotoZoom  这种变化。

前五行代码很简单,就是我创建了一个新的场景我们叫场景2,将一个图片放到了场景2上面。

第六行代码,我创建了一个TransitionRotoZoom场景切换对象实例,并且设置切换时间为2秒,将场景2传入做为切出场景。

第七行代码 ,找到Director实例对象,用场景2来替换当前场景。

就这几行代码就实现了上面的效果。

一句话,在使用场景切换的时候就是将场景变换的中介场景加入到舞台上。上面我们已经分析了中介场景,在结束的时候会真正的将切入场景replace到舞台上面。

 

为了加深理解,我们以上面的实例代码为例子在这里再分析几断Director相关函数的代码。

我们先看replaceScene都干了些什么。

void Director::replaceScene(Scene *scene) // 这里传入的scene就是我们的中介场景 myScene 
{
    CCASSERT(_runningScene, "Use runWithScene: instead to start the director");
    CCASSERT(scene != nullptr, "the scene should not be null");
    
    if (_nextScene)// 这里判断如果已经指定了下一个要切的场景那就在这里面就把下个场景释放掉。
    {
        if (_nextScene->isRunning())
        {
            _nextScene->onExitTransitionDidStart();
            _nextScene->onExit();
        }
        _nextScene->cleanup();
        _nextScene = nullptr;
    }
    // 这里将myScene放到场景栈顶,并且重新修改了_nextScene为 myScene
    ssize_t index = _scenesStack.size();

    _sendCleanupToScene = true;
    _scenesStack.replace(index - 1, scene);

    _nextScene = scene;
} // 函数结束后现在Director里面的_nextScene就是我们的myScene。 在director每一帧的主循环里我们找一下对_nextScene的处理。

我们再回顾了下Director 的mainLoop

void DisplayLinkDirector::mainLoop()
{
    if (_purgeDirectorInNextLoop)
    {
        _purgeDirectorInNextLoop = false;
        purgeDirector();
    }
    else if (! _invalid)
    {
        drawScene();// 其它的不用看,我们再跟进drawScene找找对 _nextScene的处理。     
        // release the objects
        PoolManager::getInstance()->getCurrentPool()->clear();
    }
}

下面是drawScene的代码片断

void Director::drawScene()
{    …………
    // 上面省略     /* 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 (_nextScene)// 在drawScene里面有对_nextScene处理,这时_nextScene就是我们上面会话的myScene
    {
        setNextScene();
    }

    kmGLPushMatrix();     ………… 
    // 下面省略
}

跟进setNextScene方法。

void Director::setNextScene()
{// 下面两个转换大家要注意一下,此时的_runningScene是我们的场景1 就是一个普通的scene 所以在做dynamic_cast 转换时为null      bool runningIsTransition = dynamic_cast<TransitionScene*>(_runningScene) != nullptr;    //这里runningIsTransition == false     bool newIsTransition = dynamic_cast<TransitionScene*>(_nextScene) != nullptr;           //这里newIsTransition = true

    // If it is not a transition, call onExit/cleanup
     if (! newIsTransition) // 因为_nextScene是 transiton类型所以这个if判断里面不会走进来 
     {
         if (_runningScene)
         {
             _runningScene->onExitTransitionDidStart();
             _runningScene->onExit();
         }
 
         // issue #709. the root node (scene) should receive the cleanup message too
         // otherwise it might be leaked.
         if (_sendCleanupToScene && _runningScene)
         {
             _runningScene->cleanup();
         }
     }

    if (_runningScene) // 的这里释放了_runningScene的引用计数,因为_runningScene要被_nextScene替代
    {
        _runningScene->release();
    }
    _runningScene = _nextScene; // _nextScene增加了一次引用计数
    _nextScene->retain();
    _nextScene = nullptr;

    if ((! runningIsTransition) && _runningScene) // 这个判断会进入,大家如果没弄明白仔细想想 虽然现在的_runningScene是Transition类型但我们取得 runningIsTransition值的时候是还没有将_nextScene进行替换。
    {
        _runningScene->onEnter(); // 终于找到了,在这里调用了中介场景 myScene的 onEnter 然后就有一系列的 旋转、缩放变化。这就是我们前面分析TransitionScene 这个类的过程联系起来了。直到myScene的finish方法将场景2换到舞台上。
        _runningScene->onEnterTransitionDidFinish();
    }
}

好啦,今天的内容基本就这些了。

通过上面的分析我们了 Cocos2d-x的场景变化是怎么做的。这里我只给了大家一个实例,如果想多了解Cocos2d-x都准备了哪些场景变化给我们,不妨把CCTransition.h里面的类做成Demo都试一下。

今天我们在分析中碰到了与 Action有关的类,现在我们只知道它与动画变化有关系,那么Action系列类究竟都是些什么玩意呢?

好,搞定它,我们下章就对Action相关源码做分析。

 

 

 

 

 

posted @ 2014-07-10 18:13  小鱼老师  Views(3612)  Comments(5Edit  收藏  举报