第九回 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);
...
}
...
...
};
{
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就说到这里,希望你能看得明白.下回说说资源管理.