文章目录
缘起
由于我是从unity转做的cocos2dx,在unity中仅提供了三个update方法:fixupdate、update和laterupdate,分别有不同的调用时序,但在cocos2dx中除了普通的update以外,还提供了schedule这样一个骚操作,一直对其耿耿于怀,今天抽出空来测试了一下。
主要思考的问题
与unity中的update相同的update方法非常容易理解,它主要解决所有node节点的更新操作,而开启和关闭update则通过ScheduleUpdate和UnscheduleUpdate进行;
schedule方法提供一个方法的传入接口,并根据指定的时间间隔和延迟进行调用,这个调用方式本身也是非常容易理解的;
所以问题的重点来了:
- schedule方法是通过什么方式调用的呢?
- schedule方法有一个interval参数,可以设置间隔,并且它可以设置为一个比一帧时间(默认是0.017秒)更短的时间,它真的是在这个间隔里调用的吗?它是怎么实现这个调用的?
- update方法和schedule是通过同样的机制实现的吗?
源码研究
不得不说,开源项目最根本的特点就是可以查看源码,因此我先看了一遍源码的调用。
1.底层主循环结构——Scheduler的update
首先要说的是底层的实现,所有游戏程序员都应该对游戏主循环有一个基本的了解,它是构成一个引擎的主要驱动,而cocos2dx的主循环管理放在Scheduler类中,在这个类中维护了三个需要调用指定更新方法的对象列表,一个由外部驱动的update方法,添加外部schedule的一批schedule方法这几个核心运行方法,以及一些其他的针对整个Scheduler的辅助方法,例如timescale、pause、resume等。
void update(float dt);
struct _listEntry *_updatesNegList; // list of priority < 0
struct _listEntry *_updates0List; // list priority == 0
struct _listEntry *_updatesPosList; // list priority > 0
struct _hashSelectorEntry *_hashForTimers;
struct _hashSelectorEntry *_currentTarget;
循环的核心由以上几个构建,一部分实现基础的update功能,另一部分实现灵活的schedule功能。
在Scheduler类之外,必须注意的是一系列跟scheduler高度耦合的Timer类,这些Timer类都是特定作用的容器,用于初始化各种类型的schedule,通过这一系列的Timer类实现了Scheduler与外部的解耦和内部的维护。
有句话是这么说的——计算机软件领域的一切问题都可以通过一个中间件来解决。
cocos2dx也是这么做的。
2.组件update的实现
前面提到过,scheduler由外部调用update来实现驱动,而这个驱动方在不同平台是存在差异的,在ios/mac上由objectC驱动,而其他平台则不是,不过这并不是重点。
从代码上看,scheduler类是维护了两套数据,其中一套用于普通的update方法更新,另一套用于灵活的schedule功能实现。
而它的核心update方法内容如下:
这一段是cocos2dx的源码,不想看代码可以直接跳到下一段文字。
// main loop
void Scheduler::update(float dt)
{
_updateHashLocked = true;
if (_timeScale != 1.0f)
{
dt *= _timeScale;
}
//
// Selector callbacks
//
// Iterate over all the Updates' selectors
tListEntry *entry, *tmp;
// updates with priority < 0
DL_FOREACH_SAFE(_updatesNegList, entry, tmp)
{
if ((! entry->paused) && (! entry->markedForDeletion))
{
entry->callback(dt);
}
}
// updates with priority == 0
DL_FOREACH_SAFE(_updates0List, entry, tmp)
{
if ((! entry->paused) && (! entry->markedForDeletion))
{
entry->callback(dt);
}
}
// updates with priority > 0
DL_FOREACH_SAFE(_updatesPosList, entry, tmp)
{
if ((! entry->paused) && (! entry->markedForDeletion))
{
entry->callback(dt);
}
}
// Iterate over all the custom selectors
for (tHashTimerEntry *elt = _hashForTimers; elt != nullptr; )
{
_currentTarget = elt;
_currentTargetSalvaged = false;
if (! _currentTarget->paused)
{
// The 'timers' array may change while inside this loop
for (elt->timerIndex = 0; elt->timerIndex < elt->timers->num; ++(elt->timerIndex))
{
elt->currentTimer = (Timer*)(elt->timers->arr[elt->timerIndex]);
CCASSERT
( !elt->currentTimer->isAborted(),
"An aborted timer should not be updated" );
elt->currentTimer->update(dt);
if (elt->currentTimer->isAborted())
{
// 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 removed in update
for (auto &e : _updateDeleteVector)
delete e;
_updateDeleteVector.clear();
_updateHashLocked = false;
_currentTarget = nullptr;
#if CC_ENABLE_SCRIPT_BINDING
//
// Script callbacks
//
// Iterate over all the script callbacks
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 = std::move(_functionsToPerform);
_performMutex.unlock();
for (const auto &function : temp) {
function();
}
}
}
具体逻辑其实就是简单地对所有的Timer文件进行了一次遍历运行,需要注意的是运行顺序,在Scheduler的update中,会先运行通过ScheduleUpdate方法来调用的update,然后再运行其他schedule的update——重点来了:并没有发现专门开辟的运行低于一帧时间的运行,并且从Timer类来看,运行的时候是对interval进行了一个判断,如果发现当前总运行时间少于需要运行的时间,那么就会以interval为单位进行循环调用并累加,直到时间超过需要运行的时间,而这就意味着,这些方法的调用是没有在真实的指定的时间内调用的!这仅仅是一个模拟操作罢了。
代码测试
那么是不是真的这样?有没有可能是我理解错了代码呢?
写代码永远指向功能实现,只能来测试一下了。
1.验证方案
设计三个运行,一个在update中,另外两个在schedule中,其中一个interval大于一帧时间(0.01s),另一个小于一帧时间(0.03s),查看DEBUG频率,并且通过timeval获取调用方法时的机器时间
2.验证代码
使用默认的HelloWorld类,添加三个方法,分别用于三个不同的运行。
void HelloWorld::update(float dt)
{
timeval t ;
gettimeofday(&t, NULL);
auto timer = t.tv_sec*1000 + t.tv_usec/1000;
CCLOG("\n--update dt: %f--",dt);
CCLOG("update: %ld",timer);
}
void HelloWorld::Schedule_1(float dt)
{
timeval t ;
gettimeofday(&t, NULL);
auto timer = t.tv_sec*1000 + t.tv_usec/1000;
CCLOG("--Schedule_1 dt: %f--",dt);
CCLOG("Schedule_1: %ld",timer);
}
void HelloWorld::Schedule_3(float dt)
{
timeval t ;
gettimeofday(&t, NULL);
auto timer = t.tv_sec*1000 + t.tv_usec/1000;
CCLOG("--Schedule_3 dt: %f--",dt);
CCLOG("Schedule_3: %ld",timer);
}
在HelloWorld的Init中添加以下调用代码:
scheduleUpdate();
schedule(CC_SCHEDULE_SELECTOR(HelloWorld::Schedule_1), 0.01f, CC_REPEAT_FOREVER, 0);//interval = 0.01f
schedule(CC_SCHEDULE_SELECTOR(HelloWorld::Schedule_3), 0.03f, CC_REPEAT_FOREVER, 0);//interval = 0.03f
3.运行结果
{
gl.supports_OES_map_buffer: false
gl.supports_vertex_array_object: true
cocos2d.x.version: cocos2d-x-3.17
gl.vendor: Intel Inc.
gl.supports_PVRTC: false
gl.renderer: Intel Iris OpenGL Engine
cocos2d.x.compiled_with_profiler: false
cocos2d.x.build_type: DEBUG
cocos2d.x.compiled_with_gl_state_cache: true
gl.max_texture_size: 16384
gl.supports_ETC1: false
gl.supports_BGRA8888: false
gl.max_texture_units: 16
gl.supports_OES_packed_depth_stencil: false
gl.supports_ATITC: false
gl.supports_discard_framebuffer: false
gl.supports_NPOT: true
gl.version: 2.1 INTEL-12.8.38
gl.supports_S3TC: true
gl.supports_OES_depth24: false
}
libpng warning: iCCP: known incorrect sRGB profile
cocos2d: QuadCommand: resizing index size from [-1] to [2560]
--update dt: 0.016667--
update: 1556597853692
--update dt: 0.016470--
update: 1556597853708
--Schedule_1 dt: 0.010000--
Schedule_1: 1556597853708
--update dt: 0.015498--
update: 1556597853724
--Schedule_1 dt: 0.010000--
Schedule_1: 1556597853724
--Schedule_1 dt: 0.010000--
Schedule_1: 1556597853724
--Schedule_3 dt: 0.030000--
Schedule_3: 1556597853724
--update dt: 0.016254--
update: 1556597853740
--Schedule_1 dt: 0.010000--
Schedule_1: 1556597853740
--update dt: 0.017143--
update: 1556597853757
--Schedule_1 dt: 0.010000--
Schedule_1: 1556597853757
--Schedule_1 dt: 0.010000--
Schedule_1: 1556597853757
--Schedule_3 dt: 0.030000--
Schedule_3: 1556597853757
--update dt: 0.015883--
update: 1556597853773
--Schedule_1 dt: 0.010000--
Schedule_1: 1556597853773
--Schedule_1 dt: 0.010000--
Schedule_1: 1556597853773
--update dt: 0.016146--
update: 1556597853793
--Schedule_1 dt: 0.010000--
Schedule_1: 1556597853793
--Schedule_3 dt: 0.030000--
Schedule_3: 1556597853793
--update dt: 0.015690--
update: 1556597853805
--Schedule_1 dt: 0.010000--
Schedule_1: 1556597853805
--Schedule_1 dt: 0.010000--
Schedule_1: 1556597853805
--update dt: 0.016021--
update: 1556597853821
--Schedule_1 dt: 0.010000--
Schedule_1: 1556597853822
--Schedule_3 dt: 0.030000--
Schedule_3: 1556597853822
--update dt: 0.016190--
update: 1556597853837
--Schedule_1 dt: 0.010000--
Schedule_1: 1556597853837
--Schedule_1 dt: 0.010000--
Schedule_1: 1556597853837
--update dt: 0.015924--
update: 1556597853853
--Schedule_1 dt: 0.010000--
Schedule_1: 1556597853853
--Schedule_1 dt: 0.010000--
Schedule_1: 1556597853853
--Schedule_3 dt: 0.030000--
Schedule_3: 1556597853853
--update dt: 0.016414--
update: 1556597853870
--Schedule_1 dt: 0.010000--
Schedule_1: 1556597853870
--update dt: 0.016569--
update: 1556597853886
--Schedule_1 dt: 0.010000--
Schedule_1: 1556597853886
--Schedule_1 dt: 0.010000--
Schedule_1: 1556597853886
--Schedule_3 dt: 0.030000--
Schedule_3: 1556597853886
--update dt: 0.016493--
update: 1556597853903
--Schedule_1 dt: 0.010000--
Schedule_1: 1556597853903
--Schedule_1 dt: 0.010000--
Schedule_1: 1556597853903
--Schedule_3 dt: 0.030000--
Schedule_3: 1556597853903
--update dt: 0.016783--
update: 1556597853919
--Schedule_1 dt: 0.010000--
Schedule_1: 1556597853919
--update dt: 0.016675--
update: 1556597853936
--Schedule_1 dt: 0.010000--
Schedule_1: 1556597853936
--Schedule_1 dt: 0.010000--
Schedule_1: 1556597853936
--Schedule_3 dt: 0.030000--
Schedule_3: 1556597853936
--update dt: 0.016489--
update: 1556597853953
--Schedule_1 dt: 0.010000--
Schedule_1: 1556597853953
--Schedule_1 dt: 0.010000--
Schedule_1: 1556597853953
4.运行结果分析
我在每一个update的前面加了一个换行符,这样每一个空行之后必然都是在同一次调用的结果,从上面抽出一次调用的结果:
--update dt: 0.016675--
update: 1556597853936
--Schedule_1 dt: 0.010000--
Schedule_1: 1556597853936
--Schedule_1 dt: 0.010000--
Schedule_1: 1556597853936
--Schedule_3 dt: 0.030000--
Schedule_3: 1556597853936
可以看到,无论是update还是Schedule_1或者Schdeule_3,实际的调用时间都是一致的,都在1556597853936
这个时钟节点上,并且很明显地,Schedule_1运行了两次。
这与之前代码的预期是一样的。
结论
- schedule的interval在运行时确实可以达到一帧多次运行的效果;
- 但是并不能真实地模拟中间帧运行,只是单纯地在调用时进行了多次调用;
- 必须注意的是,每一次运行都是update在前,而其他的schedule在后,这可能会造成一些不合预期的时序差异;
- 这些并不能说明schedule是无意义的,相对unity仅提供几个update的方式而言,这样的写法对于某个方法更整洁——但是在使用时一定要注意这些更新方法的时序:update永远会在同批次的所有的schedule之前调用 ;
- 此外,update里也有三种不同有时序,分别以priority<0 、priority ==0 、priority>0 进行分隔。