笨木头  2012-12-05 17:27     Cocos2d-x,Cocos2d-x2.0     阅读(7676)     评论(10)
转载请注明,原文地址: http://www.benmutou.com/archives/36
文章来源:笨木头与游戏开发

【在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++关于接口方面的知识了。

(旁白:噗,说了等于没说。)
10 条评论
  • yirancpp 2012-12-06 09:31:00

    其实就是设计模式中的 观察者模式 COCOS2D-X中有相应的例子
    0回复
  • musicvs 2012-12-06 12:02:00

    [reply]yirancpp[/reply]
    嗯嗯,我也是自己实现了之后才发现cocos2d-x已经有这个功能了,好惆怅~不过学到东西了,嘿嘿
    0回复
  • zhaomingzmhot88 2012-12-08 20:24:00

    函数指针,还有别的吗??
    0回复
  • musicvs 2012-12-10 16:57:00

    [reply]zhaomingzmhot88[/reply]
    嘿嘿,暂时就这个~
    0回复
  • zyfrog 2012-12-19 23:50:00

    MessageDispatch 的addListener函数会将msgListener的计数+1,这样你在delete msgListener之前,必须还要调用msgListener的一个成员函数,来删除订阅,但是如果象CCNotificationCenter实现,删除订阅的工作可以放到msgListener的析构函数中!这样不仅仅可以少写一行代码,重要的是防止调用者,误调。一点拙见!
    0回复
  • musicvs 2012-12-20 08:07:00

    [reply]zyfrog[/reply]
    感谢你提的观点...然后,我还是有点不理解, "MessageDispatch 的addListener函数会将msgListener的计数+1"是因为CCArray会把add进去的对象retain一次, 而CCNotificationCenter的addObserver也有同样的操作...你所说的区别是什么呢?
    0回复
  • raaland 2012-12-23 23:01:00

    被“我、她、他”弄晕了
    0回复
  • musicvs 2012-12-23 23:40:00

    [reply]raaland[/reply]
    我错了...下次尽量少用这么多角色...
    0回复
  • zyfrog 2012-12-25 15:22:00

    [reply]musicvs[/reply]
    CCNotificationCenter的addObserver并没有直接将target直接加到Array中,而是通过CCNotificationObserver包装了一下,CCNotificationObserver也并没有将target的retain+1。
    0回复
  • musicvs 2012-12-25 19:15:00

    [reply]zyfrog[/reply]
    原来如此,感谢~!
    0回复
发表评论
粤ICP备16043700号

本博客基于 BlazorAnt Design Blazor 开发