在游戏中,经常会周期执行一些检测、操作或更新一些数据等,我们称之为调度。Cocos2D-x中将调度封装为类CCScheduler,方便在游戏开发中使用。我们一起来学习一下,CCScheduler具有哪些调度功能。
从上图可知,CCScheduler直接从基类CCObject继承而来。CCScheduler的调度模式分为两种:一种是update定时器调度,它是按照优先级来进行调度,在Cocos2D-x中优先级分为三类(大于零、小于零和等于零),值越小优先级越高。另一种是按照自定义定时器调度。这种调度方式需要设定一个或多个回调函数,当定时器计时到达时,调用回调函数。
CCScheduler的内部结构和功能如下:
·CCScheduler属性:
//用于update定时器调度
float m_fTimeScale:时间刻度(默认1.0f),作用是调整计时器的快慢
struct _listEntry *m_pUpdatesNegList:优先级小于0的链表,保存调度对象
struct _listEntry *m_pUpdates0List:优先级等于0的链表,保存调度对象
struct _listEntry *m_pUpdatesPosList: 优先级大于0的链表,保存调度对象
struct _hashUpdateEntry *m_pHashForUpdates:该哈希表不用于调度,只用于快速查找调度对象所在的链表
//用于自定义定时器调度
struct _hashSelectorEntry *m_pHashForSelectors:该链表用于自定义定时器调度对象容器
struct _hashSelectorEntry *m_pCurrentTarget:该链表用于处理当前调度对象
bool m_bCurrentTargetSalvaged:当前调度对象状态标志
bool m_bUpdateHashLocked:调度对象更新标志
CCArray* m_pScriptHandlerEntries:脚本调度
·CCScheduler方法:
void update(float dt):调度函数
void scheduleSelector(SEL_SCHEDULE pfnSelector, CCObject *pTarget, float fInterval, bool bPaused, unsigned int repeat, float delay):自定义调度选择器(回调函数)
void scheduleSelector(SEL_SCHEDULE pfnSelector, CCObject *pTarget, float fInterval, bool bPaused):自定义调度选择器(自定义调度选择器(回调函数))
void pauseTarget(CCObject *pTarget):暂停调度
void resumeTarget(CCObject *pTarget):恢复调度
以上就是CCScheduler的主要属性和方法。CCScheduler内部处理机制涉及一些编程技巧(如使用哈希表快速查找调度对象),有兴趣专研的同学也许可以从中提升编程技能。由于我们重点关注CCScheduler的功能和使用,所以不详细讲解它的处理机制。
下面就具体讲解一下两种调试模式的使用和流程:
·update定时器调度:
如果对前面介绍的概念类比较熟悉的同学,一定发现在CCDirector和CCNode的属性中都有一个CCScheduler指针。
我们看一下CCScheduler对象的创建过程:
bool CCDirector::init(void) //CCDirector初始化函数
{
{
...
m_pScheduler = new CCScheduler();//这里创建CCScheduler实例
...
return true;
}
}
CCNode::CCNode(void) //CCNode构造函数
{
// set default scheduler and actionManager
CCDirector *director = CCDirector::sharedDirector();
m_pActionManager = director->getActionManager();
m_pActionManager->retain();
m_pScheduler = director->getScheduler(); //引用在CCDirector中创建的CCScheduler实例
m_pScheduler->retain(); //CCScheduler引用数加1
}
CCDirector *director = CCDirector::sharedDirector();
m_pActionManager = director->getActionManager();
m_pActionManager->retain();
m_pScheduler = director->getScheduler(); //引用在CCDirector中创建的CCScheduler实例
m_pScheduler->retain(); //CCScheduler引用数加1
}
CCScheduler对象原来是在CCDirector的初始化过程中创建的,CCNode引用了该对象。这样,只要是CCNode继承类都能引用到CCScheduler对象。
update定时器调度其实是调用CCScheduler的update方法实现的。这个方法又是在哪里调用的呢?看代码,如下:
void CCDirector::drawScene(void)
{
// calculate "global" dt
calculateDeltaTime();
{
// calculate "global" dt
calculateDeltaTime();
//tick before glClear: issue #533
if (! m_bPaused)
{
m_pScheduler->update(m_fDeltaTime);
}
if (! m_bPaused)
{
m_pScheduler->update(m_fDeltaTime);
}
...
}
它是在CCDirector绘制场景时调用的。有人可能继续追问,drawScene方法又是在哪里调用的。当然是在游戏的主循环里了。
int CCApplication::run()
{
...
{
...
//主消息循环
while (1)
{
if (! PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{
QueryPerformanceCounter(&nNow);
if (nNow.QuadPart - nLast.QuadPart > m_nAnimationInterval.QuadPart)
{
nLast.QuadPart = nNow.QuadPart;
CCDirector::sharedDirector()->mainLoop();//游戏主循环
}
else
{
Sleep(0);
}
continue;
}
while (1)
{
if (! PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{
QueryPerformanceCounter(&nNow);
if (nNow.QuadPart - nLast.QuadPart > m_nAnimationInterval.QuadPart)
{
nLast.QuadPart = nNow.QuadPart;
CCDirector::sharedDirector()->mainLoop();//游戏主循环
}
else
{
Sleep(0);
}
continue;
}
...
}
}
return (int) msg.wParam;
}
}
如果有人还问我CCApplication的run又是在哪里调用,我只能无语了,建议回过头再看一看介绍Cocos2D-x基本框架这一节。
言归正传,现在CCScheduler对象有了,update也可以周期调用了,万事俱备就等着添加想要调度对象。
那如何添加呢?不用担心,在CCNode里已经给大家准备好了。如下:
void scheduleUpdate(void)://调度优先级默认为0
void scheduleUpdateWithPriority(int priority):设置优先级可设置
void CCNode::unscheduleUpdate():从调度对象中取消调度自己
所以,被调度对象想要使用CCScheduler调度就很简单了。首先,它需要继承自CCNode,然后就是调用scheduleUpdate或scheduleUpdateWithPriority添加自己到CCScheduler调度中。
这样就完了吗?当然不是,还有一个最重要的问题。被调度对象如何调用自己的调度方法呢?因为说白了,调度最终的目的就是周期执行本对象的调度方法,才能达到调度的目的。
这个问题在CCScheduler的update方法里已经给出了答案,我们还是一起去看一下吧。
void CCScheduler::update(float dt)
{
...
{
...
// updates with priority < 0
DL_FOREACH_SAFE(m_pUpdatesNegList, pEntry, pTmp)
{
if ((! pEntry->paused) && (! pEntry->markedForDeletion))
{
pEntry->target->update(dt); //调用待调度对象的调度方法
}
}
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); //调用待调度对象的调度方法
}
}
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); //调用待调度对象的调度方法
}
}
DL_FOREACH_SAFE(m_pUpdatesPosList, pEntry, pTmp)
{
if ((! pEntry->paused) && (! pEntry->markedForDeletion))
{
pEntry->target->update(dt); //调用待调度对象的调度方法
}
}
...
}
}
这段代码比较长,读起来有点累,为大家考虑我将次要代码省略了,直奔主题。原来待调度对象的调度方法也是update。追溯一下这个update方法的来源,发现原来在基类CCObject里就已经有定义了。这就怪不得CCScheduler对象和待调度对象都有update方法。想一想,忽然豁然开朗,整个CCScheduler的调度机制其实很简单,它就是利用所有被调度对象和它都有一个共同的基类CCObject以及update方法。所以,我推测这个update方法放在CCObject里定义,可能主要目的就是为了调度。
·自定义定时器调度:
有了update定时器调度,为什么还需要自定义定时器调度呢?我想原因是update定时器调度只支持单一化调度。举个例子,如果在游戏中有一个节点对象,需要每隔2秒检测一下操作,每隔3秒更新一下后台数据,每隔5秒添加一些道具,这个时候使用update定时器调度就只能完成其中一项调度,而自定义定时器调度支持多定时器调度,可以很轻松和优雅的搞定这一切。
下面看一看它是如何实现的。在介绍CCScheduler的属性和方法时,有如下两个方法:
void scheduleSelector(SEL_SCHEDULE pfnSelector, CCObject *pTarget, float fInterval, bool bPaused, unsigned int repeat, float delay):自定义调度选择器(回调函数)
void scheduleSelector(SEL_SCHEDULE pfnSelector, CCObject *pTarget, float fInterval, bool bPaused):自定义调度选择器(自定义调度选择器(回调函数))
这两个方法专门用于自定义定时器调度注册函数,它的作用就是添加任意个待调度对象和方法。
可能上面函数中的参数SEL_SCHEDULE pfnSelector看起来有点怪异,这只是一个函数指针类型重定义,如下:
typedef void (CCObject::*SEL_SCHEDULE)(float);
定义这个函数指针用于指向回调函数,而回调函数在调度定时器间隔时间到达时会被调用。
值得注意的是,使用调度函数每添加一个自定义定时器调度,将会对应产生一个定时器。
自定义定时器调度同样是调用CCScheduler的update方法。现在纵使update方法的另一部分代码给出,如下:
void CCScheduler::update(float dt)
{...// Interate all over the custom selectors
for (tHashSelectorEntry *elt = m_pHashForSelectors; elt != NULL; )
{
m_pCurrentTarget = elt;
m_bCurrentTargetSalvaged = false;if (! m_pCurrentTarget->paused)
{//遍历所有调度的定时器
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)
{
elt->currentTimer->release();
}elt->currentTimer = NULL;
}
}
}...}//定时器更新函数void CCTimer::update(float dt)
{
if (m_fElapsed == -1)
{
m_fElapsed = 0;
m_nTimesExecuted = 0;
}
else
{
if (m_bRunForever && !m_bUseDelay)
{
m_fElapsed += dt; //定时器计时
if (m_fElapsed >= m_fInterval) //判断定时器间隔时间
{
if (m_pTarget && m_pfnSelector)
{
(m_pTarget->*m_pfnSelector)(m_fElapsed);//调用回调函数
}
m_fElapsed = 0;//定时器计时清零...
}
}...}这段代码的关键点已经注释了,应该很容易明白,就不累述。CCScheduler的自定义调度器使用非常方便,如下:用户自定义UserClass类,继承于CCNode节点类,然后在其初始化函数onEnter中添加:schedule(schedule_selector(UserClass::callbackSchedulerFunc1), 10.5f);
schedule(schedule_selector(UserClass::callbackSchedulerFunc2), 10.5f);UserClass类中分别定义callbackSchedulerFunc1和callbackSchedulerFunc2回调函数,然后在函数实现代码中添加调度代码即可。
Cocos2D-x封装的CCScheduler调度就介绍到这里,还有更为高级的调度方式(如脚本调度)和使用方法各位有兴趣可以自己研究一下。在TestCpp工程中SchedulerTest测试项有调度的各种使用实例代码,可以参考学习一下。
-------------------------------------------------------------------------------------------------------------------
注:本人在本博客的原创文章采用创作共用版权协议(http://creativecommons.org/licenses/by-nc-sa/2.5/cn/), 要求署名、非商业用途和保持一致。要求署名包含注明我的网名及文章来源(我的博客地址:http://www.cnblogs.com/binbingg)。