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相關源碼做分析。

 

 

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM