[原创]cocos2d-x研习录-第三阶 特性之调度器

时间:2025-03-21 00:04:43

在游戏中,经常会周期执行一些检测、操作或更新一些数据等,我们称之为调度。Cocos2D-x中将调度封装为类CCScheduler,方便在游戏开发中使用。我们一起来学习一下,CCScheduler具有哪些调度功能。

[原创]cocos2d-x研习录-第三阶 特性之调度器
   从上图可知,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
   }
   CCScheduler对象原来是在CCDirector的初始化过程中创建的,CCNode引用了该对象。这样,只要是CCNode继承类都能引用到CCScheduler对象。
   update定时器调度其实是调用CCScheduler的update方法实现的。这个方法又是在哪里调用的呢?看代码,如下:
   void CCDirector::drawScene(void)
   {
       // calculate "global" dt
       calculateDeltaTime();
       //tick before glClear: issue #533
       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;
        }
        ...
    }
    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); //调用待调度对象的调度方法
        }
    }
    // 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); //调用待调度对象的调度方法          
        }
    }
    ...
   }
   这段代码比较长,读起来有点累,为大家考虑我将次要代码省略了,直奔主题。原来待调度对象的调度方法也是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)。