程序運行後每達到一幀的時間間隔就會執行一次mainLoop
void CCDisplayLinkDirector::mainLoop(void)
{
//判斷是否需要釋放CCDirector,通常遊戲結束纔會執行這個步驟
if (m_bPurgeDirecotorInNextLoop)
{
m_bPurgeDirecotorInNextLoop = false;
purgeDirector();
}
else if (! m_bInvalid)
{
//繪製當前場景並執行其他必要的處理
drawScene();
//彈出自動回收池,使這一幀被放入回收池的對象全部執行release
CCPoolManager::sharedPoolManager()->pop();
}
}
那麼程序的關鍵步奏就在這裏在drawScene裏面了
void CCDirector::drawScene(void)
{
// 計算全局幀間時間差
calculateDeltaTime();
//1. 引發定時器事件
if (! m_bPaused)
{
m_pScheduler->update(m_fDeltaTime);
}
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
//2. 是否切換場景
if (m_pNextScene)
{
setNextScene();
}
kmGLPushMatrix();
// 3. 繪製當前場景
if (m_pRunningScene)
{
m_pRunningScene->visit();
}
// draw the notifications node處理通知節點
if (m_pNotificationNode)
{
m_pNotificationNode->visit();
}
if (m_bDisplayStats)
{
showStats();
}
kmGLPopMatrix();
m_uTotalFrames++;
// swap buffers
if (m_pobOpenGLView)
{
m_pobOpenGLView->swapBuffers();
}
if (m_bDisplayStats)
{
calculateMPF();
}
}
那麼可以看出,在遊戲的每一幀,都會調用CCScheduler的update來調度定時器;然後遍歷渲染樹,對遊戲進行繪製。
調度器CCScheduler
在遊戲中要顯示的元素都繼承於CCNode類,當繼承於CCNode的節點調用schedule()添加一個定時器時,CCNode通過導演->getScheduler()獲得定時器CCScheduler對象,然後將定時器交給該CCScheduler對象管理。
再來看CCScheduler內,定時器主要分爲Update定時器 和 普通interval定時器。如下CCScheduler 中的主要存儲變量。(爲提高調度器效率,使用了鏈表 和 散列表保存定時器信息。)
//Update定時器
struct _listEntry *m_pUpdatesNegList; // list of priority < 0
struct _listEntry *m_pUpdates0List; // list priority == 0
struct _listEntry *m_pUpdatesPosList; // list priority > 0
struct _hashUpdateEntry *m_pHashForUpdates; // hash used to fetch quickly the list entries for pause,delete,etc
// 普通interval定時器
struct _hashSelectorEntry *m_pHashForTimers;
在主循環的drawScene函數中調用了CCScheduler::update,下面來分析這個函數:
void CCScheduler::update(float dt)
{
m_bUpdateHashLocked = true; //$
//1. 時間差*縮放係數 一改變遊戲全局速度,可通過CCScheduler的TimeScale屬性設置
if (m_fTimeScale != 1.0f)
{
dt *= m_fTimeScale;
}
//2. 分別枚舉優先級小於0、等於0、大於0的update定時器。如果定時器沒有暫停也沒有“標記爲刪除”,則觸發定時器。
// Iterate over all the Updates' selectors
tListEntry *pEntry, *pTmp;
// updates with priority < 0
DL_FOREACH_SAFE(m_pUpdatesNegList, pEntry, pTmp)
{
if ((! pEntry->paused) && (! pEntry->markedForDeletion))
{
pEntry->target->update(dt);
}
}
// updates with priority == 0
DL_FOREACH_SAFE(m_pUpdates0List, pEntry, pTmp)
{
if ((! pEntry->paused) && (! pEntry->markedForDeletion))
{
pEntry->target->update(dt);
}
}
// updates with priority > 0
DL_FOREACH_SAFE(m_pUpdatesPosList, pEntry, pTmp)
{
if ((! pEntry->paused) && (! pEntry->markedForDeletion))
{
pEntry->target->update(dt);
}
}
//3. 1枚舉所有註冊過的普通interval定時器節點;2在枚舉該節點的定時器,調用定時器的更新方法,從而決定是否觸發該定時器
// Iterate over all the custom selectors
for (tHashTimerEntry *elt = m_pHashForTimers; elt != NULL; )
{
m_pCurrentTarget = elt;
m_bCurrentTargetSalvaged = false;
if (! m_pCurrentTarget->paused)
{
// The 'timers' array may change while inside this loop
for (elt->timerIndex = 0; elt->timerIndex < elt->timers->num; ++(elt->timerIndex))
{
elt->currentTimer = (CCTimer*)(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 = NULL;
}
}
// 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 (m_bCurrentTargetSalvaged && m_pCurrentTarget->timers->num == 0)
{
removeHashElement(m_pCurrentTarget);
}
}
// 4. 處理腳本引擎相關事件
// Iterate over all the script callbacks
if (m_pScriptHandlerEntries)
{
for (int i = m_pScriptHandlerEntries->count() - 1; i >= 0; i--)
{
CCSchedulerScriptHandlerEntry* pEntry = static_cast<CCSchedulerScriptHandlerEntry*>(m_pScriptHandlerEntries->objectAtIndex(i));
if (pEntry->isMarkedForDeletion())
{
m_pScriptHandlerEntries->removeObjectAtIndex(i);
}
else if (!pEntry->isPaused())
{
pEntry->getTimer()->update(dt);
}
}
}
// 5. 再次枚舉Update定時器,刪除前面被“標記爲刪除”的定時器
// delete all updates that are marked for deletion
// updates with priority < 0
DL_FOREACH_SAFE(m_pUpdatesNegList, pEntry, pTmp)
{
if (pEntry->markedForDeletion)
{
this->removeUpdateFromHash(pEntry);
}
}
// updates with priority == 0
DL_FOREACH_SAFE(m_pUpdates0List, pEntry, pTmp)
{
if (pEntry->markedForDeletion)
{
this->removeUpdateFromHash(pEntry);
}
}
// updates with priority > 0
DL_FOREACH_SAFE(m_pUpdatesPosList, pEntry, pTmp)
{
if (pEntry->markedForDeletion)
{
this->removeUpdateFromHash(pEntry);
}
}
m_bUpdateHashLocked = false; //$
m_pCurrentTarget = NULL;
}
對於Update定時器,每個節點只能註冊一個定時器,因此調度器中存儲定時器數據的結構體主要保存了註冊節點和優先級。每一幀通過迭代調用鏈表中節點的Update函數來實現Update定時器。
對於普通interval定時器,每個節點能註冊多個定時器,引擎使用回調函數(選擇器)來區分同一個節點的不同定時器。調度器爲每一個定時器創建了一個CCTimer對象,它記錄了定時器的目標、回調函數、觸發週期、重複觸發等屬性。
程序首先枚舉了每個註冊了定時器的對象,然後再枚舉對象中定時器對應的CCTimer對象,調用CCTimer對象的update方法來更新定時器的狀態,以便觸發定時器事件。(在CCTimer的update方法中會把每一次調用時接受的時間間隔dt積累下來,如果經歷的時間達到一次定時觸發週期,就會觸發對應節點的定時器事件(回調函數)。如果是一次的定時器,update就會終止,否者會重新計時,從而反覆觸發定時事件)
//注:$ 、“標記爲刪除”: Update定時器三個鏈表正在迭代過程中,開發者完全可能在一個定時器事件中停用另一個定時器,如果立刻停用,這樣會導致Update方法的迭代破壞。所以當定時器在迭代時(m_bUpdateHashLocked = true),刪除一個節點的Update定時器不會立刻刪除,而是“標記爲刪除”,在迭代完成後第5步再來清理被標記了的定時器,這樣就保證了迭代的正確性。
對於普通interval定時器,通過update方法獲知currentTimerSalvaged爲true時,就會執行release,所以在迭代過程中CCTimer數組會改變,需要小心處理。
前些天做一個項目的時候,註冊的一個調度器沒能執行,後來發現是該節點沒有添加到場景中,在if ((! pEntry->paused) && (! pEntry->markedForDeletion))時將會爲false。
那麼要爲一個不加入場景的節點(如:全局網絡派發器)添加調度器,就需要自己調用它的以下兩個函數:
onEnter();
onEnterTransitionDidFinish();
這樣,該節點的調度器就不會被暫停了。
至此可知,指定定時器後均由定時調度器控制,每個定時器互不干擾,串行執行。