‎Cocos2d-x 3.x 学习笔记(三):Scheduler Timer 调度与定时

时间:2022-01-20 23:34:51

‎1. 概述

Cocos2d-x 的 Scheduler 离不开 Timer。Timer 类是定时器,用来规定一个回调函数应该在何时被触发。Timer 封装了已运行时间、重复次数、已执行次数、延迟秒数、时间间隔、要触发的回调函数等等,都是与一个回调函数触发相关的成员。

Scheduler 是调度器,用来对 Timer 进行调度,Timer 只是定义了回调函数的触发条件、触发次数等,真正的触发动作由 Scheduler 执行。

2. Timer 和 TimerTargetSelector、TimerTargetCallback

Timer 的成员:

class CC_DLL Timer : public Ref
{void setupTimerWithInterval(float seconds, unsigned int repeat, float delay); //
void setAborted() { _aborted = true; }
bool isAborted() const { return _aborted; }
bool isExhausted() const; virtual void trigger(float dt) = ;
virtual void cancel() = ; void update(float dt); Scheduler* _scheduler; // weak ref
float _elapsed; // 已运行时间
bool _runForever; // 是否永远运行
bool _useDelay; // 是否使用延迟
unsigned int _timesExecuted; // 已执行次数
unsigned int _repeat; // 规定执行次数, 0 = once
float _delay; // 延迟
float _interval; // 时间间隔
bool _aborted; // fff
};

Timer 有两个子类 TimerTargetSelector、TimerTargetCallback。两个子类的成员有所不同:

// TimerTargetSelector
Ref* _target;
SEL_SCHEDULE _selector;
// TimerTargetCallback
void* _target;
ccSchedulerFunc _callback;
std::string _key;

这两个类回调函数的函数类型不同和 target 类型不同,对应了两个 Scheduler::schedule(...) 方法的回调函数指针参数的不同。两个schedule(...)方法定义如下:

void schedule(SEL_SCHEDULE selector, Ref *target, float interval, unsigned int repeat, float delay, bool paused);
void schedule(const ccSchedulerFunc& callback, void *target, float interval, bool paused, const std::string& key);

TimerTargetCallback 对应的 schedule 方法参数的函数指针为 ccSchedulerFunc& callback,同时参数中包含 key,key 是 Timer 的标志,有唯一性 。ccSchedulerFunc的定义如下:

typedef std::function<void(float)> ccSchedulerFunc;

这是一个函数类型,函数满足返回值为空,参数为1个 float。

TimerTargetSelector 对应的 schedule 方法参数的函数指针为 SEL_SCHEDULE selector,不包含 key。为什么这里就不含 key 了呢?

看 SEL_SCHEDULE 的定义:

typedef void (Ref::*SEL_SCHEDULE)(float);

SEL_SCHEDULE 是函数指针,函数是 Ref 对象的成员函数,满足返回值为空,参数为1个float。

不含 key 的原因是这里定义的是指向类成员函数的指针。指向类成员函数的指针与普通函数指针的区别是,前者不仅要匹配函数的参数类型个数和返回值类型,还要匹配所属的类的对象。也就是说,selector 起到了 key 的作用,通过 selector 和 target 能找到某个类的对象对应的 Timer,而 callback 和 target 不行,所以 TimerTargetCallback 要加上 Key。

Timer 的 update(float dt) 方法是 Timer 计算时间和执行次数,判断是否触发回调函数和是否销毁 Timer所执行的函数。在符合触发条件时调用子类的 trigger(_delay) 方法,trigger(_delay)在两个子类中,调用了回调函数 *_selector 或者 _callback。当符合 isExhausted() 条件,即 Timer 不永远执行且已重复次数大于等于规定的重复次数数时,调用子类的 cancel() 方法,即调用子类对应的 unschedule(...) 方法。

3. Scheduler 调度器内 Timer 的定义与销毁

3.1 Scheduler 2种调度方式

默认调度方式:每帧调度,每帧更新,是不带间隔的调度,间隔是 interval 变量,使用 scheduleUpdate()

用户自定义调度方式:带间隔调度,不每帧更新,用户通过间隔决定更新时机,使用 schedule(...)

3.2 Scheduler 3个结构体成员

// 每帧调度使用
typedef struct _listEntry
{
struct _listEntry *prev, *next;
ccSchedulerFunc callback;
void *target;
int priority;
bool paused;
bool markedForDeletion;
} tListEntry; typedef struct _hashUpdateEntry
{
tListEntry **list; // Which list does it belong to ?
tListEntry *entry; // entry in the list
void *target;
ccSchedulerFunc callback;
UT_hash_handle hh;
} tHashUpdateEntry; // 自定义调度使用,带间隔
typedef struct _hashSelectorEntry
{
ccArray *timers;
void *target;
int timerIndex;
Timer *currentTimer;
bool paused;
UT_hash_handle hh;
} tHashTimerEntry;

tHashTimerEntry 包含了 UT_hash_handle 类型变量。在结构体中使用 UT_hash_handle 类型,就实现了哈希链表。通过哈希链表连接,每个 Entry 以 void * 类型的 target 作为“key”,Entry 作为“value”。在下面的 schedule(...) 方法中可以知道,对于Entry 的查找正是通过“key”,即指针 target 来进行的。通过“key”(target)快速找到对应的 Entry。Entry 中包括了 ccArray * 类型的 timers,每个 target 的众多 Timer 按 ccArray 数据结构排列,timers 是指向这个存储 Timer 的数据结构的指针。

剩下两个结构体在3.6节介绍。

3.3 Scheduler 的 schedule(...) 成员方法

按间隔调度使用Scheduler 的 schedule(...) 方法,该方法可以对 Entry 和 Entry 内部的定时器 Timer 进行定义、修改。

该方法看似有很多重载,实际只根据 Timer 两个子类,分为两种,在第2节也有介绍:

// 针对 TimerTargetSelector
void schedule(SEL_SCHEDULE selector, Ref *target, float interval, unsigned int repeat, float delay, bool paused);
// 针对 TimerTargetCallback
void schedule(const ccSchedulerFunc& callback, void *target, float interval, bool paused, const std::string& key);

下面是 TimeTargetSelector 的 schedule(...) 方法内执行的大致过程:

‎Cocos2d-x 3.x 学习笔记(三):Scheduler Timer 调度与定时

第1节有介绍,因为 TimeTargetSelector 用 selector 对 Timer 有唯一性,所以在判断 target 的每一个 Timer 是否为要找的 Timer 时,用 selector == timer->getSelector() 进行判断。而在 TimerTargetCallback 对应的 schedule(...) 方法中,这步判断改为

key == timer->getKey(),因为此情况下 key 对 Timer有唯一性,故用 key 进行判断。

3.4 Scheduler 的 unschedule(...) 成员方法

unschedule(...) 根据 Timer 两个子类,分为两种:

void unschedule(const std::string &key, void *target);
void unschedule(SEL_SCHEDULE selector, Ref *target);

下面是 TimerTargetSelector 的 unschedule(...)方法执行大致过程:‎Cocos2d-x 3.x 学习笔记(三):Scheduler Timer 调度与定时

Timer 有两个子类,用 key 或 selector 判断 Timer 不再赘述。

3.5 Scheduler 的 schedulePerFrame(...) 方法

每帧调度用到的是 schedulePerFrame(...) 方法:

void schedulePerFrame(const ccSchedulerFunc& callback, void *target, int priority, bool paused);

对该方法的调用过程如下,从上向下进行:

// ABCScene::init()
this->scheduleUpdate(); // Node::scheduleUpdate()
scheduleUpdateWithPriority(); // Node::scheduleUpdateWithPriority(int priority)
_scheduler->scheduleUpdate(this, priority, !_running); // Scheduler
template <class T>
void scheduleUpdate(T *target, int priority, bool paused)
{
this->schedulePerFrame([target](float dt){
target->update(dt);
}, target, priority, paused);
}

3.2节提到,每帧调度用到了两个结构体变量:

// 每帧调度使用
typedef struct _listEntry
{
struct _listEntry *prev, *next;
ccSchedulerFunc callback;
void *target;
int priority;
bool paused;
bool markedForDeletion;
} tListEntry; typedef struct _hashUpdateEntry
{
tListEntry **list; // Which list does it belong to ?
tListEntry *entry; // entry in the list
void *target;
ccSchedulerFunc callback;
UT_hash_handle hh;
} tHashUpdateEntry;

tListEntry 以双向链表方式相连,markedForDeletion 标记告诉 Schedule 是否删除该 Entry,同时存储了回调函数、优先级等。

每帧调度中,一个 target 绑定一个回调函数,一对一的关系,因为是每帧调度,不需考虑调度的间隔、次数等,所以 target 和回调函数直接绑定在一个结构体变量,也可以理解成一个 target 绑定了一个 Timer,Timer 中只定义了回调函数。

而按间隔调度一个 target 绑定多个回调函数,一对多的关系,因为每个回调函数调度的时机不同,所以用到 Timer 进行区分,众多 Timer 组合在一起成为 timers,和一个 target 绑定在一起。

tHashUpdateEntry 通过哈希链表连接,可以快速地通过“key”(target),找到指向 tListEntry 的指针 entry,list 变量用以区分该哈希链表中的 Entry 优先级与0的大小关系,list 分为3类。

schedulePerFrame(...) 方法执行大致过程如下:

‎Cocos2d-x 3.x 学习笔记(三):Scheduler Timer 调度与定时

4. Scheduler 执行调度

终于写到 update(float dt)方法了。

该方法被调用到的顺序如下:

Application::run()
Director::mainLoop()
Director::drawScene()
calculateDeltaTime();
_scheduler->update(_deltaTime);

update(...) 方法执行的大致过程:

‎Cocos2d-x 3.x 学习笔记(三):Scheduler Timer 调度与定时

一些 bool 变量的大致作用:

_updateHashLocked 在 Scheduler 构造函数中置 false。在 Scheduler 的 upadte 方法开始置 true,方法结束置 false,该变量。在 Scheduler::removeUpdateFromHash(struct _listEntry *entry) 方法中,当该变量为 false 时,可以删除一个每帧调度类型的Entry。

if (!_updateHashLocked)
CC_SAFE_DELETE(element->entry);
else
{
element->entry->markedForDeletion = true;
_updateDeleteVector.push_back(element->entry);
}

如果该变量为 true,则调度器正在 update,此时不能直接删除 Entry,需要把 Entry 加入到 _updateDeleteVector 中,在 update 方法结束前,即所有更新结束后进行删除。

_currentTargetSalvaged 在构造函数中置 false。在 Scheduler 的 upadte 方法中,把当前遍历到的 Entry 作为正在执行的 Entry 后,_currentTargetSalvaged 置 false。在 unscheduleAllForTarget(void *target) 和unschedule(const std::string &key, void *target)方法中,如果选择的 target (Entry)正在执行,则该变量置 true,不在执行则成功删除 Entry。其余操作在 Action相关文件中,暂未学习,在学习后对此处补充。