18、Cocos2dx 3.0游戏开发找小三之cocos2d-x,请问你是怎么调度的咩
生活随笔
收集整理的這篇文章主要介紹了
18、Cocos2dx 3.0游戏开发找小三之cocos2d-x,请问你是怎么调度的咩
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
重開發者的勞動成果,轉載的時候請務必注明出處:http://blog.csdn.net/haomengzhu/article/details/30478251
Cocos2d 的一大特色就是提供了事件驅動的游戲框架, 引擎會在合適的時候調用事件處理函數,我們只需要在函數中添加對各種游戲事件的處理, 就可以完成一個完整的游戲了。 例如,為了實現游戲的動態變化,Cocos2d 提供了兩種定時器事件; 為了響應用戶輸入,Cocos2d 提供了觸摸事件和傳感器事件; 此外,Cocos2d 還提供了一系列控制程序生命周期的事件。
Cocos2d 的調度原理管理著所有的事件,Cocos2d 已經為我們隱藏了游戲主循環的實現。
首先來看看游戲實現的原理: 游戲乃至圖形界面的本質是不斷地繪圖,然而繪圖并不是隨意的,任何游戲都需要遵循一定的規則來呈現出來,這些規則就體現為游戲邏輯。游戲邏輯會控制游戲內容,使其根據用戶輸入和時間流逝而改變。 因此,游戲可以抽象為不斷地重復以下動作: 處理用戶輸入 ; 處理定時事件 ; 繪圖 ;
游戲主循環就是這樣的一個循環,它會反復執行以上動作,保持游戲進行下去,直到玩家退出游戲。 在 Cocos2d-x 3.0 中,以上的動作包含在 Director 的某個方法之中,而引擎會根據不同的平臺設法使系統不斷地調用這個方法,從而完成了游戲主循環。
在cocos2d-x 3.0中,Director 包含一個管理引擎邏輯的方法,它就是 Director::mainLoop()方法, 這個方法負責調用定時器,繪圖,發送全局通知,并處理內存回收池。 該方法按幀調用,?每幀調用一次,而幀間間隔取決于兩個因素,一個是預設的幀率,默認為 60 幀每秒; 另一個是每幀的計算量大小。 當邏輯 處理與繪圖計算量過大時,設備無法完成每秒 60 次繪制,此時幀率就會降低。?
mainLoop()方法會被定時調用,然而在不同的平臺下它的調用者不同。 通常 Application 類負責處理平臺相關的任務,其中就包含了對 mainLoop()的調用; 不同的平臺具體實現也不相同,具體可參考cocos\2d\platform目錄;
mainLoop()方法是定義在 Director 中的抽象方法,它的實現位于同一個文件中的?DisplayLinkDirector類中; virtual void mainLoop() = 0; 具體實現是: void DisplayLinkDirector::mainLoop() { if (_purgeDirectorInNextLoop) { _purgeDirectorInNextLoop = false; purgeDirector(); } else if (! _invalid) { drawScene(); // release the objects //釋放資源對象 PoolManager::getInstance()->getCurrentPool()->clear(); } } 上述代碼主要包含如下 3 個步驟。 1、判斷是否需要釋放 Director,如果需要,則刪除 Director 占用的資源。通常,游戲結束時才會執行這個步驟。 2、調用 drawScene()方法,繪制當前場景并進行其他必要的處理。 3、彈出自動回收池,使得這一幀被放入自動回收池的對象全部釋放。
mainLoop()把內存管理以外的操作都交給了 drawScene()方法,因此關鍵的步驟都在 drawScene()方法之中; 再來看看drawScene方法: void Director::drawScene() { // calculate "global" dt //計算全局幀間時間差 dt calculateDeltaTime(); // skip one flame when _deltaTime equal to zero. if(_deltaTime < FLT_EPSILON) { return; } if (_openGLView) { _openGLView->pollInputEvents(); } //tick before glClear: issue #533 if (! _paused) { _scheduler->update(_deltaTime); _eventDispatcher->dispatchEvent(_eventAfterUpdate); } glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); /* 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) { setNextScene(); } kmGLPushMatrix(); // global identity matrix is needed... come on kazmath! kmMat4 identity; kmMat4Identity(&identity); // draw the scene //繪制場景 if (_runningScene) { _runningScene->visit(_renderer, identity, false); _eventDispatcher->dispatchEvent(_eventAfterVisit); } // draw the notifications node //處理通知節點 if (_notificationNode) { _notificationNode->visit(_renderer, identity, false); } if (_displayStats) { showStats(); } _renderer->render(); _eventDispatcher->dispatchEvent(_eventAfterDraw); kmGLPopMatrix(); _totalFrames++; // swap buffers //交換緩沖區 if (_openGLView) { _openGLView->swapBuffers(); } if (_displayStats) { calculateMPF(); } } 可以分析出: 在主循環中,我們主要進行了以下 3 個操作。 1、調用了定時調度器的 update 方法,引發定時器事件。 2、如果場景需要被切換,則調用 setNextStage 方法,在顯示場景前切換場景。 3、調用當前場景的 visit 方法,繪制當前場景。
在游戲主循環 drawScene 方法中,我們可以看到每一幀引擎都會調用?_scheduler的 update 方法。 【Scheduler *_scheduler;】_scheduler 是 Scheduler 類型的對象,是一個定時調度器。 所謂定時調度器,就是一個管理所有節點定時器的對象,? 它負責記錄定時器,并在合適的時間觸發定時事件。
再來分析一下定時器的情況: Cocos2d-x 提供了兩種定時器,分別是: update 定時器,每一幀都被觸發,使用 scheduleUpdate 方法來啟用; schedule 定時器,可以設置觸發的間隔,使用 schedule 方法來啟用。 看下Node中的實現: void Node::scheduleUpdateWithPriority(int priority) { _scheduler->scheduleUpdate(this, priority, !_running); } void Node::schedule(SEL_SCHEDULE selector, float interval, unsigned int repeat, float delay) { CCASSERT( selector, "Argument must be non-nil"); CCASSERT( interval >=0, "Argument must be positive"); _scheduler->schedule(selector, this, interval , repeat, delay, !_running); } 其中?_scheduler是 Scheduler 對象。 可以看到,這兩個方法的內部除去檢查參數是否合法,只是調用了 Scheduler提供的方法。 換句話說,Node 提供的定時器只是對 Scheduler 的包裝而已。 不僅這兩個方法如此,其他定時器相關的方法也都是這樣。
Scheduler的分析 經過上面的分析,我們已經知道 Node 提供的定時器不是由它本身而是由 Scheduler管理的。 因此,我們把注意力轉移到定時調度器上。 顯而易見,定時調度器應該對每一個節點維護一個定時器列表,在恰當的時候就會觸發其定時事件。
Scheduler的主要成員請查看:cocos\2d\CCScheduler.h
為了注冊一個定時器,開發者只要調用調度器提供的方法即可。 同時調度器還提供了一系列對定時器的控制接口,例如暫停和恢復定時器。 在調度器內部維護了多個容器,用于記錄每個節點注冊的定時器; 同時,調度器會接受其他組件(通常 與平臺相關)的定時調用,隨著系統時間的改變驅動調度器。?
調度器可以隨時增刪或修改被注冊的定時器。 具體來看,調度器將 update 定時器與普通定時器分別處理: 當某個節點注冊 update 定時器時,調度器就會把節點添加到 Updates 容器中, 即struct _hashUpdateEntry *_hashForUpdates里面; 為了提高調度器效率,Cocos2d-x 使用了散列表與鏈表結合的方式來保存定時器信息; 當某個節點注冊普通定時器時,調度器會把回調函數和其他信息保存到 Selectors 散列表中, 即struct _hashSelectorEntry *_hashForTimers里面。
在游戲主循環中,我們已經見到了 update 方法。 可以看到,游戲主循環會不停地調用 update 方法。 該方法包含一個實型參數,表示兩次調用的時間間隔。 在該方法中,引擎會利用兩次調用的間隔來計算何時觸發定時器。 我們再來分析下?update 方法的工作流程: // main loop void Scheduler::update(float dt) { _updateHashLocked = true; //a.預處理 if (_timeScale != 1.0f) { dt *= _timeScale; } // // Selector callbacks // // Iterate over all the Updates' selectors //b.枚舉所有的 update 定時器 tListEntry *entry, *tmp; // updates with priority < 0 //優先級小于 0 的定時器 DL_FOREACH_SAFE(_updatesNegList, entry, tmp) { if ((! entry->paused) && (! entry->markedForDeletion)) { entry->callback(dt); } } // updates with priority == 0 //優先級等于 0 的定時器 DL_FOREACH_SAFE(_updates0List, entry, tmp) { if ((! entry->paused) && (! entry->markedForDeletion)) { entry->callback(dt); } } // updates with priority > 0 //優先級大于 0 的定時器 DL_FOREACH_SAFE(_updatesPosList, entry, tmp) { if ((! entry->paused) && (! entry->markedForDeletion)) { entry->callback(dt); } } // Iterate over all the custom selectors //c.枚舉所有的普通定時器 for (tHashTimerEntry *elt = _hashForTimers; elt != nullptr; ) { _currentTarget = elt; _currentTargetSalvaged = false; if (! _currentTarget->paused) { // The 'timers' array may change while inside this loop //枚舉此節點中的所有定時器 //timers 數組可能在循環中改變,因此在此處需要小心處理 for (elt->timerIndex = 0; elt->timerIndex < elt->timers->num; ++(elt->timerIndex)) { elt->currentTimer = (Timer*)(elt->timers->arr[elt->timerIndex]); elt->currentTimerSalvaged = false; elt->currentTimer->update(dt); if (elt->currentTimerSalvaged) { // The currentTimer told the remove itself. To prevent the timer from // accidentally deallocating itself before finishing its step, we retained // it. Now that step is done, it's safe to release it. elt->currentTimer->release(); } elt->currentTimer = nullptr; } } // elt, at this moment, is still valid // so it is safe to ask this here (issue #490) elt = (tHashTimerEntry *)elt->hh.next; // only delete currentTarget if no actions were scheduled during the cycle (issue #481) if (_currentTargetSalvaged && _currentTarget->timers->num == 0) { removeHashElement(_currentTarget); } } // delete all updates that are marked for deletion // updates with priority < 0 //d.清理所有被標記了刪除記號的 update 方法 //優先級小于 0 的定時器 DL_FOREACH_SAFE(_updatesNegList, entry, tmp) { if (entry->markedForDeletion) { this->removeUpdateFromHash(entry); } } // updates with priority == 0 //優先級等于 0 的定時器 DL_FOREACH_SAFE(_updates0List, entry, tmp) { if (entry->markedForDeletion) { this->removeUpdateFromHash(entry); } } // updates with priority > 0 //優先級大于 0 的定時器 DL_FOREACH_SAFE(_updatesPosList, entry, tmp) { if (entry->markedForDeletion) { this->removeUpdateFromHash(entry); } } _updateHashLocked = false; _currentTarget = nullptr; #if CC_ENABLE_SCRIPT_BINDING // // Script callbacks // // Iterate over all the script callbacks //e.處理腳本引擎相關的事件 if (!_scriptHandlerEntries.empty()) { for (auto i = _scriptHandlerEntries.size() - 1; i >= 0; i--) { SchedulerScriptHandlerEntry* eachEntry = _scriptHandlerEntries.at(i); if (eachEntry->isMarkedForDeletion()) { _scriptHandlerEntries.erase(i); } else if (!eachEntry->isPaused()) { eachEntry->getTimer()->update(dt); } } } #endif // // Functions allocated from another thread // // Testing size is faster than locking / unlocking. // And almost never there will be functions scheduled to be called. if( !_functionsToPerform.empty() ) { _performMutex.lock(); // fixed #4123: Save the callback functions, they must be invoked after '_performMutex.unlock()', otherwise if new functions are added in callback, it will cause thread deadlock. auto temp = _functionsToPerform; _functionsToPerform.clear(); _performMutex.unlock(); for( const auto &function : temp ) { function(); } } } 借助注釋,能夠看出 update 方法的流程大致如下所示。 1、參數 dt 乘以一個縮放系數,以改變游戲全局的速度,其中縮放系數可以由 Scheduler的TimeScale屬性設置。 2、分別枚舉優先級小于 0、等于 0、大于 0 的 update 定時器。 如果定時器沒有暫停,也沒有被標記為即將刪除,則觸發定時器。 3、枚舉所有注冊過普通定時器的節點,再枚舉該節點的定時器,調用定時器的更新方法,從而決定是否觸發該定時器。 4、再次枚舉優先級小于 0、等于 0、大于 0 的 update 定時器,移除前幾個步驟中被標記了刪除記號的定時器。 我們暫不關心腳本引擎相關的處理。?
對于 update 定時器來說,每一節點只可能注冊一個定時器, 因此調度器中存儲定時器數據的結構體tListEntry *entry主要保存了注冊者與優先級。 對于普通定時器來說,每一個節點可以注冊多個定時器, 引擎使用回調函數(選擇器)來區分同一節點下注冊的不同定時器。 調度器為每一個定時器創建了一個 Timer 對象, 它記錄了定時器的目標、回調函數、觸發周期、重復觸發還是僅觸發一次等屬性。
Timer 也提供了 update 方法,它的名字和參數都與 Scheduler 的 update 方法一樣, 而且它們也都需要被定時調用。 不同的是,Timer?的 update 方法會把每一次調用時接收的時間間隔 dt 積累下來, 如果經歷的時間達到了周期就會引發定時器的定時事件。? 第一次引發了定時事件后,如果是僅觸發一次的定時器, 則 update 方法會中止,否則定時器會重新計時,從而反復地觸發定時事件。
來看看Timer的update方法: void Timer::update(float dt) { if (_elapsed == -1) { _elapsed = 0; _timesExecuted = 0; } else { if (_runForever && !_useDelay) {//standard timer usage _elapsed += dt; if (_elapsed >= _interval) { trigger(); _elapsed = 0; } } else {//advanced usage _elapsed += dt; if (_useDelay) { if( _elapsed >= _delay ) { trigger(); _elapsed = _elapsed - _delay; _timesExecuted += 1; _useDelay = false; } } else { if (_elapsed >= _interval) { trigger(); _elapsed = 0; _timesExecuted += 1; } } if (!_runForever && _timesExecuted > _repeat) { //unschedule timer cancel(); } } } }
再次回到 Scheduler 的 update 方法上來。 在步驟 c 中,程序首先枚舉了每一個注冊過定時器的對象,然后再枚舉對象中定時?器對應的 Timer 對象, 調用 Timer 對象的 update 方法來更新定時器狀態,以便觸發定時事件。
至此,我們可以看到事件驅動的普通定時器調用順序為: 系統的時間事件驅動游戲主循環,游戲主循環調用 Scheduler 的 update 方法,Scheduler 調用普通定時器對應的 Timer 對象的 update 方法,Timer 類的 update 方法調用定時器 對應的回調函數。
對于 update 定時器,調用順序更為簡單,因此前面僅列出了普通定時器的調用順序。? 同時,我們也可以看到,在定時器被觸發的時刻,Scheduler 類的 update 方法正在迭代之中, 開發者完全可能在定時器?事件中啟用或停止其他定時器。 不過,這么做會導致 update 方法中的迭代被破壞。 Cocos2d-x 的設計已經考慮到了這個問題,采用了一些技巧避免迭代被破壞。 例如,update 定時器被刪除時,不會直接刪除,而是標記為將要刪除,在定時器迭代完畢后再清理被標記的定時器,這樣即可保證迭代的正確性。
Cocos2d-x 的設計使得很多離散在各處的代碼通過事件聯系起來,在每一幀中起作用。 基于事件驅動的游戲框架易于掌握,使用靈活,而且所有事件串行地在同一線程中執行,不會出現線程同步的問題。
可以看到,Cocos2d-x是多么的強大!!! 小伙伴們,知道cococs2d-x是怎么調度了咩!! 咩!!
郝萌主友情提示: 多看看源碼,你就能更了解cocos2d-x了、、、
Cocos2d 的一大特色就是提供了事件驅動的游戲框架, 引擎會在合適的時候調用事件處理函數,我們只需要在函數中添加對各種游戲事件的處理, 就可以完成一個完整的游戲了。 例如,為了實現游戲的動態變化,Cocos2d 提供了兩種定時器事件; 為了響應用戶輸入,Cocos2d 提供了觸摸事件和傳感器事件; 此外,Cocos2d 還提供了一系列控制程序生命周期的事件。
Cocos2d 的調度原理管理著所有的事件,Cocos2d 已經為我們隱藏了游戲主循環的實現。
首先來看看游戲實現的原理: 游戲乃至圖形界面的本質是不斷地繪圖,然而繪圖并不是隨意的,任何游戲都需要遵循一定的規則來呈現出來,這些規則就體現為游戲邏輯。游戲邏輯會控制游戲內容,使其根據用戶輸入和時間流逝而改變。 因此,游戲可以抽象為不斷地重復以下動作: 處理用戶輸入 ; 處理定時事件 ; 繪圖 ;
游戲主循環就是這樣的一個循環,它會反復執行以上動作,保持游戲進行下去,直到玩家退出游戲。 在 Cocos2d-x 3.0 中,以上的動作包含在 Director 的某個方法之中,而引擎會根據不同的平臺設法使系統不斷地調用這個方法,從而完成了游戲主循環。
在cocos2d-x 3.0中,Director 包含一個管理引擎邏輯的方法,它就是 Director::mainLoop()方法, 這個方法負責調用定時器,繪圖,發送全局通知,并處理內存回收池。 該方法按幀調用,?每幀調用一次,而幀間間隔取決于兩個因素,一個是預設的幀率,默認為 60 幀每秒; 另一個是每幀的計算量大小。 當邏輯 處理與繪圖計算量過大時,設備無法完成每秒 60 次繪制,此時幀率就會降低。?
mainLoop()方法會被定時調用,然而在不同的平臺下它的調用者不同。 通常 Application 類負責處理平臺相關的任務,其中就包含了對 mainLoop()的調用; 不同的平臺具體實現也不相同,具體可參考cocos\2d\platform目錄;
mainLoop()方法是定義在 Director 中的抽象方法,它的實現位于同一個文件中的?DisplayLinkDirector類中; virtual void mainLoop() = 0; 具體實現是: void DisplayLinkDirector::mainLoop() { if (_purgeDirectorInNextLoop) { _purgeDirectorInNextLoop = false; purgeDirector(); } else if (! _invalid) { drawScene(); // release the objects //釋放資源對象 PoolManager::getInstance()->getCurrentPool()->clear(); } } 上述代碼主要包含如下 3 個步驟。 1、判斷是否需要釋放 Director,如果需要,則刪除 Director 占用的資源。通常,游戲結束時才會執行這個步驟。 2、調用 drawScene()方法,繪制當前場景并進行其他必要的處理。 3、彈出自動回收池,使得這一幀被放入自動回收池的對象全部釋放。
mainLoop()把內存管理以外的操作都交給了 drawScene()方法,因此關鍵的步驟都在 drawScene()方法之中; 再來看看drawScene方法: void Director::drawScene() { // calculate "global" dt //計算全局幀間時間差 dt calculateDeltaTime(); // skip one flame when _deltaTime equal to zero. if(_deltaTime < FLT_EPSILON) { return; } if (_openGLView) { _openGLView->pollInputEvents(); } //tick before glClear: issue #533 if (! _paused) { _scheduler->update(_deltaTime); _eventDispatcher->dispatchEvent(_eventAfterUpdate); } glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); /* 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) { setNextScene(); } kmGLPushMatrix(); // global identity matrix is needed... come on kazmath! kmMat4 identity; kmMat4Identity(&identity); // draw the scene //繪制場景 if (_runningScene) { _runningScene->visit(_renderer, identity, false); _eventDispatcher->dispatchEvent(_eventAfterVisit); } // draw the notifications node //處理通知節點 if (_notificationNode) { _notificationNode->visit(_renderer, identity, false); } if (_displayStats) { showStats(); } _renderer->render(); _eventDispatcher->dispatchEvent(_eventAfterDraw); kmGLPopMatrix(); _totalFrames++; // swap buffers //交換緩沖區 if (_openGLView) { _openGLView->swapBuffers(); } if (_displayStats) { calculateMPF(); } } 可以分析出: 在主循環中,我們主要進行了以下 3 個操作。 1、調用了定時調度器的 update 方法,引發定時器事件。 2、如果場景需要被切換,則調用 setNextStage 方法,在顯示場景前切換場景。 3、調用當前場景的 visit 方法,繪制當前場景。
在游戲主循環 drawScene 方法中,我們可以看到每一幀引擎都會調用?_scheduler的 update 方法。 【Scheduler *_scheduler;】_scheduler 是 Scheduler 類型的對象,是一個定時調度器。 所謂定時調度器,就是一個管理所有節點定時器的對象,? 它負責記錄定時器,并在合適的時間觸發定時事件。
再來分析一下定時器的情況: Cocos2d-x 提供了兩種定時器,分別是: update 定時器,每一幀都被觸發,使用 scheduleUpdate 方法來啟用; schedule 定時器,可以設置觸發的間隔,使用 schedule 方法來啟用。 看下Node中的實現: void Node::scheduleUpdateWithPriority(int priority) { _scheduler->scheduleUpdate(this, priority, !_running); } void Node::schedule(SEL_SCHEDULE selector, float interval, unsigned int repeat, float delay) { CCASSERT( selector, "Argument must be non-nil"); CCASSERT( interval >=0, "Argument must be positive"); _scheduler->schedule(selector, this, interval , repeat, delay, !_running); } 其中?_scheduler是 Scheduler 對象。 可以看到,這兩個方法的內部除去檢查參數是否合法,只是調用了 Scheduler提供的方法。 換句話說,Node 提供的定時器只是對 Scheduler 的包裝而已。 不僅這兩個方法如此,其他定時器相關的方法也都是這樣。
Scheduler的分析 經過上面的分析,我們已經知道 Node 提供的定時器不是由它本身而是由 Scheduler管理的。 因此,我們把注意力轉移到定時調度器上。 顯而易見,定時調度器應該對每一個節點維護一個定時器列表,在恰當的時候就會觸發其定時事件。
Scheduler的主要成員請查看:cocos\2d\CCScheduler.h
為了注冊一個定時器,開發者只要調用調度器提供的方法即可。 同時調度器還提供了一系列對定時器的控制接口,例如暫停和恢復定時器。 在調度器內部維護了多個容器,用于記錄每個節點注冊的定時器; 同時,調度器會接受其他組件(通常 與平臺相關)的定時調用,隨著系統時間的改變驅動調度器。?
調度器可以隨時增刪或修改被注冊的定時器。 具體來看,調度器將 update 定時器與普通定時器分別處理: 當某個節點注冊 update 定時器時,調度器就會把節點添加到 Updates 容器中, 即struct _hashUpdateEntry *_hashForUpdates里面; 為了提高調度器效率,Cocos2d-x 使用了散列表與鏈表結合的方式來保存定時器信息; 當某個節點注冊普通定時器時,調度器會把回調函數和其他信息保存到 Selectors 散列表中, 即struct _hashSelectorEntry *_hashForTimers里面。
在游戲主循環中,我們已經見到了 update 方法。 可以看到,游戲主循環會不停地調用 update 方法。 該方法包含一個實型參數,表示兩次調用的時間間隔。 在該方法中,引擎會利用兩次調用的間隔來計算何時觸發定時器。 我們再來分析下?update 方法的工作流程: // main loop void Scheduler::update(float dt) { _updateHashLocked = true; //a.預處理 if (_timeScale != 1.0f) { dt *= _timeScale; } // // Selector callbacks // // Iterate over all the Updates' selectors //b.枚舉所有的 update 定時器 tListEntry *entry, *tmp; // updates with priority < 0 //優先級小于 0 的定時器 DL_FOREACH_SAFE(_updatesNegList, entry, tmp) { if ((! entry->paused) && (! entry->markedForDeletion)) { entry->callback(dt); } } // updates with priority == 0 //優先級等于 0 的定時器 DL_FOREACH_SAFE(_updates0List, entry, tmp) { if ((! entry->paused) && (! entry->markedForDeletion)) { entry->callback(dt); } } // updates with priority > 0 //優先級大于 0 的定時器 DL_FOREACH_SAFE(_updatesPosList, entry, tmp) { if ((! entry->paused) && (! entry->markedForDeletion)) { entry->callback(dt); } } // Iterate over all the custom selectors //c.枚舉所有的普通定時器 for (tHashTimerEntry *elt = _hashForTimers; elt != nullptr; ) { _currentTarget = elt; _currentTargetSalvaged = false; if (! _currentTarget->paused) { // The 'timers' array may change while inside this loop //枚舉此節點中的所有定時器 //timers 數組可能在循環中改變,因此在此處需要小心處理 for (elt->timerIndex = 0; elt->timerIndex < elt->timers->num; ++(elt->timerIndex)) { elt->currentTimer = (Timer*)(elt->timers->arr[elt->timerIndex]); elt->currentTimerSalvaged = false; elt->currentTimer->update(dt); if (elt->currentTimerSalvaged) { // The currentTimer told the remove itself. To prevent the timer from // accidentally deallocating itself before finishing its step, we retained // it. Now that step is done, it's safe to release it. elt->currentTimer->release(); } elt->currentTimer = nullptr; } } // elt, at this moment, is still valid // so it is safe to ask this here (issue #490) elt = (tHashTimerEntry *)elt->hh.next; // only delete currentTarget if no actions were scheduled during the cycle (issue #481) if (_currentTargetSalvaged && _currentTarget->timers->num == 0) { removeHashElement(_currentTarget); } } // delete all updates that are marked for deletion // updates with priority < 0 //d.清理所有被標記了刪除記號的 update 方法 //優先級小于 0 的定時器 DL_FOREACH_SAFE(_updatesNegList, entry, tmp) { if (entry->markedForDeletion) { this->removeUpdateFromHash(entry); } } // updates with priority == 0 //優先級等于 0 的定時器 DL_FOREACH_SAFE(_updates0List, entry, tmp) { if (entry->markedForDeletion) { this->removeUpdateFromHash(entry); } } // updates with priority > 0 //優先級大于 0 的定時器 DL_FOREACH_SAFE(_updatesPosList, entry, tmp) { if (entry->markedForDeletion) { this->removeUpdateFromHash(entry); } } _updateHashLocked = false; _currentTarget = nullptr; #if CC_ENABLE_SCRIPT_BINDING // // Script callbacks // // Iterate over all the script callbacks //e.處理腳本引擎相關的事件 if (!_scriptHandlerEntries.empty()) { for (auto i = _scriptHandlerEntries.size() - 1; i >= 0; i--) { SchedulerScriptHandlerEntry* eachEntry = _scriptHandlerEntries.at(i); if (eachEntry->isMarkedForDeletion()) { _scriptHandlerEntries.erase(i); } else if (!eachEntry->isPaused()) { eachEntry->getTimer()->update(dt); } } } #endif // // Functions allocated from another thread // // Testing size is faster than locking / unlocking. // And almost never there will be functions scheduled to be called. if( !_functionsToPerform.empty() ) { _performMutex.lock(); // fixed #4123: Save the callback functions, they must be invoked after '_performMutex.unlock()', otherwise if new functions are added in callback, it will cause thread deadlock. auto temp = _functionsToPerform; _functionsToPerform.clear(); _performMutex.unlock(); for( const auto &function : temp ) { function(); } } } 借助注釋,能夠看出 update 方法的流程大致如下所示。 1、參數 dt 乘以一個縮放系數,以改變游戲全局的速度,其中縮放系數可以由 Scheduler的TimeScale屬性設置。 2、分別枚舉優先級小于 0、等于 0、大于 0 的 update 定時器。 如果定時器沒有暫停,也沒有被標記為即將刪除,則觸發定時器。 3、枚舉所有注冊過普通定時器的節點,再枚舉該節點的定時器,調用定時器的更新方法,從而決定是否觸發該定時器。 4、再次枚舉優先級小于 0、等于 0、大于 0 的 update 定時器,移除前幾個步驟中被標記了刪除記號的定時器。 我們暫不關心腳本引擎相關的處理。?
對于 update 定時器來說,每一節點只可能注冊一個定時器, 因此調度器中存儲定時器數據的結構體tListEntry *entry主要保存了注冊者與優先級。 對于普通定時器來說,每一個節點可以注冊多個定時器, 引擎使用回調函數(選擇器)來區分同一節點下注冊的不同定時器。 調度器為每一個定時器創建了一個 Timer 對象, 它記錄了定時器的目標、回調函數、觸發周期、重復觸發還是僅觸發一次等屬性。
Timer 也提供了 update 方法,它的名字和參數都與 Scheduler 的 update 方法一樣, 而且它們也都需要被定時調用。 不同的是,Timer?的 update 方法會把每一次調用時接收的時間間隔 dt 積累下來, 如果經歷的時間達到了周期就會引發定時器的定時事件。? 第一次引發了定時事件后,如果是僅觸發一次的定時器, 則 update 方法會中止,否則定時器會重新計時,從而反復地觸發定時事件。
來看看Timer的update方法: void Timer::update(float dt) { if (_elapsed == -1) { _elapsed = 0; _timesExecuted = 0; } else { if (_runForever && !_useDelay) {//standard timer usage _elapsed += dt; if (_elapsed >= _interval) { trigger(); _elapsed = 0; } } else {//advanced usage _elapsed += dt; if (_useDelay) { if( _elapsed >= _delay ) { trigger(); _elapsed = _elapsed - _delay; _timesExecuted += 1; _useDelay = false; } } else { if (_elapsed >= _interval) { trigger(); _elapsed = 0; _timesExecuted += 1; } } if (!_runForever && _timesExecuted > _repeat) { //unschedule timer cancel(); } } } }
再次回到 Scheduler 的 update 方法上來。 在步驟 c 中,程序首先枚舉了每一個注冊過定時器的對象,然后再枚舉對象中定時?器對應的 Timer 對象, 調用 Timer 對象的 update 方法來更新定時器狀態,以便觸發定時事件。
至此,我們可以看到事件驅動的普通定時器調用順序為: 系統的時間事件驅動游戲主循環,游戲主循環調用 Scheduler 的 update 方法,Scheduler 調用普通定時器對應的 Timer 對象的 update 方法,Timer 類的 update 方法調用定時器 對應的回調函數。
對于 update 定時器,調用順序更為簡單,因此前面僅列出了普通定時器的調用順序。? 同時,我們也可以看到,在定時器被觸發的時刻,Scheduler 類的 update 方法正在迭代之中, 開發者完全可能在定時器?事件中啟用或停止其他定時器。 不過,這么做會導致 update 方法中的迭代被破壞。 Cocos2d-x 的設計已經考慮到了這個問題,采用了一些技巧避免迭代被破壞。 例如,update 定時器被刪除時,不會直接刪除,而是標記為將要刪除,在定時器迭代完畢后再清理被標記的定時器,這樣即可保證迭代的正確性。
Cocos2d-x 的設計使得很多離散在各處的代碼通過事件聯系起來,在每一幀中起作用。 基于事件驅動的游戲框架易于掌握,使用靈活,而且所有事件串行地在同一線程中執行,不會出現線程同步的問題。
可以看到,Cocos2d-x是多么的強大!!! 小伙伴們,知道cococs2d-x是怎么調度了咩!! 咩!!
郝萌主友情提示: 多看看源碼,你就能更了解cocos2d-x了、、、
轉載于:https://blog.51cto.com/haomengzhu/1664404
總結
以上是生活随笔為你收集整理的18、Cocos2dx 3.0游戏开发找小三之cocos2d-x,请问你是怎么调度的咩的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 李嘉诚无锡演讲:骂到你成功
- 下一篇: 数据中心产品化的蜕变