|
|
一个基于OGRE图形引擎的高度面向对象的游戏结构
0.前言
其实在此之前真正的游戏我只写过一个的,就是一个叫《紫星村》的。那个游戏情节很缩水的,策划让我增加情节,我就说不行了,因为结构太差了,里面有2000行的switch啊。
当然任何有一点点制作经验的人都会知道这个switch应该用类似状态机这样的更好的方式来实现,在这个事情之后,我就没敢再写过游戏,我一边学习OGRE图形引擎,一面努力的学习游戏结构,这样的文章也真不少,光《游戏编程精粹》中就有《一个游戏实体工厂》《一个基于对象组合的游戏构架》《一个针对巨量多玩家游戏的灵活的仿真框架》等(都是中文版游戏编程精粹2&3中的文章),网上文章也很多。不过或者太复杂或者太简单,或者用来维护任务而没有消息驱动,或者太底层连内存池都要管理。
去其糟粕取其精华,我们需要一个高度面向对象,消息驱动,拥有有限状态机的结构,能很好的结合OGRE以及其他引擎,能够应用于绝大部分类型游戏,可以支持游戏储存/读取(对象序列化)的结构。我尝试写了一个这样的结构,不敢乱吹牛,不过本人用起来还是满爽的。
1.总体
介绍一下这个结构的思路。
游戏中所有可以活动的物体被称为角色(Actor),角色首先是一个有限状态机,所以他可以有简单的智能。有限状态机能执行瞬时动作或者注册持续动作给角色,角色能维护一些持续的动作(比如寻径)。角色之间可以通过传递事件消息进行交流。
角色们共同存在的空间被称为世界(World),这个概念很广泛,比如一个装载很多物品的背包就是一个物品角色的世界,而背包在更大的世界中却是一个角色,最底层的世界用来维护场景地图(比如BSP地图)。当角色执行某个动作的时候可以对世界广播这个事件,在收听这个广播者角色频道的所有角色都能得到这个事件,当然世界本身也可以向其中的角色广播事件。
写这个程序的时候,我很认真的规范了风格,并生成了相应的文档,下面提到的任何事情,您都可以在本文提供的文档中找到对应的介绍和代码实现。
2.角色的实现
白色的Actor_t(“_t”代表接口)就是角色的接口,当然我们希望在接口层不依赖引擎的实现,是一个绝对的抽象,在角色接口类Actor_t类下面的是游戏具体实现的角色(节点管理角色和表层管理角色,节点管理角色又分成摄像机管理等等),而角色接口类Actor_t本身是一种有限状态机(FSM_t),有限状态机又是一种可以储存对象(SaveObj_t),可以储存对象拥有字符串接口(OGRE::SringInterface)。
OGRE::SringInterface:
这个是OGRE引擎提供的一个可以把类中的属性和字符串互相转换的接口。我们不是说接口层要脱离引擎么,怎么又使用了这个OGRE的东西呢。看了OGRE的源码您就清楚了,这个接口抽象得很漂亮,和其他的引擎的类没有什么依赖的,我们只把这个接口当作大师实现的对象序列化策略就可以了。
SaveObj_t:
在我的源代码中预留了
void SaveObj_t::load ( Std::istream & is ) [virtual]
void SaveObj_t::save ( Std: stream & os ) [virtual]
这两个读取和储存的接口,并没有具体实现,但是只要有了:SringInterface接口,只要写进去您的储存策略就好了。
FSM_t:
有限状态机的实现。
公有成员
FSM_t ()
virtual ~FSM_t ()
virtual void postEvent (Event_t * evt)
FSM_t::postEvent( Event_t * evt ) 得到事件.
virtual bool exect ()
exect() 处理下一个事件.
void exectAll () 处理所有事件
virtual void addState (State_t * state) 增加新状态
virtual bool setState (std::string stateName) 设置当前状态
在这里我们把提交事件和处理事件做成了异步处理,这样不仅可以提高松散耦合,更能在以后的改进中用更加灵活的策略处理事件(考虑多线程)。
Actor_t:
游戏世界中的角色
公有成员
Actor_t (std::string name)
virtual ~Actor_t ()
const std::string& getName () 得到角色名字
virtual void update ( float timeElapsed )
更新角色.
virtual void goTo ( World_t * world )
记录进入世界.
virtual void escape ()
退出世界.
virtual void registerAct ( Active_t * act )
向角色注册动作(就是角色的任务).
virtual void addBaseParameters (void)
增加参数.
World_t* getWorld (void) 得到角色所在世界的指针
AddBaseParameters是实现SringInterface中的接口。
(以上代码情参看相应文档和代码)
3.消息事件的实现:
Event_t类:
公有成员
virtual std::string& getTypeID () 得到消息类型
Event_t (std::string & id) 构造消息
virtual Event_t* Clone () 用克隆模式复制消息
4.动作类的实现:
角色负责执行那些动作和维护那些持续的动作,而真正让角色活动起来的对象就是形形色色的动作了。
Active_t:
公有成员
Active_t ( Actor_t * actor)
Virtual ~Active_t ()
virtual void exect ( Event_t * evt = NULL )
执行或注册动作.
virtual void run ( float timeElapsed )
运行动作.
virtual void stop ()
停止动作.
virtual bool isRun ()
是否运行.
如果时瞬时动作就只执行exect,如果是持续动作就在exect中向角色注册这个动作,角色通过调用run来运行动作,当不运行时候执行stop结束。
Actives:
一个复合动作,通过
void addAct (Active_t * act)
增加子动作,exect执行所有子动作,isRun返回是否有一个或超过一个子动作在执行。
OgreExitActive:
退出游戏动作,作为一个简单的例子这里给出源码。这个和OGRE运行机制相关的,用过OGRE的人能看懂吧。
00012 class OgreExitActive : public Active_t , public FrameListener
00013 {
00014 public:
00015 OgreExitActive():Active_t(NULL){}
00016 virtual ~OgreExitActive(){}
00017 bool frameStarted(const FrameEvent& evt){return true;}
00018 bool frameEnded(const FrameEvent& evt){return false; }
00019 void exect(Event_t *evt = 0 )
00020 {
00021 Root::getSingleton().addFrameListener(this);
00022 }
00023
00024 };
对于不同的动作,大致划分为三类,第一钟是引擎无关通用,比如广播事件动作和上面的复合动作,第二种是引擎相关通用,比如上面提到的退出游戏,第三钟是游戏相关,比如我例子中的射击动作,第三种动作要分别为每个游戏编码。其它动作请参看相应源代码和文档。
5.状态:
有状态机就要有状态。
State_t:
公有成员
State_t ( std::string name , Active_t * beginAct = NULL , Active_t * endAct = NULL ,FSMGuardCondition_t * FSMgc =NULL)参数为:状态名,开始动作,结束动作,当执行这个状态时的判定类
virtual ~State_t ()
const std::string& getName ()
virtual void addEvent (std::string eventID, Active_t * act , std::string nextStateName)参数为 增加事件,对应动作,以及下一个状态,当对应动作为NULL时认为不执行动作,当下一状态为空字符串时认为本事件执行内部迁移。
virtual const std::string postEvent (Event_t * evt)
为状态提交事件.
virtual void init () 执行进入动作
virtual void shutdown () 执行退出动作 并停止本状态所有持续动作
这个有限状态机和状态的实现参照了《程序员》中的一篇关于有限状态机的文章。
6.世界的实现
TautoMap是参照《游戏编程精粹3》中自动列表模式实现的,自动为每一个继承它的类实现一个维护实例的字典(std::map)。这样便于通过世界的名字标识对世界进行管理。
World_t:
公有成员
World_t ( std::string name )
Virtual ~World_t ()
世界毁灭之时 世间元素皆消亡
virtual void addListener ( std::string channel , Actor_t * actor )
订阅频道,有关消息传递
virtual void removeListener ( std::string channel , Actor_t * actor = NULL )
取消订阅. 有关消息传递
virtual void broadcasting ( Actor_t * announcer , Event_t * evt )
广播的事件. 有关消息传递
virtual void broadcasting ( Event_t * evt )
向世界广播的事件. 有关消息传递
virtual bool broadcasting ( Event_t * evt , std::string listenerName )
通告的事件. 有关消息传递
virtual void enable ()
激活世界.
virtual void disable ()
关闭世界..
bool isEnable () 检查世界是否活动
const std::string& getName ()
virtual void comeIn ( Actor_t * actor )
角色进入世界.
virtual Actor_t* goOut ( std::string actName )
角色退出世界.
float getSimulationStepSize (void)
void setSimulationStepSize (float step)
virtual void simulationStep (float timeElapsed)
Performs a simulation step( 安步更新世界 ).
其中很重要的就是世界维护了角色们的消息传递,其中一种方式就是,角色向世界订阅要接受的频道,当这个频道产生消息时候,世界将向所有收听这个频道的角色发送消息。这是类似监听者的一种模式,但是更加松散耦合一些,不过相应也丧失了一点点效率的代价。
BspWorld:
实现了关于维护Bsp场景的世界,是一个和OGRE引擎紧密相连的实现,如果进入对象是OGRE的对象,便挂接到渲染树上(需要RTTI来识别进入类型)。
7.结语
以上便是这个框架的特色菜,至于GameApp类是这个游戏的实例,FactorySys_t是具体实现系统的类,OGRE演示程序框架中的ExampleApplication被分解到GameApp类中,ExampleFrameListener类被取消,由每个动作自己维护自己监听的动作。我用这个框架实现了一个简单的游戏演示,请参看源码。
8.未完善部分
音频的实现,我准备不依赖于角色,虽然感觉好像是某个角色发出的声音,但是如果是拍桌子这样的动作,到底是手发出的声音还是桌子呢?答案是共同发出的,所以声音不能依赖于某个角色,而是声音本来在世界中存在,手和桌子作用结果调用了这个世界的声音,我准备仍然实现一个自动列表模式(继承AutoMap)来实现声音系统。
脚本的实现,因为这个游戏框架十分灵活,可以用一个工厂模式和builder模式(生成器模式)来创造整个游戏,而通过分析脚本来执行这两个模式都是很简单的。关键是定义一个完善的脚本。
网络部分,还没有具体应用,给我一段时间,我会结合相关的文章进行考虑的。
世界管理器,在我写的例子中只有一个地图,也就是只有一个底层的世界,要是线多地图切换,应该实现一个底层世界管理器。
采用框架游戏的开发过程应该是,设计游戏,收集相关资源(图片,3D模型,声音等),编写游戏特有的角色和动作,通过脚本并结合通用的角色和动作来实现整个逻辑。比如A*这种寻径动作可以写到一个通用的动作中,当需要扩展算法时可以继承这个动作实现新的动作,在脚本中替换这个动作就可以了。
在我实现的例子里,还没有用到脚本,也便硬编码了整个结构,下面就给出整个游戏的主要部分。
00103 virtual void createScene(void)
00104 {
00105 //系统灯光设置区
00106 mSceneMgr->setAmbientLight(ColourValue(0.5, 0.5, 0.5));
00107 Light* l = mSceneMgr->createLight("MainLight");
00108 l->setPosition(20,80,50);
00109
00110 //激活世界(之后角色才可以进入)
00111 mWorld->enable();
00112
00113 //变量声明区
00114 OgreEntityCameraActor * cameraActor;//一个挂接物件和摄像机的节点角色指针
00115 OgreNodePrexyActor * proxyCameraActor;//一个挂接其他节点的节点角色指针,并代理其他角色动作.
00116 OgreOverlaysActor * overlaysActor;//一个“界面”角色指针,负责维护很多Overlay,在这个游戏中负责全部
00117 State_t * state;//一个状态的指针
00118 Overlay* o;//一个界面的指针
00119 Actives * acts;//一个动作群指针 负责和并一群动作成为一个动作添加到状态中.
00120 AnimationState * animState;
00121
00122 //物件摄像机节点角色
00123 cameraActor = new OgreEntityCameraActor("Camera" , mSceneMgr,"mp5.mesh");//名字Camera,读取mp5.mesh模型
00124 //设置摄像机和模型相对位置
00125 animState=cameraActor->getEntity()->getAnimationState("idle1");
00126 animState->setEnabled(true);
00127 cameraActor->getCamera()->yaw(190.0f);
00128 cameraActor->getCamera()->pitch(-2.0f);
00129 cameraActor->getCamera()->setPosition(Vector3(-18,8,-8));
00130 //cameraActor->getCamera()->setPosition(Vector3(0,10,-10));
00131
00132 mCamera = cameraActor->getCamera();//系统摄像机
00133 //建立完摄像机可以建立视口了
00134 createViewports();
00135 //这个也是系统设置
00136 TextureManager::getSingleton().setDefaultNumMipMaps(5);
00137 createEventProcessor();
00138 //摄像机设置
00139 cameraActor->getCamera()->setNearClipDistance(4);
00140 cameraActor->getCamera()->setFarClipDistance(4000);
00141
00142 //给摄像机添加状态
00143 //Camera状态,进入状态时候执行可以用鼠标控制Y轴的动作
00144 state = new State_t("Camera", new MoveOgreNodeActive( cameraActor ,false,true) );
00145 //增加这个状态的事件,当得到reload消息时候,执行播放动画reload动作;
00146 state->addEvent("reload",new OgreAnimationActive(cameraActor ,cameraActor->getEntity()->getAnimationState("reload"),animState),"");
00147 state->addEvent("anim/stop/reload",NULL,"Shoot");
00148
00149 cameraActor->addState( state );//添加这个状态
00150 cameraActor->setState("Camera");//设置这个状态为初始状态
00151
00152 //增加射击状态,
00153 acts = new Actives;
00154 acts->addAct(new MoveOgreNodeActive( cameraActor ,false,true));
00155 acts->addAct(new GameShootActive(cameraActor,cameraActor->getEntity(),animState));
00156 acts->addAct(new PostEventActive( cameraActor ,new Event_t(std::string("Ready"))));
00157 state = new State_t("Shoot",acts);
00158 state->addEvent("NoBullet",NULL,"Camera");
00159 cameraActor->addState( state );//添加这个状态
00160
00161 //代理摄像机借点角色(主要功能是添加一个父节点)
00162 proxyCameraActor= new OgreNodePrexyActor("proxy/Camera",mSceneMgr,Vector3(0,10,0));//第三个参数是让这个副节点提高10单位,让人物站高点。
00163 proxyCameraActor->link(cameraActor);//连接摄像机角色
00164 //增加一个状态(可以用鼠标控制这个角色节点的X轴)
00165 proxyCameraActor->addState(new State_t("First", new MoveOgreNodeActive( proxyCameraActor ,true,false) ));
00166 //设置初始状态
00167 proxyCameraActor->setState("First");
00168
00169 //代理角色进入世界(因为他代理了摄像机角色,所以我们认为摄像机角色也一同进入了世界,所有摄像机角色的事情直接操作代理就好)
00170
00171 mWorld->addListener("Overlays",proxyCameraActor);
00172 mWorld->comeIn( proxyCameraActor );
00173
00174 //关于界面的系统设置
00175 //建立鼠标并初始化
00176 OverlayManager::getSingleton().createCursorOverlay();
00177 GuiContainer* pCursorGui = OverlayManager::getSingleton().getCursorGui();
00178 pCursorGui->setMaterialName("Cursor/default");
00179 pCursorGui->setDimensions(32.0/640.0, 32.0/480.0);
00180
00181
00182 //建立界面角色(名字)
00183 overlaysActor = new OgreOverlaysActor("Overlays");
00184
00185 //设置第一个状态(每个状态对应一个界面,状态名等同于界面名字)
00186 state = new State_t("KillMan/Overlay1");
00187 //在这个状态中 界面角色收听 "KillMan/ComeIn""KillMan/Exit"两个按键发放来的消息
00188 GuiManager::getSingleton().getGuiElement("KillMan/ComeIn")->addActionListener(overlaysActor);
00189 GuiManager::getSingleton().getGuiElement("KillMan/Exit")->addActionListener(overlaysActor);
00190
00191 state->addEvent("KillMan/Exit",new OgreExitActive(),"");//收到"KillMan/Exit"消息 执行退出动作
00192 state->addEvent("KillMan/ComeIn",NULL,"KillMan/Overlay2");//收到"KillMan/ComeIn"消息 进入下一个状态(界面)
00193 //添加这个状态对应的界面(请保证和状态同名的界面在校本中定义)
00194 o=(Overlay*)OverlayManager::getSingleton().getByName(state->getName());
00195 overlaysActor->addOverlay(o);
00196 overlaysActor->addState(state);//添加这个状态到角色
00197
00198 //设置第二个状态(进入动作是让一个GuiElement隐藏)
00199 state = new State_t("KillMan/Overlay2",new OgreStringInterfaceActive(GuiManager::getSingleton().getGuiElement("KillMan/ComeIn2"),"visible","false") );
00200 //在这个状态中 界面角色收听 "KillMan/Man""KillMan/Girl""KillMan/Woman""KillMan/ComeIn2"按键发放来的消息
00201 GuiManager::getSingleton().getGuiElement("KillMan/Man")->addActionListener(overlaysActor);
00202 GuiManager::getSingleton().getGuiElement("KillMan/Girl")->addActionListener(overlaysActor);
00203 GuiManager::getSingleton().getGuiElement("KillMan/Woman")->addActionListener(overlaysActor);
00204 GuiManager::getSingleton().getGuiElement("KillMan/ComeIn2")->addActionListener(overlaysActor);
00205
00206 //添加复合动作
00207 acts = new Actives;
00208 //更改贴图
00209 acts->addAct(new OgreStringInterfaceActive(GuiManager::getSingleton().getGuiElement("KillMan/MsgPanel"),"material","killMan/woman"));
00210 //显示按键
00211 acts->addAct(new OgreStringInterfaceActive(GuiManager::getSingleton().getGuiElement("KillMan/ComeIn2"),"visible","false"));
00212 //当得到"KillMan/Woman"事件时,执行这个复合动作
00213 state->addEvent("KillMan/Woman",acts,"");
00214
00215 //添加复合动作
00216 acts = new Actives;
00217 acts->addAct(new OgreStringInterfaceActive(GuiManager::getSingleton().getGuiElement("KillMan/MsgPanel"),"material","killMan/man"));
00218 acts->addAct(new OgreStringInterfaceActive(GuiManager::getSingleton().getGuiElement("KillMan/ComeIn2"),"visible","false"));
00219 //当得到"KillMan/Man"事件时,执行这个复合动作
00220 state->addEvent("KillMan/Man",acts,"");
00221
00222 //添加复合动作
00223 acts = new Actives;
00224 acts->addAct(new OgreStringInterfaceActive(GuiManager::getSingleton().getGuiElement("KillMan/MsgPanel"),"material","killMan/girl"));
00225 acts->addAct(new OgreStringInterfaceActive(GuiManager::getSingleton().getGuiElement("KillMan/ComeIn2"),"visible","true"));
00226 //当得到"KillMan/Girl"事件时,执行这个复合动作
00227 state->addEvent("KillMan/Girl",acts,"");
00228
00229 //当得到"KillMan/Overlay3"事件时,切换到 "KillMan/Overlay3"状态(界面)
00230 state->addEvent("KillMan/ComeIn2",NULL,"KillMan/Overlay3");
00231 //添加这个状态对应的界面
00232 o=(Overlay*)OverlayManager::getSingleton().getByName(state->getName());
00233 overlaysActor->addOverlay(o);
00234 //添加这个状态到角色
00235 overlaysActor->addState(state);
00236
00237 //设置第三个状态(进入动作是让鼠标指针隐藏)
00238 acts = new Actives;
00239 acts->addAct(new OgreStringInterfaceActive(OverlayManager::getSingleton().getCursorGui(),"visible","false"));
00240 acts->addAct(new PostEventActive(overlaysActor,new Event_t(std::string("reload"))));
00241 state = new State_t("KillMan/Overlay3",acts);
00242 acts = new Actives;
00243 acts->addAct(new OgreStringInterfaceActive(GuiManager::getSingleton().getGuiElement("KillMan/Face"),"material","killMan/girl2"));
00244 acts->addAct(new OgreStringInterfaceActive(GuiManager::getSingleton().getGuiElement("KillMan/Fire"),"material","killMan/fire2"));
00245 state->addEvent("ShootBegin",acts,"");
00246 acts = new Actives;
00247 acts->addAct(new OgreStringInterfaceActive(GuiManager::getSingleton().getGuiElement("KillMan/Face"),"material","killMan/girl1"));
00248 acts->addAct(new OgreStringInterfaceActive(GuiManager::getSingleton().getGuiElement("KillMan/Fire"),"material","killMan/fire"));
00249 state->addEvent("ShootOver",acts,"");
00250
00251 state->addEvent("Ready",new OgreStringInterfaceActive(GuiManager::getSingleton().getGuiElement("KillMan/Bullet"),"caption","60/60"),"");
00252
00253 acts = new Actives;
00254 acts->addAct(new GameBulletTextActive(overlaysActor,GuiManager::getSingleton().getGuiElement("KillMan/Bullet"),"caption","/60",60));
00255 acts->addAct(new GameTextsActive(overlaysActor,GuiManager::getSingleton().getGuiElement("KillMan/Text"),"caption"));
00256
00257 state->addEvent("Shoot",acts,"");
00258 state->addEvent("NoBullet",NULL,"KillMan/Overlay4");
00259
00260 mWorld->addListener("Camera",overlaysActor);
00261 mWorld->addListener(overlaysActor->getName(),overlaysActor);
00262
00263
00264
00265
00266 //添加相应的界面
00267 o=(Overlay*)OverlayManager::getSingleton().getByName(state->getName());
00268 overlaysActor->addOverlay(o);
00269
00270 //添加这个状态到角色
00271 overlaysActor->addState(state);
00272
00273
00274 //设置第四个状态
00275 state = new State_t("KillMan/Overlay4",
00276 new OgreStringInterfaceActive(OverlayManager::getSingleton().getCursorGui(),"visible","true"));
00277 state->addEvent("KillMan/GameOver",new OgreExitActive(),"");
00278 GuiManager::getSingleton().getGuiElement("KillMan/GameOver")->addActionListener(overlaysActor);
00279
00280 o=(Overlay*)OverlayManager::getSingleton().getByName(state->getName());
00281 overlaysActor->addOverlay(o);
00282 overlaysActor->addState(state);
00283
00284 //设置"KillMan/Overlay1"状态(界面)为初始状态(界面)
00285 overlaysActor->setState("KillMan/Overlay1");
00286 //这个界面角色进入世界
00287 mWorld->comeIn( overlaysActor );
00288
00289 }
你会在演示程序中看到几个界面,会变化的脸谱,一个会上子弹的手,和射击动作。你会看到整个结构非常清晰,很适合用脚本来编写。希望这个结构能给您即将开发的游戏有所帮助,这个游戏结构和演示的源码和文档和编译后的程序其实更详尽,您也可以直接拿来使用。
n.另外
我发现源码中有一些单词拼错了,我现在人在上海,手上没有能编译这个程序的机器,所以大家就将就一下吧。
关于OGRE中文输入的文章我会一起给大家发上来的。这个文章也很简陋,希望大家能看一下与之相关的文档。
|
|