关于cocos2dx中scheduleUpdate和schedule方法的一点测试

缘起

由于我是从unity转做的cocos2dx,在unity中仅提供了三个update方法:fixupdate、update和laterupdate,分别有不同的调用时序,但在cocos2dx中除了普通的update以外,还提供了schedule这样一个骚操作,一直对其耿耿于怀,今天抽出空来测试了一下。

主要思考的问题

与unity中的update相同的update方法非常容易理解,它主要解决所有node节点的更新操作,而开启和关闭update则通过ScheduleUpdate和UnscheduleUpdate进行;
schedule方法提供一个方法的传入接口,并根据指定的时间间隔和延迟进行调用,这个调用方式本身也是非常容易理解的;
所以问题的重点来了:

  1. schedule方法是通过什么方式调用的呢?
  2. schedule方法有一个interval参数,可以设置间隔,并且它可以设置为一个比一帧时间(默认是0.017秒)更短的时间,它真的是在这个间隔里调用的吗?它是怎么实现这个调用的?
  3. 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运行了两次。
这与之前代码的预期是一样的。

结论

  1. schedule的interval在运行时确实可以达到一帧多次运行的效果;
  2. 但是并不能真实地模拟中间帧运行,只是单纯地在调用时进行了多次调用;
  3. 必须注意的是,每一次运行都是update在前,而其他的schedule在后,这可能会造成一些不合预期的时序差异;
  4. 这些并不能说明schedule是无意义的,相对unity仅提供几个update的方式而言,这样的写法对于某个方法更整洁——但是在使用时一定要注意这些更新方法的时序:update永远会在同批次的所有的schedule之前调用 ;
  5. 此外,update里也有三种不同有时序,分别以priority<0 、priority ==0 、priority>0 进行分隔。

猜你喜欢

转载自blog.csdn.net/fjjaylz/article/details/89673488