IxEngine开发笔记

导航

第九回 Stub

我不知道它有没有专门的名称,在engine里,我叫它Stub,所谓Stub是指一个对象暴露出来和外界联系的接口,它分为两种,signal和slot,熟悉Qt的朋友肯定立即就会知道这是什么东西了,事实上这个东西我也是从Qt里借鉴过来的,只是我的实现方法和它不一样罢了.一个对象在它需要的时候可以发出各种事件,称为signal,也可以通过slot接受来自外界的事件,每个signal和slot都有一个名字,使用这些对象时,可以通过这些名字来连接指定的signal和slot,使对象之间协同工作.举几个例子:
最简单的就是按钮了,当它被点击时会发出一个Click的signal:

 

  

 

一个延时器,它有一个名叫Start的slot,用来触发计时开始,然后当指定的时间到了以后,会发出一个TimeUp的signal:

 

  

 

一个角色控制器,它有一个名叫UserInput的slot,用来接收各种鼠标/键盘消息,处理后,会把它转换成对角色的各种操控命令,以signal的形式发出,比如Move,Turn等,未处理的消息,通过UnHandled发出.

 

  

 

玩过Crysis的朋友应该会有点眼熟吧,至少从界面上和Crysis Editor的Flow Graph有点像,事实上Crysis的Flow Graph也的确是这套系统的重点参考对象.

下面说说这个东西的实现,在目前的实现中,一个signal的本质是一个函数调用,而不是一个消息什么的,我们为每一个signal维护一个函数指针的队列,发出一个signal时,实际做的事情是去找到这些函数指针,并一一调用它们,而slot其实就是一个可以拿到函数指针的对外接口,并且与之对应需要实现一个处理函数,用来响应来自signal的调用.连接signal和slot的时候,我们根据slot名字到Target对象中找出一个函数指针,然后把它添加到Source对象的signal的函数指针队列中去.既然是函数调用,就会有参数,目前一次调用只能传递一个参数,我叫它Property,所有的Property都派生自一个基类,用来抽象出参数的new/delete/复制/比较/类型转换等一些基本操作,然后我为大多数常用的数值类型(Int,Float,Vector3D,Matrix,String等)都各写了一个Property的类,这样这些类型的数值就可以成为signal/slot的调用参数了,当然如果需要的话你也可以定义自己需要的Property.我还写了一个"百搭"的Property,它可以存储多个Property,并且它可以和其它类型Property相互转换,这样使一次调用传递多个参数成为可能.Stub的描述信息(比如名称,注释,参数类型等)是作为静态成员变量写在类里面的,而signal的函数指针队列则是每个对象独有的,必须写成成员变量.当然我还是用我的一贯丑陋的宏+模板的方式来完成这套系统.
上面是对实现的粗略的介绍,我想可能并没能讲得很清楚,不过还是那句话,知道做什么比知道怎么做要重要的多.大多数有经验的程序员应该都能写出一套这样的东西的,而且可能会写得更漂亮.
下面是几个使用的例子:
代码
class CButton
{
public:
    ...
    ...
protected:
    GStubBegin(CButton)

        GSignalVoid(Clicked, "按钮被点击"//这个Signal没有参数,
    
    GStubEnd()
    
    ...
    ...
    
void OnClick()
    {
        ...
        
        StubFireVoid(Clicked);//发送Clicked的signal
    }
        
};

class CDelay
{
public:
    ...
    ...
protected:
    GStubBegin(CDelay)

        GSlotVoid(Start, "开始"
        GSignalVoid(TimeUp, "时间到")
    
    GStubEnd()

    
//Start 的处理函数
    void handler_Start()
    {
        
//开始计时
        ...
    }
    
    
    
void Update()
    {
        ...
        ...

        
if (IsTimeUp())//时间到
            StubFireVoid(TimeUp);
    }
    ...
    ...
};

struct PropInput:public PropertyBase
{
    
//鼠标,键盘消息
    ...
    ...
};
    

class CCharCtrl
{
public:
    ...
    ...
protected:
    GStubBegin(CCharCtrl)

        GSlotDefine(UserInput,PropInput,"键盘/鼠标输入消息");//这个Slot接受一个PropInput类型的参数

        GSignalDefine(UnHandled,PropInput,"未处理的键盘/鼠标输入消息");//这个Signal发送一个PropInput类型的参数

        GSignalVec3D(Move,"朝某方向移动");//参数为一个Vector3D,代表一个方向
        GSignalVoid(StopMove,"停止移动消息");
        GSignalVoid(TurnLeft,"向左转动消息");
        GSignalVoid(TurnRight,"向右转动消息");
        GSignalVoid(StopTurn,"停止转动消息");
        GSignalVec3D(TurnTo,"旋转到指定角度");//参数为一个Vector3D,代表一个欧拉角

    GStubEnd()

    
//UserInput 的处理函数
    void handler_UserInput(PropInput *input)
    {
        BOOL bHandled=FALSE;

        
//根据输入发送各种控制命令(Move,StopMove,TurnLeft,TurnRight,StopTurn,TurnTo)
        ...
        ...


        
if (!bHandled)
            StubFire(PropInput,input);
        ...
    }

    ...
    ...
};

 


下面说说使用这套系统的原因:
1.相对于通过继承/重载的方式来复用代码而言,我更喜欢使用组件化的方式来复用代码,底层实现很多基本功能模块,上层负责组合它们,这是我喜欢的方式,而Stub系统为组件之间的通讯提供了一种方便的手段.
2.图形化的编辑方式很cool,可以让非程序员参与设计游戏逻辑.我想我不是唯一一个被Crysis编辑器的FlowGraph震撼的人.不过好像CryEngine这套系统并不易用,似乎缺乏模块化重用的机制.这方面我做了些改进.
3.普通的程序/脚本在描述有时延的过程时非常不方便,比如,某个角色走到A点后,放个技能,然后跳一下,着地后再走向B点,这样的过程用普通的脚本是很难描述的,因为几乎每个命令都需要一定时间来完成,而脚本是立即执行完毕的.(不过UnrealScript好像有这个功能,不得不佩服一下).而通过Stub系统+组件图形化的编辑方式,可以比较方便和直观的描述这样的过程.我希望我们的engine将来可以用完全图形化的方式来编辑角色的技能.

我对这套系统还是有担忧的,因为它看上去太像一个玩具了,而且性能上也有些问题,在实用中表现如何还需要检验,希望不要沦为一个玩具.下面是一个例子,用来展示组件是如何通过Stub的联系来工作的:
  
这个flow graph描述了在一个网游里面游戏主角进入世界地图前的一小段过程:开始后首先弹出一个等待画面,并且在后台等待server端发过来的主角信息,当主角信息全部收到后,关闭等待画面,然后进入下一个过程.各个节点对象的意义:
proxy: 一个转接口,将一个signal转发给多个slot
curtain: 等待画面
clock: 时钟,定时发送时钟signal
waitplayer: 这是一个用脚本实现的节点,用来检查主角信息是否已经完全收到
delay: 延时(因为初始化时网络和硬盘都比较繁忙,可能会卡,所以略等一会).
关于Stub就说到这里,希望你能看得明白.下回说说资源管理.

posted on 2010-07-19 23:01  ixnehc  阅读(1316)  评论(0编辑  收藏  举报