工作随想
关于cocos c++11标准的一些探讨

##前言 这两天公司下了一个新需求,是道具要支持玩家能够选择多连发,在这里出现了一个小插曲,需要执行的action有的是需要永久执行的,有的是需要按顺序执行的。因此,将Sequence、Spawn与RepeatForever一起使用时,发现了RepeatForever动作会失效,经过多方查阅资料才发现原来Sequence、Spawn与RepeatForever不能一起使用,要想达到我们需要的需求。Spawn只需要用这个节点分别执行这两个action即可,而Sequence则必须使用CallBackFunc创建action的方式来实现。好了接下来便是要讨论的内容了。

##正文 ###cocos中的定时器 定时器在cocos引擎中可谓是非常重要的,cocos整个界面的刷新、重绘都是在定时器中进行的。而要实现的功能——道具多连发,由于Repeat只能是等道具这一次发送完成之后才会开启第二次发送,因此必须使用定时器schedule来实现道具多发(那种前赴后继的效果)。

####1.schedule类与函数指针 在使用schedule时,对这个类产生了极大的兴趣,于是决定探究一下源代码,因为schedule一般都是节点Node在使用,因此,在Node类中发现了一下这么多schedule重载函数:

’''

void Node::schedule(SEL_SCHEDULE selector)
{
    this->schedule(selector, 0.0f, CC_REPEAT_FOREVER, 0.0f);
}

void Node::schedule(SEL_SCHEDULE selector, float interval)
{
    this->schedule(selector, interval, CC_REPEAT_FOREVER, 0.0f);
}

void Node::schedule(SEL_SCHEDULE selector, float interval, unsigned int repeat, float delay)
{
    CCASSERT( selector, "Argument must be non-nil");
    CCASSERT( interval >=0, "Argument must be positive");

    _scheduler->schedule(selector, this, interval , repeat, delay, !_running);
}

void Node::schedule(const std::function<void(float)> &callback, const std::string &key)
{
    _scheduler->schedule(callback, this, 0, !_running, key);
}

void Node::schedule(const std::function<void(float)> &callback, float interval, const std::string &key)
{
    _scheduler->schedule(callback, this, interval, !_running, key);
}

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);
}

void Node::scheduleOnce(SEL_SCHEDULE selector, float delay)
{
    this->schedule(selector, 0.0f, 0, delay);
}

void Node::scheduleOnce(const std::function<void(float)> &callback, float delay, const std::string &key)
{
    _scheduler->schedule(callback, this, 0, 0, delay, !_running, key);
}

’''

首先发现Node类中deschedule都调用了_scheduler->schedule(…)这个函数,于是查看_schedule到底是个什么东东。

从上图可以看出来,_schedule是通过导演类获得的一个Schedule类单例对象。既然Node类的schedule都调用了Schedule类的schedule,我们便直接分析Schedule类的schedule函数。

’''

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);

 /** 这个定时器方法将会被每隔interval秒被调用一次.
@param callback 被调用函数,稍后讨论.
@param target 被调用函数所属的类.
@param interval 调用时间间隔/秒,如果为0,则每一帧都会被调用.
@param paused 是否停止这个定时器.
@param repeat 重复调用这个callback repeat次.
@param delay 隔多少秒调用一次callback,只对第一次调用有效,后面的则有interval确定
@param 标记这个调用函数, 作为停止这个定时器时使用,因为没有别的方法可以标识 std::function<>仿函数.
@since 从cocos3.0加入这个方法
*/

’''

我们可以看到Schedule一共为我们提供了四个创建定时器的方法,其中上面两个是cocos3.0新加入的,和后面的两个创建定时器的方法唯一不同的便是第一个参数和第二个参数,其中最后一个参数是用来标记std::function<>的,这个我们稍后再讨论,先看第一个参数。后面两个函数的第一个参数为SEL_SCHEDULE selector,我们进入其定义

很熟悉的感觉,这里cocos为我们定义了一组函数指针,用来处理回调 ’''

#define CC_CALLFUNC_SELECTOR(_SELECTOR) static_cast<cocos2d::SEL_CallFunc>(&_SELECTOR)
#define CC_CALLFUNCN_SELECTOR(_SELECTOR) static_cast<cocos2d::SEL_CallFuncN>(&_SELECTOR)
#define CC_CALLFUNCND_SELECTOR(_SELECTOR) static_cast<cocos2d::SEL_CallFuncND>(&_SELECTOR)
#define CC_CALLFUNCO_SELECTOR(_SELECTOR) static_cast<cocos2d::SEL_CallFuncO>(&_SELECTOR)
#define CC_MENU_SELECTOR(_SELECTOR) static_cast<cocos2d::SEL_MenuHandler>(&_SELECTOR)
#define CC_SCHEDULE_SELECTOR(_SELECTOR) static_cast<cocos2d::SEL_SCHEDULE>(&_SELECTOR)

’''

把这个单独拿出来讨论是因为记忆中笔者记得_SELECTOR与&_SELECTOR应该都是函数地址的,为什么还要取地址符和强转呢,经过多方查阅资料和求证才发现原来是的基础掌握的不够扎实。函数名和对函数名取地址虽然指向的都是同一块地址,但是其意义还是有些不同的_SELECTOR指的是这个函数实体,仅仅这个函数,而&_SELECTOR代表的确实函数指针,它可以指向任何这种类型的函数实体;就如同数组名是数组第一个元素地址、&数组名是整个数组的地址一般。而在以前c语言中确实可以直接用函数名对函数指针进行赋值,原来能够赋值的都为左值,并且在赋值的过程中编译器自动为我们进行了一个类型转换——将_SELECTOR转换成&_SELECTOR,而在c++标准中,类的非静态成员函数不为左值,不能进行隐式的类型转换,如果我们想要为函数指针赋值,则必须在函数名前加&并进行显示的类型转换。

####2.函数指针的调用

cocos中的函数指针理解之后我们继续进入schedule函数的内部查看,因为类成员函数指针的使用最终肯定会是this->*func()这种形式,我们最终要验证自己的猜测。在schedule函数的最后我们发现了这个

继续进入查看

在这里,cocos将SEL_SCHEDULE selector, Ref* target的值都传给了对应的成员变量,我们继续进行搜索。

果不其然我们找到了这个函数指针的调用,而这个trigger函数便是在update函数中调用的。

###cocos c++11新标准

cocos自从3.0之后便全面支持了c++11新标准,其中最为称道的便是std::function仿函数的加入了。

C++标准是这样定义的

类模版std::function是一种通用、多态的函数封装。std::function的实例可以对任何可以调用的目标实体进行存储、复制、和调用操作,这些目标实体包括普通函数、Lambda表达式、函数指针、以及其它函数对象等。std::function对象是对C++中现有的可调用实体的一种类型安全的包裹(我们知道像函数指针这类可调用实体,是类型不安全的)

通俗来说仿函数就是用来取代单不仅仅只是函数指针的,它比函数指针更为安全也更为灵活。

在上面的两个schedul函数中,第一个参数const ccSchedulerFunc& callback,我们查看其定义。发现

其就是为void(float)这个仿函数类起了一个别名.

再次发现了这个函数

继续进入

我们再次寻找_callback

这一次直接调用就行了,没有使用函数指针

然后我们在传入回调函数时如果是类的非静态成员函数,还必须使用std::bind进行绑定,因为类的成员函数是属于具体对象的,我们必须要传入类的对象才能够调用成功,例如CCMeniItem源码

例如我们调用schedule便可以采用这种形式:

###Lambda表达式

既然仿函数可以包裹Lambda表达式,我们便可直接采用Lambda表达式来代替std::bind进行绑定,同时也减少了成员函数的定义,轻量级的回调成员函数更加方便于阅读和修改等优点,代码如下:

’''

schedule([=](float dt)
	{
		//定时器回调函数的内容
	}, "aaa");

’’' 这种方式是不是要比std::bind简单的多呢,还不用定义回调函数。

既然如此,想起了cocos创建action的类也可以是回调函数CallFuncN。查看其定义发现其create函数为

’''

static CallFuncN * create(const std::function<void(Node*)>& func);

’’' 其定义也是一个std::function仿函数,因此我们直接使用仿函数

’''

sp->runAction(Sequence::create(EaseSineIn::create	(bez),
	[=](Node *node){
		node->removeFromParent();
	}, NULL));

’’' 发现程序报错

判断应该是创建动作时出现的错误,是不是直接用Lambda表达式不行呢,显示的强转成std::function试试。

’''

sp->runAction(Sequence::create(EaseSineIn::create(bez),
	std::function<void(Node *)>([=](Node *node){
		node->removeFromParent();
	}), NULL));

’’' 发现继续报错

这个错误就很明显了,CallFuncN是一个继承自Ref的类,这样它就加入了cocos的自动内存管理机制中去了,而我们单纯的用一个std::function作为参数传进去是没有自动管理内存机制的,因此,只需将代码改成这样

’''

sp->runAction(Sequence::create(EaseSineIn::create(bez),
CallFuncN::create([=](Node *node){
	node->removeFromParent();
	}), NULL));

’’' 就能成功的执行了,因此我们以后可以在开发中愉快的使用仿函数和Lambda表达式了


最后修改于 2022-07-12