【Cocos2d-x】Cocos2d-x3.x 定时器 Scheduler 的用法

Cocos2d-x 3.x 由导演类维护一个全局的 Scheduler,由导演控制其刷新,在任何地方都可以通过导演来获取这个全局定时器的实例指针,当然也可以自己手动创建另一个定时器,但手动创建的定时器默认不会刷新,需要手动去刷新。

今日事今日毕,写文章有头就得有尾,任何时候都不要产生以后再完善的想法。

Scheduler

Scheduler 是定义在 Director 下的全局定时器,在任何地方任何时候都可以通过导演类来取得定时器实例指针。

1
Scheduler *scheduler = Director::getInstance()->getScheduler();

Scheuler 提供以下接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
void schedule(const ccSchedulerFunc& callback, void *target, float interval, unsigned int repeat, float delay, bool paused, const std::string& key);
void schedule(const ccSchedulerFunc& callback, void *target, float interval, bool paused, const std::string& key);
void schedule(SEL_SCHEDULE selector, Ref *target, float interval, unsigned int repeat, float delay, bool paused);
void schedule(SEL_SCHEDULE selector, Ref *target, float interval, bool paused);
void scheduleUpdate(T *target, int priority, bool paused)
unsigned int scheduleScriptFunc(unsigned int handler, float interval, bool paused);

void unschedule(const std::string& key, void *target);
void unschedule(SEL_SCHEDULE selector, Ref *target);
void unscheduleUpdate(void *target);
void unscheduleScriptEntry(unsigned int scheduleScriptEntryID);
void unscheduleAllForTarget(void *target);
void unscheduleAll();
void unscheduleAllWithMinPriority(int minPriority);

bool isScheduled(const std::string& key, const void *target) const;
bool isScheduled(SEL_SCHEDULE selector, const Ref *target) const;
void pauseTarget(void *target);
std::set<void*> pauseAllTargets();
std::set<void*> pauseAllTargetsWithMinPriority(int minPriority);
bool isTargetPaused(void *target);
void resumeTarget(void *target);
void resumeTargets(const std::set<void*>& targetsToResume);

CC_DEPRECATED_ATTRIBUTE void scheduleSelector(SEL_SCHEDULE selector, Ref *target, float interval, bool paused);
CC_DEPRECATED_ATTRIBUTE void scheduleSelector(SEL_SCHEDULE selector, Ref *target, float interval, unsigned int repeat, float delay, bool paused);
CC_DEPRECATED_ATTRIBUTE void scheduleUpdateForTarget(T* target, int priority, bool paused);
CC_DEPRECATED_ATTRIBUTE void unscheduleSelector(SEL_SCHEDULE selector, Ref *target);
CC_DEPRECATED_ATTRIBUTE void unscheduleUpdateForTarget(Ref *target);
CC_DEPRECATED_ATTRIBUTE bool isScheduledForTarget(Ref *target, SEL_SCHEDULE selector);

这些接口分为四组,第一组接口为注册定时器,第二组接口为反注册定时器,第三组为包括判断是否注册了定时器、暂停定时器和恢复定时器,最后一组的 6 个接口前面都有修饰符 CC_DEPRECATED_ATTRIBUTE,表示这是一个废弃的接口,之所以还留着是为了兼容旧版本引擎,所以这几个接口直接忽略就行。最常用的是第一组和第二组,注册和反注册是成对的,我们重点看下注册定时器就行。

  1. schedule 注册自定义定时器,分为两组,每个组有两个函数,一个完整参数的函数和一个简化参数的函数。第一组的回调函数类型为 ccScheduleFunc,接收一个非成员函数,然后定义一个 key 用于标识这个定时器;第二组的回调函数类型为 SEL_SCHEDULE,接收一个成员函数,关于这两个回调函数类型,下面再详细讲。
  • @param ccScheduleFunc 定时器触发时的回调函数,非成员函数;
  • @param SEL_SCHEDULE 定时器触发时的回调函数,成员函数;
  • @param target 注册定时器的目标对象,通常是定义回调函数的对象;
  • @param interval 触发间隔;
  • @param repeat 触发次数,1 为单次触发,CC_REPEAT_FOREVER 为永久触发,其值其实是 unsigned int 的最大值;
  • @param delay 首次触发的延迟时间;
  • @param paused 是否暂停;
  • @param key 定时器的唯一标识,通过这个 key 可以拿到对应的定时器,比如反注册的时候。
  1. sheduleUpdate 注册每帧调度的定时器,不需要指定回调函数,触发时会自动回调到目标对象的 update 方法,如果目标对象没有 update 方法,则编译的时候就会报错。
  • @param target 注册定时器的目标对象;
  • @param priority 回调的优先级,数字越小越先回调,因为 Scheduler 可以为多个对象注册每帧调度,所以需要说明哪个对象的 update 方法先执行,哪个后执行;
  • @param paused 是否暂停。
  1. scheduleScriptFunc 这是绑定到脚本的接口,提供给 Lua 代码使用,一般不会在 C++ 层调用。
  • @param handler 回调函数;
  • @param interval 触发间隔;
  • @param paused 是否暂停。

下面是具体的用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
void BaseScene::update(float delta)
{
log("update per frame, delta = %f", delta);
}

void BaseScene::updateOnce(float delta)
{
log("update once, delta = %f", delta);
Scheduler *scheduler = Director::getInstance()->getScheduler();
scheduler->unschedule(CC_SCHEDULE_SELECTOR(BaseScene::updateOnce), this);
scheduler->unschedule(CC_SCHEDULE_SELECTOR(BaseScene::onProgress), this);
scheduler->unschedule("func", this);
scheduler->unscheduleUpdate(this);
}

void BaseScene::onProgress(float delta)
{
log("update with target, delta = %f", delta);
}

void onProgress2(float delta)
{
log("update with function, delta = %f", delta);
}

void BaseScene::testScheduler()
{
Scheduler *scheduler = Director::getInstance()->getScheduler();
scheduler->schedule(CC_SCHEDULE_SELECTOR(BaseScene::updateOnce), this, 0.5f, 1, 0, false);
scheduler->schedule(CC_SCHEDULE_SELECTOR(BaseScene::onProgress), this, 0.1f, false);
scheduler->schedule(onProgress2, this, 0.2f, kRepeatForever, 0, false, "func");
scheduler->scheduleUpdate(this, 1, false);
}

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
update per frame, delta = 0.016666
update per frame, delta = 0.062076
update per frame, delta = 0.016666
update per frame, delta = 0.016666
update per frame, delta = 0.016666
update with target, delta = 0.100000
update per frame, delta = 0.016666
update per frame, delta = 0.016667
update per frame, delta = 0.016667
update per frame, delta = 0.016666
update per frame, delta = 0.016666
update per frame, delta = 0.016666
update with target, delta = 0.100000
update with function, delta = 0.200000
update per frame, delta = 0.016666
update per frame, delta = 0.016666
update per frame, delta = 0.016666
update per frame, delta = 0.016666
update per frame, delta = 0.016666
update per frame, delta = 0.016668
update with target, delta = 0.100000
update per frame, delta = 0.016666
update per frame, delta = 0.016666
update per frame, delta = 0.016667
update per frame, delta = 0.016664
update per frame, delta = 0.016666
update per frame, delta = 0.016667
update with target, delta = 0.100000
update with function, delta = 0.200000
update per frame, delta = 0.016666
update per frame, delta = 0.016666
update per frame, delta = 0.016681
update per frame, delta = 0.016666
update per frame, delta = 0.016666
update per frame, delta = 0.016667
update once, delta = 0.500000

4 个 schedule 方法及 schduleUpdate 方法均没有导出 Lua,但专门写了一个导出 Lua 使用的方法 scheduleScriptFunc,使用方法如下。要注意的是 scheduleUpdate 没有导出 Lua,所以不能使用。

1
2
3
4
5
local callback1 = function()
end
local timerId1 = Scheduler:scheduleScriptFunc(callback1, 0.1, false)

Scheduler:unscheduleScriptEntry(timerId1)

私有定时器

除了导演管理的全局定时器,每个 Node 还有自己的私有定时器,这个私有定时器是个指针,默认指向的还是 Director 管理的全局定时器,只是为了方便使用,不用每次都去 Director 中去获取 Scheduler 实例。因此,默认情况下整个游戏还是只有一个定时器,那就是导演类管理下的”全局定时器“,这个定时器的刷新由导演控制,即导演刷新的时候刷新定时器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
Node::Node()
{
//...
_director = Director::getInstance();
_scheduler = _director->getScheduler();
_scheduler->retain();
//...
}

bool Director::init()
{
//...
_scheduler = new (std::nothrow) Scheduler();
//...
}

void Director::mainLoop()
{
//...
drawScene();
//...
}

void Director::drawScene()
{
//...
_scheduler->update(_deltaTime);
//...
}

可以通过接口 setScheduler 来给 Node 设置一个新的 Scheduler,但要注意的是这个自定义的 Scheduler 默认是不会刷新的,不会刷新就意味着不会调度,是个死的调度器,所以要自己在合适地方调用 scheduler->update(dt)

1
2
3
4
5
auto scheduler = new Scheduler();
node->setScheduler(scheduler);

function customLoop(dt)
scheduler->update(dt);

Node 提供的接口和 Scheduler 基本一样,需要用的时候去查 API 或源码即可,这些接口的内部还是调用的 Scheduler 的相关接口来实现。

1
2
3
4
void Node::schedule(const std::function<void(float)>& callback, float interval, unsigned int repeat, float delay, const std::string &key)
{
_scheduler->schedule(callback, this, interval, repeat, delay, !_running, key);
}

如果 Node 没有自定义 Scheduler,则下面语句是等价的。

1
2
3
4
5
6
7
8
9
10
Scheduler *scheduler = Director::getInstance()->getScheduler();
scheduler->schedule(CC_SCHEDULE_SELECTOR(BaseScene::updateOnce), this, 0.5f, 1, 0, false);
scheduler->schedule(CC_SCHEDULE_SELECTOR(BaseScene::onProgress), this, 0.1f, false);
scheduler->schedule(onProgress2, this, 0.2f, kRepeatForever, 0, false, "func");
scheduler->scheduleUpdate(this, 1, false);

this->schedule(CC_SCHEDULE_SELECTOR(BaseScene::updateOnce), 0.5f, 1, 0);
this->schedule(CC_SCHEDULE_SELECTOR(BaseScene::onProgress), 0.1f);
this->schedule(onProgress2, 0.2f, "func");
this->scheduleUpdate();

除了和 Scheduler 一样的接口外,Node 还提供两个额外的接口,scheduleOnce 用于单次调度,scheduleUpdateWithPriorityLua 则是 scheduleUpdate 的 Lua 版,前面说过 Scheduler 本身没有对 scheduleUpdate 进行 Lua 绑定,所以无法在 Lua 中使用全局调度器进行每帧调度,但是结点的私有调度器则提供了 scheduleUpdateWithPriorityLua 这个接口,用于结点的每帧调度。

函数指针

接下来我们回过头来看 schedule 的回调函数类型 ccScheduleFuncSEL_SCHEDULE,无论是哪种,schedule 接收的第一个参数都是一个“函数”,调度器调度的时候执行这个函数。

SEL_SCHEDULE

首先让我们一步步了解一下 C++ 中的宏定义、类型重定义、函数指针。

1
2
3
4
#define ZERO 0
typedef int INT;
typedef void(*FUNC)(int, float);
typedef void(DemoClass::*FUNC2)(int, float);

宏定义 #define A B,定义一个宏 A,用这个宏来表示 B,编译的时候会把宏替换成对应的值,比如 #define ZERO 0,则代码里可用 ZERO 来表示 0,但编译的时候会统一将 ZERO 替换成 0。

类型重定义 typedef B A;typedef 则刚好相反,它是给数据类型 B 起一个别名 A,比如 typedef int INT;

definetypedef 的区别:

  1. 宏定义是预处理语句,编译的时候处理,结尾无分号;类型重定义是代码语句,结尾必须分号。
  2. 定义宏 A 表示 B,给类型 B 起别名 A,宏定义是宏在前面,值在后面;类型重定义是值在前面,别名在后面。

typedef 可以给数据类型起一个别名,还可以用于定义函数指针,用一个简短的名字表示一个函数原型,其语法为 typedef void(*FUNC)(int, float),这么一看很难理解,调整一下顺序就好理解多了,typedef void(int, float) (*FUNC)void(int, float) 是一个函数原型,给其起个别名 FUNC* 表示这是一个指针类型,即 FUNC 是一个函数指针类型,这个函数指针只能赋值原型为 void(int, float) 的函数地址。只是语法要求把别名 FUNC 放在返回值 void 和参数列表 (int, float) 之间,所以最后的形式为 typedef void(*FUNC)(int, float)

FUNC 现在是一个数据类型了,直接用其定义变量即可,直接赋值一个函数地址即可,看下面例子:

1
2
3
4
5
6
7
8
9
void foo(int a, float b) { cout << "foo  " << a + b << endl; }
typedef void(*FUNC)(int, float);

void test2()
{
FUNC f = foo;
f(3, 3.14); // foo 6.14
foo(3, 3.14); // foo 6.14
}

除了全局函数,typedef 定义函数指针同样适用于类成员函数,只需要在类型前面加上类名,typedef void(DemoClass::*FUNC2)(int, float),这样只有类 DemoClass 和其子类的原型为 void(float, int) 的方法才能赋值给函数指针类型 FUNC2。类静态函数指针和全局函数指针一样,不需要类名,如果函数原型一样,则一个函数指针类型既可以接收全局函数,也可以接收类静态方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class DemoClass
{
public:
void foo(int a, float b) { cout << "foo " << a + b << endl; }
static void foo2(int a, float b) { cout << "static foo " << a + b << endl; }
};
typedef void (DemoClass::*FUNC2)(int, float);

void test2()
{
FUNC fc = &DemoClass::foo2;
fc(1, 3.14);

FUNC2 f = &DemoClass::foo;

DemoClass *demo = new DemoClass();
(demo->*f)(1, 3.14);

DemoClass demo2 = DemoClass();
(demo2.*f)(1, 3.14);
}

FUNC 是前面定义的一个函数指针类型,用于赋值全局函数,在这里赋值一个类的静态函数也是可以的。FUNC2 是用于赋值 DemoClass 成员函数的函数指针类型,要注意的是类成员函数,执行的时候需要一个类实例对象,指针也好,对象也好,然后函数指针前面需要加上一个 *,比如 demo->*f()demo.*f()。还有一点,无论是类静态方法还是成员方法,赋值给函数指针的时候都需要在前面加上 &,用于取其地址,而全局函数加不加都行,对应的全局函数调用的时候前面加不加 * 都行。

准备工作完了,接下来看看 SEL_SCHEDULE 的定义:

1
2
3
4
typedef void (Ref::*SEL_SCHEDULE)(float);
#define CC_SCHEDULE_SELECTOR(_SELECTOR) static_cast<cocos2d::SEL_SCHEDULE>(&_SELECTOR)

void schedule(SEL_SCHEDULE selector, Ref *target, float interval, unsigned int repeat, float delay, bool paused);

第一行定义函数指针类型 SEL_SCHEDULE,这个函数指针接收类 Ref 及其子类的原型为 void(float) 的成员函数。

第二行定义一个宏 CC_SCHEDULE_SELECTOR,它的值是 static_cast<SEL_SCHEDULE>(&_SELECTOR),即将传进来的值强制转换成 SEL_SCHEDULE 类型,cocos2d::SEL_SCHEDULE 所在的命名空间而已。之所以需要 static_cast 作强制类型转换,是因为传进来的函数可以是 Ref 子类的方法,如果是 Ref 的方法,这一步也是可以省的。

一般注册一个定时器是这样写的:

1
scheduler->schedule(CC_SCHEDULE_SELECTOR(BaseScene::onProgress), this, 0.1f, false);

等价于:

1
scheduler->schedule(static_cast<SEL_SCHEDULE>(&BaseScene::onProgress), this, 0.1f, false);

如果是 Ref 对象,还等价于:

1
scheduler->schedule(&Ref::onProgress, this, 0.1f, false);

归根到底,SEL_SCHEDULE 表示的是一个函数指针,调用 scheule 的时候传一个函数名进行即可,只是这个函数必须是 Ref 类的原型为 void(float) 的函数,而且如果是 Ref 子类的方法,还必须使用 static_cast 作一下强制类型转换。

ccSchedulerFunc

<functional> 库中有一种定义函数指针更简便的方法,即使用数据类型 std::function<函数原型> ,使用 typedef 给其自定义一个别名。

1
2
3
4
5
6
7
8
9
10
11
12
void foo(int a, int b) { cout << a << " + " << b << " = " << a + b << endl; }
typedef std::function<void(int, int)> FOO;

void test1()
{
std::function<void(int, int)> func1 = foo;
FOO func2 = foo;
// func1 == func2 == foo
foo(2, 4);
func1(2, 4);
func2(2, 4);
}

除了全局函数,std::function 对类成员函数和类静态函数也是适用的,语法是typedef std::function<void(DemoClass, int)> FOO2;,不过 cocos2d-x 中只有全局函数用了 std::function,这里就不展开讲了。ccSchedulerFunc 的定义如下:

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

特别简单,这是一个函数指针类型,接收原型为 void(float) 的全局函数;使用也很简单,直接赋值函数名即可。

1
2
void onProgress2(float delta) {}
scheduler->schedule(onProgress2, this, 0.2f, kRepeatForever, 0, false, "func");

总结一下,如果回调函数是类成员函数,则将其转成 SEL_SCHEDULE 类型,通过宏 CC_SCHEDULE_SELECTOR 进行强制转换;反之则为 ccSchedulerFunc 类型。