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

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

直接看TransitionScene的定义

class CC_DLL TransitionScene : public Scene
{
public:
/** Orientation Type used by some transitions
*/
enum class Orientation
{
/// An horizontal orientation where the Left is nearer
LEFT_OVER = 0,
/// An horizontal orientation where the Right is nearer
RIGHT_OVER = 1,
/// A vertical orientation where the Up is nearer
UP_OVER = 0,
/// A vertical orientation where the Bottom is nearer
DOWN_OVER = 1,
}; /** creates a base transition with duration and incoming scene */
static TransitionScene * create(float t, Scene *scene); /** called after the transition finishes */
void finish(void); /** used by some transitions to hide the outer scene */
void hideOutShowIn(void); //
// Overrides
//
virtual void draw(Renderer *renderer, const kmMat4 &transform, bool transformUpdated) override;
virtual void onEnter() override;
virtual void onExit() override;
virtual void cleanup() override; CC_CONSTRUCTOR_ACCESS:
TransitionScene();
virtual ~TransitionScene(); /** initializes a transition with duration and incoming scene */
bool initWithDuration(float t,Scene* scene); protected:
virtual void sceneOrder();
void setNewScene(float dt); Scene *_inScene;
Scene *_outScene;
float _duration;
bool _isInSceneOnTop;
bool _isSendCleanupToScene; private:
CC_DISALLOW_COPY_AND_ASSIGN(TransitionScene);
};

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

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

TransitionScene 类一共同拥有五个成员变量这五个变量从变量命名上就已经能猜得差点儿相同了。

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

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

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

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

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

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

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

TransitionScene * TransitionScene::create(float t, Scene *scene)
{
TransitionScene * pScene = new TransitionScene();
if(pScene && pScene->initWithDuration(t,scene))
{
pScene->autorelease();
return pScene;
}
CC_SAFE_DELETE(pScene);
return nullptr;
}

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

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

bool TransitionScene::initWithDuration(float t, Scene *scene)
{
CCASSERT( scene != nullptr, "Argument scene must be non-nil"); if (Scene::init())
{
_duration = t; // retain
_inScene = scene;
_inScene->retain();
_outScene = Director::getInstance()->getRunningScene();
if (_outScene == nullptr)
{
_outScene = Scene::create();
}
_outScene->retain(); CCASSERT( _inScene != _outScene, "Incoming scene must be different from the outgoing scene" ); sceneOrder(); return true;
}
else
{
return false;
}
}

从这个函数的參数来推断,这个初始化函数的作用是,通过指定场景切换的持续时间与要切换的场景指针来初始化一个场景切换中介场景(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 这是一个虚函数,里面的过程非常easy,仅仅是设置了_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方法,大家一看就能知道是场景切换完毕时调用的方法

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

值得注意的是最后将一个回调函数增加到了定时调度器里 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  切换场景的过程。

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

通过对 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  这样的变化。

前五行代码非常easy,就是我创建了一个新的场景我们叫场景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相关源代码做分析。

上一篇:TAP/TUN浅析(一)


下一篇:eclipseIDE for javaee developers 开发环境搭建详解图文