程序运行后每达到一帧的时间间隔就会执行一次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,pTmp) { if ((! pEntry->paused) && (! pEntry->markedForDeletion)) { pEntry->target->update(dt); } } // updates with priority > 0 DL_FOREACH_SAFE(m_pUpdatesPosList,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,pTmp) { if (pEntry->markedForDeletion) { this->removeUpdateFromHash(pEntry); } } // updates with priority == 0 DL_FOREACH_SAFE(m_pUpdates0List,pTmp) { if (pEntry->markedForDeletion) { this->removeUpdateFromHash(pEntry); } } // updates with priority > 0 DL_FOREACH_SAFE(m_pUpdatesPosList,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();
这样,该节点的调度器就不会被暂停了。
至此可知,指定定时器后均由定时调度器控制,每个定时器互不干扰,串行执行。