尝试游戏对象的建模(1)——游戏对象间的关系
2009-05-16 19:27
232 查看
在游戏中,我们经常会看见很多活动的或静止的物体,如:人物、怪物、房屋、可破坏的巨石和墙面等等。 这些物体可以跟玩家进行各种交互,譬如碰撞检测、出发剧情、战斗和位移。像这样的情况几乎在每款游戏中都存在着,且使用之频繁超出大多数人的预料,因此而产生的BUG和设计问题常常是让人头痛的问题,我写这一系列的文章也是为了尝试设计一些有广泛扩展性和可靠性的架构来解决问题,虽然最终可能也并不理想,但作为自己的实践提高,也为了能够抛砖引玉,更是为了与广大高手们交流。
这就是最典型的怪物—玩家之间的交互,我们可以通过这样的描述对这组行为进行建模并做适当扩展,这当然不是最终版本,但把它写出来有助于在未来查看代码进化的过程,如下面的代码:
从上面的代码中可以看到,我使用了boost::signals库进行消息间传递,这个库需要做一下编译才能正确使用。然后,我使用了monster和player两个类作为怪物和玩家两个游戏对象的虚拟,他们都继承自game_object类,而game_object类又继承自signal_source类,而为了将来可能的扩展情况,我又让signal_source继承自boost::any,这是一个万能变量,我们可以在未来的设计中将任意类型的游戏对象传递到any类型的参数上。但这也许是不必要的吧,总之先考虑万全是没坏处的。
这段代码中,monster的位置改变会通知player,player做的相应行为都写在player::onPositionChanged函数里。但是我在设计的过程中发现了一个情况,我们必须要在player中记录与monster的消息连接,因为boost::signals::connection会在连接的时候被boost::signal::connect()函数返回,这个connection掌握着该连接的生杀大权,必须放到player对象中保持起来才能得到有效控制。假设玩家在进入怪物的领域后,与怪物进行了交互,然后玩家逃离了怪物周围,这时候怪物的行为将不对玩家产生任何影响,所以消息连接就可以断开以节省资源。player的实例中必须记录了connection对象才行,所以,我设计代码时让连接后产生一个消息给指定玩家对象的成员函数,这时候,player::onPositionChanged函数的参数中有一个is_first()函数,用来检测该消息是否是连接后的第一次消息,如果是,则表示这是一个传回connection的消息,可以借此机会将传回的connection对象保持起来,否则就是一个普通的PositionChanged消息。与connection对象一同传回的还有消息源对象,这有利于分类保持并管理connection对象。
好了,现在要去保存connection对象,所以,我对player类做一些修改,新的类添加了connection的容器作为成员变量,但是,由于要存储connection的类何止是player一个呢?所以,我又把connection容器的声明移到了game_object中,新的game_object的声明如下:
新的game_object类多了一个struct定义,一个typedef,一个成员变量
我们先来看看这个struct的定义:
这是一个std::map的key值类型,因为我们要用map来存储connection的缘故,为了便于查找。键值包含了两个值,一个是连接消息源对象的地址,另一个是监听的消息类型,这两个元素唯一标识了一个connection,方便以后在代码和脚本中查找连接并做相应处理。
再来看看为了可读性而定义的typedef:
typedef std::map<connection_ley, boost::BOOST_SIGNALS_NAMESPACE::connection> connection_container;
最后是成员变量:
connection_container mConnections;
当然,以上这些设计均不保证在未来不做变更。
好了,现在我们可以修改一下player::onPositionChanged的代码来让新修改的机能发挥作用:
就目前来看,这还算是一个很漂亮的方法,但在后面的设计中,这里还要做很多的改动。
下面,有些消息模式设计经验的人都会注意到,我的代码中一个很蹩脚的地方,就是在event_args类中并没有记录事件的类别。对的,类别本身并没有去记录,于是player::onPositionChanged得到的参数args中也没有办法确认这到底是不是PostionChanged消息。那么,既然函数命名是onPositionChanged,则我们必然会去处理PostionChanged消息了,似乎没有确认消息类型的必要。
但是,一个不争得事实就是:函数名和函数会干什么并没有逻辑和编译器法规上的约定。
命名是随意的,这不能成为不去检测消息类型的借口。更重要的是,我们可能会在代码中用一个函数去处理多个消息类型。譬如,我想将怪物所做的一切位移行为都用一个函数来做处理,这个函数命名为:player::onMonsterCoordValueChanged(),它将处理PositionChanged、PositionChanging、DirectionChanged、DirectionChanging这4种消息。我们对event_args类和player类稍作修改:
1、 为event_args添加一个成员变量:int mSignalTag,新的类如下:
2、 player类的onPositionChanged函数名改为onMonsterCoordValueChanged
3、 Main函数中的onPositionChanged函数名参数改为onMonsterCoordValueChanged,且添加一行,具体如下:
4、 改动player:: onMonsterCoordValueChanged的实现,具体如下:
5、 改变game_object类中的push_slot和fire_signal两个函数的实现,如下:
6、 我们还需要改动一下game_object::get_actor函数,因为它太过危险,事实也是如此,我在运行时出现了错误,因为字节对齐的关系,我的算法失误了,新的函数如下:
新的代码完成了,看起来比以前要好了很多啊,下面我们继续修改。
这就是最典型的怪物—玩家之间的交互,我们可以通过这样的描述对这组行为进行建模并做适当扩展,这当然不是最终版本,但把它写出来有助于在未来查看代码进化的过程,如下面的代码:
#include <iostream> #include <boost/bind.hpp> #include <boost/function.hpp> #include <boost/signals.hpp> using namespace std; typedef boost::signals::connection signal_connection; class signal_source : public boost::any { }; struct event_args { event_args(signal_source* src, signal_connection& conn):mSrc(src), mConn(conn), mFirst(true){} event_args(signal_source* src):mSrc(src), mFirst(false){} bool is_first(){return mFirst;} signal_connection mConn; signal_source* mSrc; bool mFirst; }; typedef boost::shared_ptr<event_args> event_args_ptr; class game_object : public signal_source { public: typedef boost::signal<void (event_args_ptr&)> signal_actor; enum SigTag { Sig_PosChanged = 0, Sig_PosChanging, Sig_DirChanged, Sig_DirChanging, Sig_Attacking, Sig_Detection, Sig_Sleep }; game_object(){} game_object(const game_object& val){} public: void push_slot(SigTag sigTag, const signal_actor::slot_type& callbackfn) { signal_actor& sig = get_actor(sigTag); // 连接时将会调用一次被连接上的函数,且将信号源和连接对象传回去。 event_args_ptr args(new event_args(this, sig.connect(callbackfn))); callbackfn.get_slot_function()(args); } void fire_signal(SigTag sigTag) { signal_actor& sig = get_actor(sigTag); sig(event_args_ptr(new event_args(this))); } protected: // 通过地址偏移来获取正确的信号对象,提高执行效率(必须在项目属性中将结构体顺序优化关闭,不过默认就是关闭的) signal_actor& get_actor(SigTag sigTag) { return *((&mSigPositionChanged) + sizeof(signal_actor)*sigTag); } protected: signal_actor mSigPositionChanged; signal_actor mSigPositionChanging; signal_actor mSigDirectionChanged; signal_actor mSigDirectionChanging; }; class monster : public game_object { public: //monster(){} protected: signal_actor mSigAttacking; signal_actor mSigDetection; signal_actor mSigSleep; }; class player : public game_object { public: //player(){} void onPositionChanged(event_args_ptr& args) { if (args->is_first()) { cout<<"连接成功,获取了连接对象"<<endl; } else { cout<<"获取到了PositionChanged消息"<<endl; } } }; int _tmain(int argc, _TCHAR* argv[]) { monster _monster; player _player; _monster.push_slot(monster::Sig_PosChanged, boost::bind(&player::onPositionChanged, _player, _1)); _monster.fire_signal(monster::Sig_PosChanged); return 0; }
从上面的代码中可以看到,我使用了boost::signals库进行消息间传递,这个库需要做一下编译才能正确使用。然后,我使用了monster和player两个类作为怪物和玩家两个游戏对象的虚拟,他们都继承自game_object类,而game_object类又继承自signal_source类,而为了将来可能的扩展情况,我又让signal_source继承自boost::any,这是一个万能变量,我们可以在未来的设计中将任意类型的游戏对象传递到any类型的参数上。但这也许是不必要的吧,总之先考虑万全是没坏处的。
这段代码中,monster的位置改变会通知player,player做的相应行为都写在player::onPositionChanged函数里。但是我在设计的过程中发现了一个情况,我们必须要在player中记录与monster的消息连接,因为boost::signals::connection会在连接的时候被boost::signal::connect()函数返回,这个connection掌握着该连接的生杀大权,必须放到player对象中保持起来才能得到有效控制。假设玩家在进入怪物的领域后,与怪物进行了交互,然后玩家逃离了怪物周围,这时候怪物的行为将不对玩家产生任何影响,所以消息连接就可以断开以节省资源。player的实例中必须记录了connection对象才行,所以,我设计代码时让连接后产生一个消息给指定玩家对象的成员函数,这时候,player::onPositionChanged函数的参数中有一个is_first()函数,用来检测该消息是否是连接后的第一次消息,如果是,则表示这是一个传回connection的消息,可以借此机会将传回的connection对象保持起来,否则就是一个普通的PositionChanged消息。与connection对象一同传回的还有消息源对象,这有利于分类保持并管理connection对象。
好了,现在要去保存connection对象,所以,我对player类做一些修改,新的类添加了connection的容器作为成员变量,但是,由于要存储connection的类何止是player一个呢?所以,我又把connection容器的声明移到了game_object中,新的game_object的声明如下:
class game_object : public signal_source { public: enum SigTag { Sig_PosChanged = 0, Sig_PosChanging, Sig_DirChanged, Sig_DirChanging, Sig_Attacking, Sig_Detection, Sig_Sleep }; struct connection_key { connection_key():_addrSrc(NULL),_tagSignal(-1){} connection_key(signal_source* src, game_object::SigTag sig):_addrSrc(src),_tagSignal(sig){} bool operator < (const connection_key& val) { if (this->_addrSrc != val._addrSrc) return (this->_addrSrc < val._addrSrc); return (this->_tagSignal < val._tagSignal); } bool operator == (const connection_key& val) { return ((this->_addrSrc != val._addrSrc) && (this->_tagSignal == val._tagSignal)); } signal_source* _addrSrc; game_object::SigTag _tagSignal; }; public: typedef boost::signal<void (event_args_ptr&)> signal_actor; typedef std::map<connection_key, boost::BOOST_SIGNALS_NAMESPACE::connection> connection_container; game_object(){} game_object(const game_object& val){} public: void push_slot(SigTag sigTag, const signal_actor::slot_type& callbackfn) { signal_actor& sig = get_actor(sigTag); // 连接时将会调用一次被连接上的函数,且将信号源和连接对象传回去。 event_args_ptr args(new event_args(this, sig.connect(callbackfn))); callbackfn.get_slot_function()(args); } void fire_signal(SigTag sigTag) { signal_actor& sig = get_actor(sigTag); sig(event_args_ptr(new event_args(this))); } protected: // 通过地址偏移来获取正确的信号对象,提高执行效率(必须在项目属性中将结构体顺序优化关闭,不过默认就是关闭的) signal_actor& get_actor(SigTag sigTag) { return *((&mSigPositionChanged) + sizeof(signal_actor)*sigTag); } protected: signal_actor mSigPositionChanged; signal_actor mSigPositionChanging; signal_actor mSigDirectionChanged; signal_actor mSigDirectionChanging; signal_actor mSigAttacking; signal_actor mSigDetection; signal_actor mSigSleep; connection_container mConnections; };
新的game_object类多了一个struct定义,一个typedef,一个成员变量
我们先来看看这个struct的定义:
struct connection_key { connection_key():_addrSrc(NULL),_tagSignal(-1){} connection_key(signal_source* src, game_object::SigTag sig):_addrSrc(src),_tagSignal(sig){} bool operator < (const connection_key& val) { if (this->_addrSrc != val._addrSrc) return (this->_addrSrc < val._addrSrc); return (this->_tagSignal < val._tagSignal); } bool operator == (const connection_key& val) { return ((this->_addrSrc != val._addrSrc) && (this->_tagSignal == val._tagSignal)); } signal_source* _addrSrc; game_object::SigTag _tagSignal; };
这是一个std::map的key值类型,因为我们要用map来存储connection的缘故,为了便于查找。键值包含了两个值,一个是连接消息源对象的地址,另一个是监听的消息类型,这两个元素唯一标识了一个connection,方便以后在代码和脚本中查找连接并做相应处理。
再来看看为了可读性而定义的typedef:
typedef std::map<connection_ley, boost::BOOST_SIGNALS_NAMESPACE::connection> connection_container;
最后是成员变量:
connection_container mConnections;
当然,以上这些设计均不保证在未来不做变更。
好了,现在我们可以修改一下player::onPositionChanged的代码来让新修改的机能发挥作用:
void onPositionChanged(event_args_ptr& args) { if (args->is_first()) { mConnections[connection_key(args->mSrc, SigTag::Sig_PosChanged)] = args->mConn; cout<<"连接成功,获取了连接对象"<<endl; } else { cout<<"获取到了PositionChanged消息"<<endl; } }
就目前来看,这还算是一个很漂亮的方法,但在后面的设计中,这里还要做很多的改动。
下面,有些消息模式设计经验的人都会注意到,我的代码中一个很蹩脚的地方,就是在event_args类中并没有记录事件的类别。对的,类别本身并没有去记录,于是player::onPositionChanged得到的参数args中也没有办法确认这到底是不是PostionChanged消息。那么,既然函数命名是onPositionChanged,则我们必然会去处理PostionChanged消息了,似乎没有确认消息类型的必要。
但是,一个不争得事实就是:函数名和函数会干什么并没有逻辑和编译器法规上的约定。
命名是随意的,这不能成为不去检测消息类型的借口。更重要的是,我们可能会在代码中用一个函数去处理多个消息类型。譬如,我想将怪物所做的一切位移行为都用一个函数来做处理,这个函数命名为:player::onMonsterCoordValueChanged(),它将处理PositionChanged、PositionChanging、DirectionChanged、DirectionChanging这4种消息。我们对event_args类和player类稍作修改:
1、 为event_args添加一个成员变量:int mSignalTag,新的类如下:
struct event_args { event_args(int tagSig, signal_source* src, signal_connection& conn):mSrc(src), mConn(conn), mSignalTag(tagSig), mFirst(true){} event_args(int tagSig, signal_source* src):mSrc(src), mSignalTag(tagSig), mFirst(false){} bool is_first(){return mFirst;} signal_connection mConn; signal_source* mSrc; int mSignalTag; bool mFirst; };
2、 player类的onPositionChanged函数名改为onMonsterCoordValueChanged
3、 Main函数中的onPositionChanged函数名参数改为onMonsterCoordValueChanged,且添加一行,具体如下:
int _tmain(int argc, _TCHAR* argv[]) { monster _monster; player _player; _monster.push_slot(monster::Sig_PosChanging, boost::bind(&player::onMonsterCoordValueChanged, _player, _1)); _monster.push_slot(monster::Sig_PosChanged, boost::bind(&player::onMonsterCoordValueChanged, _player, _1)); _monster.fire_signal(monster::Sig_PosChanged); _monster.fire_signal(monster::Sig_PosChanging); return 0; }
4、 改动player:: onMonsterCoordValueChanged的实现,具体如下:
void onMonsterCoordValueChanged(event_args_ptr& args) { if (args->is_first()) { // 较为关键的修改 mConnections[connection_key(args->mSrc, static_cast<SigTag>(args->mSignalTag))] = args->mConn; cout<<"连接成功,获取了连接对象"<<endl; } else { switch(args->mSignalTag) { case Sig_PosChanged: cout<<"获取到了Sig_PosChanged消息"<<endl; break; case Sig_PosChanging: cout<<"获取到了Sig_PosChanging消息"<<endl; break; case Sig_DirChanged: cout<<"获取到了Sig_DirChanged消息"<<endl; break; case Sig_DirChanging: cout<<"获取到了Sig_DirChanging消息"<<endl; break; } } }
5、 改变game_object类中的push_slot和fire_signal两个函数的实现,如下:
6、 我们还需要改动一下game_object::get_actor函数,因为它太过危险,事实也是如此,我在运行时出现了错误,因为字节对齐的关系,我的算法失误了,新的函数如下:
signal_actor& get_actor(SigTag sigTag) { switch(sigTag) { case Sig_PosChanged: return mSigPositionChanged; case Sig_PosChanging: return mSigPositionChanging; case Sig_DirChanged: return mSigDirectionChanged; case Sig_DirChanging: return mSigDirectionChanging; } }
新的代码完成了,看起来比以前要好了很多啊,下面我们继续修改。
相关文章推荐
- 尝试游戏对象的建模(4)——游戏对象的代理模式(网络游戏)
- 尝试游戏对象的建模(2)——游戏对象属性化
- 尝试游戏对象的建模(3)——游戏对象与脚本
- 【转载】PowerDesigner中的对象与关系映射建模
- 面向对象建模依赖、关联、聚合以及组合关系区别小结
- 【Unity常识】游戏对象及脚本的状态与Start等函数的调用关系
- Genesis-3D学习手册——7.游戏对象和组件的关系
- Unity3D入门基础之游戏对象 (GameObject) 和组件 (Component) 的关系
- PowerDesigner中的对象与关系映射建模
- PowerDesigner中的对象与关系映射建模
- 对象类[置顶] 游戏开发技术总结(经典之作)第六集 穿越丛林-----游戏角色的角色遮挡(前后关系)
- PowerDesigner中的对象与关系映射建模
- Unity3D入门基础之游戏对象 (GameObject) 和组件 (Component) 的关系
- PowerDesigner中的对象与关系映射建模
- PowerDesigner中的对象与关系映射建模
- C#类型、对象、线程栈、托管堆在CLR中的关系
- 使用对象-关系映射(ORM)系统中间件提升软件开发效率及质量
- 对象的继承关系在数据库中的实现方式和PowerDesigner设计
- Building Coder(Revit 二次开发) - 对象关系
- hibernate 对象 - 关系映射