18、Cocos2dx 3.0游戏开发找小三之cocos2d-x,请问你是怎么调度的咩

发布时间:2020-06-28 16:52:04 作者:danielzzu
来源:网络 阅读:323
重开发者的劳动成果,转载的时候请务必注明出处: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 方法正在迭代之中,
开发者完全可能在定时器 事件中启用或停止其他定时器。
18、Cocos2dx 3.0游戏开发找小三之cocos2d-x,请问你是怎么调度的咩
18、Cocos2dx 3.0游戏开发找小三之cocos2d-x,请问你是怎么调度的咩
不过,这么做会导致 update 方法中的迭代被破坏。
Cocos2d-x 的设计已经考虑到了这个问题,采用了一些技巧避免迭代被破坏。
例如,update 定时器被删除时,不会直接删除,而是标记为将要删除,在定时器迭代完毕后再清理被标记的定时器,这样即可保证迭代的正确性。

Cocos2d-x 的设计使得很多离散在各处的代码通过事件联系起来,在每一帧中起作用。
基于事件驱动的游戏框架易于掌握,使用灵活,而且所有事件串行地在同一线程中执行,不会出现线程同步的问题。

可以看到,Cocos2d-x是多么的强大!!!
小伙伴们,知道cococs2d-x是怎么调度了咩!!
咩!!

郝萌主友情提示:
多看看源码,你就能更了解cocos2d-x了、、、

推荐阅读:
  1. cocos2dx中的核心类是什么
  2. cocos2dx Scene 生命周期(replaceScene,popScene后续补充)

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

游戏开发 scheduler 调度

上一篇:Redis有什么特点

下一篇:python开始编辑的方法

相关阅读

您好,登录后才能下订单哦!

密码登录
登录注册
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》