游戏开发论坛

 找回密码
 立即注册
搜索
查看: 2322|回复: 2

相投搞 附件没地方传

[复制链接]

193

主题

870

帖子

903

积分

高级会员

Rank: 4

积分
903
QQ
发表于 2004-10-25 23:31:00 | 显示全部楼层 |阅读模式
一个基于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中文输入的文章我会一起给大家发上来的。这个文章也很简陋,希望大家能看一下与之相关的文档。

193

主题

870

帖子

903

积分

高级会员

Rank: 4

积分
903
QQ
 楼主| 发表于 2004-10-25 23:34:00 | 显示全部楼层

Re: 相投搞 附件没地方传

还有
让OGRE支持中文3——中文输入
也完成了
不过因为太大 贴不上来

还有些事情想和站长商量 站长一般什么时候上QQ啊

1万

主题

1万

帖子

2万

积分

管理员

中级会员

Rank: 9Rank: 9Rank: 9

积分
20686
发表于 2004-10-26 09:06:00 | 显示全部楼层

Re:相投搞 附件没地方传

好的,你白天上线的时候给我QQ发个消息。

另外,收到你在QQ的留言,基本上没什么问题。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

作品发布|文章投稿|广告合作|关于本站|游戏开发论坛 ( 闽ICP备17032699号-3 )

GMT+8, 2025-12-22 23:03

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

快速回复 返回顶部 返回列表