【在Cocos2d-x中面向接口编程01】向她表白之实现函数回调
首先,我要郑重地声明一件事情:我现在很尿急,但是厕所有人~!
嗯,然后,我本来是一名Java程序员,由于Cocos2d-x太吸引人,我就硬着头皮学,可是C++和Java那看似差不多,实际又确实差很多的纠结,让我变得...很尿急~!
(旁白:可以少说一些废话么?)
最近我在试着写一个小游戏,叫做《粒子格斗》。
(旁白:你已经说过了。。。)
然后,我自己写了一个消息派发的功能,因为当时我并不知道Cocos2d-x已经集成了这个功能。我写Java习惯了,所以我按照Java的思维习惯实现了这个消息派发。如果对消息派发不熟悉或者完全不懂的朋友,那个,还是百度一下会好一些~
(旁白:等等~!你不是应该介绍实现函数回调吗?)
哦,对,我应该介绍实现函数回调的,但是如果你和我一样,是Java出身的,并且Java功底也不是很深厚,对于掌握新语言不太迅速的话,那么,请继续往下看。
如果你对C++已经是比较熟悉了,那,可以忽略这篇文章。笨木头花心贡献,啥?花心?不呢,是用心~转载请注明,原文地址:
http://blog.csdn.net/musicvs/article/details/8261147正文:1. 从Java移植过来的消息派发按照Java的思路,实现如下:
a. 首先要实现消息派发的核心类,用于订阅、删除以及发布消息,这个类只需用实现3个函数:
/* 订阅消息 */
addListener(I_MessageDispath msgListener, const char* sMsgType);
/* 删除订阅的消息 */
delListener(I_MessageDispath msgListener, const char* sMsgType);
/* 发布消息 */
dispatchMsg(const char* sMsgType, CCObject* pData);
b. 然后就要实现一个接口,一个代表消息订阅者的接口(这很美妙,我太喜欢接口了,它让我的代码之间的耦合度减小了很多很多~!),我们命名为 I_MessageDispatch。
[cce_cpp]class I_MessageDispatch : public CCObject {
public:
void onRecv(CCObject* pData);
};
[/cce_cpp]
c. 详细的就不多解释了,我们再用一个CCDictionary保存不同类型的订阅者对象,比较完整的头文件内容如下:
[cce_cpp]class MessageDispatch : public CCObject {
public:
static MessageDispatch* sharedMessageDispatch();
/* 订阅消息 */
addListener(I_MessageDispath msgListener, const char* sMsgType);
/* 删除订阅的消息 */
delListener(I_MessageDispath msgListener, const char* sMsgType);
/* 发布消息 */
dispatchMsg(const char* sMsgType, CCObject* pData);
private:
/* CCDictionary<CCArray<I_MessageDispatch*>, const char*> */
CCDictionary* mListenerDict;
};[/cce_cpp]
d. 然后就是一些细节的处理,好,不多说。就是字典里保存了消息类型和消息订阅者列表的对应关系。添加订阅的时候,就把实现了I_MessageDispatch接口的对象添加到对应的列表里(删除同理),然后发布消息的时候,取出要发布的消息类型对应的订阅者列表,对列表中的每个对象都调用一次onRecv函数,就可以了。
(旁白:你不是说不多说么?= = 我感觉你说了很多)
2. 看起来它工作得很好看起来,这个消息派发类工作得不错,所有功能都能正常运行。可是,问题来了。
C++中是没有接口的说法的,它只有多继承,我太怕多继承了,我可给它整晕了好多次。首先它有一个弱点,二义性,额,我就不解释了,专业解释请百度一下(旁白:你压根儿就解释不了吧...)。比如我有这样一个类:
[cce_cpp]class ClientFight : public CCLayer {
public:
bool initWithLayer(CCLayer* mLayer);
static ClientFight* createWithScene(CCLayer* mLayer);
#endif[/cce_cpp]
嗯,看起来没有什么问题,但是,如果我这个类想订阅消息,怎么办?很简单啊,让它继承I_Messagedispatch接口,然后实现onRecv函数,然后订阅消息,就OK了,这是我在写Java时最容易想到的方法。
[cce_cpp]class ClientFight : public CCLayer, public I_MessageDispatch {
public:
bool initWithLayer(CCLayer* mLayer);
static ClientFight* createWithScene(CCLayer* mLayer);
virtual void onRecv(CCObject* pData);
#endif[/cce_cpp]
3. 二义性,烦死你来看看这样一个情景,某个类创建了ClientFight对象,并且保持了它的引用,当我要释放引用的时候,很自然要调用release函数。于是,编译器会告诉你,ClientFight的release函数调用不明确。
原因呢?ClientFight继承了CCLayer, CCLayer继承了CCObject,I_MessageDispatch也继承了CCObject。
于是,它们两个都有release函数,而ClientFight作为它们的子类,不知道该继承谁的release才好,所以编译器晕了,我也晕了。
要解决这个问题很简单,本来是这样调用的:mClientFight->release();
现在改为:mClientFight->CCLayer::release();
OK,这样编译器就不会晕了。但我还是晕啊,这太麻烦了~!这只是其中一个小问题,以后还会遇到更多问题,对于Java出身的人来说,我暂时无法接受这么麻烦的编码。
4. 发现使用接口的新方式我写的那个消息派发还不止这点故障,还有更多的故障,我就不说了。
偶然的机会,我百度了一下:Cocos2d-x 观察者。
然后被我发现了子龙山人的博客,他介绍了Cocos2d-x的观察者模式,正好就是说的消息派发,木了个头的,原来已经有这个功能了,我还干嘛写得这么辛苦?
(旁白:你废话多,所以总是干些多余的事情= =)
这个消息派发的功能大家可以看看这个类:CCNotificationCenter。
或者看看大牛的博客:
http://www.zilongshanren.com/cocos2d-x-design-pattern-6-observer/http://www.xinze.me/cocos2d-x-ccnotificationcenter/我稍微拜读了一下Cocos2d-x的源码,发了一个很奇妙的地方,是一种“另类”的接口使用方式。
CCNotificationCenter在添加订阅者的时候,不需要订阅者实现某个接口,直接添加就可以了。
CCNotificationCenter会自动新建一个对象,这个对象是CCNotificationObserver,其实这就可以当做是一个接口,只不过订阅者不是实现这个接口,而是直接绑定到这个接口身上。按照我朋友的一个说法,这有点像适配器模式,也许就是适配器模式。
[cce_cpp]void CCNotificationCenter::addObserver(CCObject *target,
SEL_CallFuncO selector,
const char *name,
CCObject *obj)
{
if (this->observerExisted(target, name))
return;
CCNotificationObserver *observer = new CCNotificationObserver(target, selector, name, obj);
if (!observer)
return;
observer->autorelease();
m_observers->addObject(observer);
}[/cce_cpp]
怎么样?这样我们就可以避免一个对象为了订阅消息而不得不使用多继承了~
5. 实例:向她表白我们先来练练手,熟悉一下C++的函数参数的传递,再次声明,已经熟悉C++的朋友,直接忽略我。
(旁白:我们早就忽略你了。。。)
我们来实现一个经典的事情:我不敢向喜欢的女生表达,所以我想让我的朋友帮我这个忙。
这个类很简单:
[cce_cpp]#ifndef __TELL_LIKE_HER_H__
#define __TELL_LIKE_HER_H__
#include "cocos2d.h"
USING_NS_CC;
typedef void (CCObject::*SEL_CallFuncL) (CCObject*);
#define callfuncL_selector(_SELECTOR) (SEL_CallFuncL)(&_SELECTOR)
/* 这是一个不太像接口的接口 */
class I_LikeHer : public CCObject {
public:
I_LikeHer(CCObject* targer, SEL_CallFuncL selector);
void performSelector();
private:
SEL_CallFuncL mSelector;
CCObject* mTargert;
};
/* 这是我的朋友,他会帮我表达我的心意 */
class TellLikeHer : public CCObject {
public:
static TellLikeHer* sharedTellLikeHer();
/* 设置要表白的主角~ */
void setTheActor(CCObject* targer, SEL_CallFuncL selector);
/* 她就在这里,TellLikeHer将代替主角传达心意 */
void sheIsHere();
private:
static TellLikeHer* mTellLikeHer;
I_LikeHer* mLikerHer;
};
#endif[/cce_cpp]
要实现函数回调,我们需要两样东西:
typedef void (CCObject::*SEL_CallFuncL) (CCObject*);
#define callfuncL_selector(_SELECTOR) (SEL_CallFuncL)(&_SELECTOR)
定义一个SEL_CallFuncL类型,以及一个传递函数参数的宏,我不太善于解释这个,我是依葫芦画瓢仿照源码写的,所以我选择不解释。
我们看到有一个I_LikeHer类,这个就是相当于一个接口,TellLikeHer类只保存这种接口的类,所以,我们依旧是面向接口编程,减少了耦合度。
我们来看看setTheActor函数(函数名取得不好,不要灭了我。。。):
[cce_cpp]void TellLikeHer::setTheActor( CCObject* targer, SEL_CallFuncL selector ) {
mLikerHer = new I_LikeHer(targer, selector);
}[/cce_cpp]
很简单,外部传进来的可以是任意的对象类型(当然,必须是继承了CCObject的),TellLikeHer类根据就不需要关心谁传进来了,因为它会创建一个I_LikeHer对象,把传进来的对象包装起来。
噗,听起来又有点像装饰者模式?是蛮像的。
再来看看sheIsHere函数:
[cce_cpp]void TellLikeHer::sheIsHere() {
mLikerHer->performSelector();
}[/cce_cpp]
这确实是在面向接口编程,因为我们只需要调用I_LikeHer接口的performSelector函数,调用了这个函数之后,监听者就会被回调,是的,我们帮他传达了心意了,并且告诉了他结果。
[cce_cpp]void I_LikeHer::performSelector() {
(mTargert->*mSelector)(NULL);
}[/cce_cpp]
这个是什么东西?它就是我们的主题:函数的回调。
这有点混乱,我明白。(旁白:你真心觉得只有一点点混乱?)我们来画图理解:

这很好地解决了二义性的问题。
也许是我的经验太少,对接口的使用了解不深入,所以总以为面向接口编程,就是定义一个接口,让其他类来实现。
在Java里,我暂时没有遇到什么大问题。
不过在Cocos2d-x中刚好暴露了这种方式的缺陷。
好吧,对我来说,这是一次不错的收获。因为二义性的问题,让我认识了Cocos2d-x的消息派发(CCNotificationCenter)以及一种很不错接口使用方式。
又不过,这种接口的使用方式也不见得是万能的,如果一个接口要实现多个函数,那,又该怎么做呢?于是,我得拿出一本C++的教程,研究一下C++关于接口方面的知识了。
(旁白:噗,说了等于没说。)