Cocos2d-x 3.x 由导演类维护一个全局的 Scheduler,由导演控制其刷新,在任何地方都可以通过导演来获取这个全局定时器的实例指针,当然也可以自己手动创建另一个定时器,但手动创建的定时器默认不会刷新,需要手动去刷新。
今日事今日毕,写文章有头就得有尾,任何时候都不要产生以后再完善的想法。
Scheduler
Scheduler
是定义在 Director
下的全局定时器,在任何地方任何时候都可以通过导演类来取得定时器实例指针。
1 | Scheduler *scheduler = Director::getInstance()->getScheduler(); |
Scheuler
提供以下接口:
1 | void schedule(const ccSchedulerFunc& callback, void *target, float interval, unsigned int repeat, float delay, bool paused, const std::string& key); |
这些接口分为四组,第一组接口为注册定时器,第二组接口为反注册定时器,第三组为包括判断是否注册了定时器、暂停定时器和恢复定时器,最后一组的 6 个接口前面都有修饰符 CC_DEPRECATED_ATTRIBUTE
,表示这是一个废弃的接口,之所以还留着是为了兼容旧版本引擎,所以这几个接口直接忽略就行。最常用的是第一组和第二组,注册和反注册是成对的,我们重点看下注册定时器就行。
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 可以拿到对应的定时器,比如反注册的时候。
sheduleUpdate
注册每帧调度的定时器,不需要指定回调函数,触发时会自动回调到目标对象的update
方法,如果目标对象没有update
方法,则编译的时候就会报错。
@param target
注册定时器的目标对象;@param priority
回调的优先级,数字越小越先回调,因为Scheduler
可以为多个对象注册每帧调度,所以需要说明哪个对象的update
方法先执行,哪个后执行;@param paused
是否暂停。
scheduleScriptFunc
这是绑定到脚本的接口,提供给 Lua 代码使用,一般不会在 C++ 层调用。
@param handler
回调函数;@param interval
触发间隔;@param paused
是否暂停。
下面是具体的用法:
1 | void BaseScene::update(float delta) |
运行结果:
1 | update per frame, delta = 0.016666 |
4 个 schedule
方法及 schduleUpdate
方法均没有导出 Lua,但专门写了一个导出 Lua 使用的方法 scheduleScriptFunc
,使用方法如下。要注意的是 scheduleUpdate
没有导出 Lua,所以不能使用。
1 | local callback1 = function() |
私有定时器
除了导演管理的全局定时器,每个 Node
还有自己的私有定时器,这个私有定时器是个指针,默认指向的还是 Director
管理的全局定时器,只是为了方便使用,不用每次都去 Director
中去获取 Scheduler
实例。因此,默认情况下整个游戏还是只有一个定时器,那就是导演类管理下的”全局定时器“,这个定时器的刷新由导演控制,即导演刷新的时候刷新定时器。
1 | Node::Node() |
可以通过接口 setScheduler
来给 Node
设置一个新的 Scheduler
,但要注意的是这个自定义的 Scheduler
默认是不会刷新的,不会刷新就意味着不会调度,是个死的调度器,所以要自己在合适地方调用 scheduler->update(dt)
。
1 | auto scheduler = new Scheduler(); |
Node
提供的接口和 Scheduler
基本一样,需要用的时候去查 API 或源码即可,这些接口的内部还是调用的 Scheduler
的相关接口来实现。
1 | void Node::schedule(const std::function<void(float)>& callback, float interval, unsigned int repeat, float delay, const std::string &key) |
如果 Node
没有自定义 Scheduler
,则下面语句是等价的。
1 | Scheduler *scheduler = Director::getInstance()->getScheduler(); |
除了和 Scheduler
一样的接口外,Node
还提供两个额外的接口,scheduleOnce
用于单次调度,scheduleUpdateWithPriorityLua
则是 scheduleUpdate
的 Lua 版,前面说过 Scheduler
本身没有对 scheduleUpdate
进行 Lua 绑定,所以无法在 Lua 中使用全局调度器进行每帧调度,但是结点的私有调度器则提供了 scheduleUpdateWithPriorityLua
这个接口,用于结点的每帧调度。
函数指针
接下来我们回过头来看 schedule
的回调函数类型 ccScheduleFunc
和 SEL_SCHEDULE
,无论是哪种,schedule
接收的第一个参数都是一个“函数”,调度器调度的时候执行这个函数。
SEL_SCHEDULE
首先让我们一步步了解一下 C++ 中的宏定义、类型重定义、函数指针。
1 |
|
宏定义 #define A B
,定义一个宏 A
,用这个宏来表示 B
,编译的时候会把宏替换成对应的值,比如 #define ZERO 0
,则代码里可用 ZERO
来表示 0,但编译的时候会统一将 ZERO
替换成 0。
类型重定义 typedef B A;
,typedef
则刚好相反,它是给数据类型 B
起一个别名 A
,比如 typedef int INT;
。
define
与 typedef
的区别:
- 宏定义是预处理语句,编译的时候处理,结尾无分号;类型重定义是代码语句,结尾必须分号。
- 定义宏 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 | void foo(int a, float b) { cout << "foo " << a + b << endl; } |
除了全局函数,typedef
定义函数指针同样适用于类成员函数,只需要在类型前面加上类名,typedef void(DemoClass::*FUNC2)(int, float)
,这样只有类 DemoClass
和其子类的原型为 void(float, int)
的方法才能赋值给函数指针类型 FUNC2
。类静态函数指针和全局函数指针一样,不需要类名,如果函数原型一样,则一个函数指针类型既可以接收全局函数,也可以接收类静态方法。
1 | class DemoClass |
FUNC
是前面定义的一个函数指针类型,用于赋值全局函数,在这里赋值一个类的静态函数也是可以的。FUNC2
是用于赋值 DemoClass
成员函数的函数指针类型,要注意的是类成员函数,执行的时候需要一个类实例对象,指针也好,对象也好,然后函数指针前面需要加上一个 *
,比如 demo->*f()
,demo.*f()
。还有一点,无论是类静态方法还是成员方法,赋值给函数指针的时候都需要在前面加上 &
,用于取其地址,而全局函数加不加都行,对应的全局函数调用的时候前面加不加 *
都行。
准备工作完了,接下来看看 SEL_SCHEDULE
的定义:
1 | typedef void (Ref::*SEL_SCHEDULE)(float); |
第一行定义函数指针类型 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 | void foo(int a, int b) { cout << a << " + " << b << " = " << a + b << endl; } |
除了全局函数,std::function
对类成员函数和类静态函数也是适用的,语法是typedef std::function<void(DemoClass, int)> FOO2;
,不过 cocos2d-x 中只有全局函数用了 std::function
,这里就不展开讲了。ccSchedulerFunc
的定义如下:
1 | typedef std::function<void(float)> ccSchedulerFunc; |
特别简单,这是一个函数指针类型,接收原型为 void(float)
的全局函数;使用也很简单,直接赋值函数名即可。
1 | void onProgress2(float delta) {} |
总结一下,如果回调函数是类成员函数,则将其转成 SEL_SCHEDULE
类型,通过宏 CC_SCHEDULE_SELECTOR
进行强制转换;反之则为 ccSchedulerFunc
类型。