我正在尝试编写一个简单的RPG.到目前为止,每次我尝试启动它立即变得一团糟,我不知道如何组织任何东西.所以我重新开始,试图构建一个基本上是MVC框架的新结构.我的应用程序在Controller中开始执行,它将创建View和Model.然后它将进入游戏循环,游戏循环的第一步是收集用户输入.
用户输入将由View的一部分收集,因为它可以变化(3D View将直接轮询用户输入,而远程View可能通过telnet连接接收它,或者命令行视图将使用System.in ).输入将被转换为消息,每个消息将被提供给Controller(通过方法调用),然后可以解释消息以修改模型数据,或通过网络发送数据(因为我希望有一个网络选项) .
在联网游戏的情况下,该消息处理技术也可用于处理网络消息.到目前为止,我是否保持MVC的精神?
无论如何我的问题是,表示这些消息的最佳方式是什么?
这是一个用例,每条消息都用斜体显示:假设用户启动游戏并选择角色2.然后用户移动到坐标(5,2).然后他说公开聊天,"嗨!" .然后他选择保存并退出.
视图应该如何将这些消息包装成控制器可以理解的内容?或者您认为我应该有单独的控制器方法,如chooseCharacter(),moveCharacterTo(),publicChat()?当我转向网络游戏时,我不确定这种简单的实现是否有效.但在极端的另一端,我不想只是向控制器发送字符串.这很难,因为choose-character动作需要一个整数,move-to需要两个整数,聊天需要一个字符串(和一个范围(公共私有全局),在私有情况下,一个目标用户); 这一切都没有真正的设置数据类型.
任何一般性的建议都非常受欢迎; 我在合适的时间担心这件事吗?我是否正确地走向一条布局合理的MVC应用程序?有什么我忘了吗?
谢谢!
(免责声明:我从来没有用Java编写游戏,只能用C++编写.但是一般的想法也应该适用于Java.我提出的想法不是我自己的想法,而是我在书中或"在互联网上找到的混合解决方案" ",请参阅参考资料部分.我自己使用了这一切,到目前为止,它产生了一个简洁的设计,我知道在哪里放置我添加的新功能.)
我担心这将是一个很长的答案,第一次阅读时可能不太清楚,因为我无法自上而下地描述它,所以会有来回的参考,这是由于我的缺乏解释技巧,不是因为设计有缺陷.事后来看,我过度了,甚至可能偏离主题.但既然我已经写完了所有这些,我就不能把它扔掉.只要问一下有什么不清楚的地方.
在开始设计任何包和类之前,先从分析开始.您希望在游戏中拥有哪些功能.不要计划"也许我稍后会添加",因为在您开始认真添加此功能之前,几乎可以肯定您做出的设计决策,您计划的存根将是不够的.
而对于动机,我从这里的经验说起,不要把你的任务想象为编写游戏引擎,编写游戏!无论你如何思考未来的项目会有什么好处,除非你把它放在你正在写的游戏中,否则拒绝它.没有未经测试的死代码,没有动力问题,因为无法解决甚至不是即将发生的项目的问题.没有完美的设计,但有一个足够好.值得记住这一点.
如上所述,我不相信MVC在设计游戏时有任何用处.模型/视图分离不是问题,并且控制器的东西非常复杂,太多以至于被称为"控制器".如果你想拥有名为model,view,control的子包,请继续.以下内容可以整合到这种包装方案中,但其他方案至少同样合理.
很难找到我的解决方案的起点,所以我只是从最顶层开始:
在主程序中,我只是创建Application对象,初始化它并启动它.应用程序init()
将创建功能服务器(见下文)并进入它们.此外,第一个游戏状态被创建并推送到顶部.(见下文)
功能服务器封装正交游戏功能.这些可以独立实现,并通过消息松散耦合.示例功能:声音,视觉表示,碰撞检测,人工智能/决策,物理等.功能本身的组织方式如下所述.
游戏状态提供了一种组织输入控制的方法.我通常有一个类来收集输入事件或捕获输入状态并稍后轮询它(InputServer/InputManager).如果使用基于事件的方法,则将事件给予单个注册的活动游戏状态.
当开始游戏时,这将是主菜单游戏状态.游戏状态具有init/destroy
和resume/suspend
功能.Init()
将初始化游戏状态,在主菜单的情况下,它将显示最顶层的菜单级别.Resume()
将控制此状态,它现在从InputServer获取输入.Suspend()
将从屏幕清除菜单视图,并destroy()
释放主菜单所需的任何资源.
GameStates可以堆叠,当用户使用"新游戏"选项启动游戏时,MainMenu游戏状态将被暂停,PlayerControlGameState将被放入堆栈,现在接收输入事件.这样您就可以根据游戏状态处理输入.只需在任何给定时间激活一个控制器,即可极大地简化控制流程.
输入集合由游戏循环触发.游戏循环基本上确定当前循环的帧时间,更新特征服务器,收集输入并更新游戏状态.帧时间或者给予每个帧的更新功能,或者由Timer单例提供.这是用于确定自上次更新呼叫以来的持续时间的规范时间.
这种设计的核心是游戏对象和功能的互动.如上所示,这种意义上的特征是可以彼此独立地实现的游戏功能.游戏对象是以任何方式与玩家或任何其他游戏对象交互的任何东西.示例:玩家头像本身是游戏对象.火炬是游戏对象,NPC是游戏对象,照明区域和声源或这些的任何组合.
传统的RPG游戏对象是一些复杂的类层次结构的顶级类,但实际上这种方法是错误的.许多正交方面不能放入层次结构中,即使最后使用接口,也必须具有具体的类.项目是游戏对象,可选项目是游戏对象,胸部是容器是项目,但是使用这种方法可以选择或不选择胸部,因为您必须拥有一个层次结构.当你想要一个只有在谜语被回答时打开的说话魔术谜语时,它会变得更加复杂.没有人适合所有人.
更好的方法是只有一个游戏对象类,并将每个正交方面(通常在类层次结构中表示)放入其自己的组件/要素类中.游戏对象可以容纳其他物品吗?然后将ContainerFeature添加到它,它可以说话,将TalkTargetFeature添加到它,等等.
在我的设计中,GameObject只有一个内在的唯一id,name和location属性,其他所有东西都被添加为一个特征组件.可以通过调用addComponent(),removeComponent()在运行时通过GameObject接口添加组件.因此,要使其可见,请添加VisibleComponent,使其发出声音,添加AudableComponent,使其成为容器,添加ContainerComponent.
VisibleComponent对您的问题很重要,因为这是提供模型和视图之间链接的类.并非一切都需要经典意义上的观点.触发区域将不可见,环境声区也不会.只有具有VisibleComponent的游戏对象才可见.当VisibleFeatureServer更新时,可视表示在主循环中更新.然后根据注册到它的VisibleComponents更新视图.它是查询每个状态还是只是从它们接收的队列消息取决于您的应用程序和底层可视化库.
在我的情况下,我使用Ogre3D.这里,当VisibleComponent附加到游戏对象时,它会创建一个SceneNode,它附加到场景图,并创建一个Entity(3d网格的表示).立即处理每个TransformMessage(见下文).然后,VisibleFeatureServer使Ogre3d将场景重绘为RenderWindow(实质上,细节更复杂,一如既往)
那么这些功能和游戏状态以及游戏对象如何相互通信呢?通过消息.此设计中的消息只是Message类的任何子类.每个具体的Message都可以拥有自己的界面,方便其任务.
消息可以从一个GameObject发送到其他GameObject,从GameObject发送到其组件,从FeatureServers发送到它们负责的组件.
创建FeatureComponent并将其添加到游戏对象时,它会通过为要接收的每条消息调用myGameObject.registerMessageHandler(this,MessageID)将自身注册到游戏对象.它还将自己注册到其功能服务器,以获取它希望从那里接收的每条消息.
如果玩家试图与其焦点中的角色交谈,那么用户将以某种方式触发谈话动作.例如:如果焦点中的焦点是友好的NPC,则通过按下鼠标按钮触发标准交互.通过向其发送GetStandardActionMessage来查询目标游戏对象标准操作.目标游戏对象接收该消息,并且从第一个注册的消息开始,通知其想要了解该消息的特征组件.然后,此消息的第一个组件将标准操作设置为将触发自身的操作(TalkTargetComponent将标准操作设置为Talk,它将首先接收它.)然后将消息标记为已消耗.GameObject将测试消耗并看到它确实消耗并返回给调用者.然后评估现在修改的消息并调用结果操作
是的,这个例子似乎很复杂,但它已经是一个比较复杂的了.其他像TransformMessage用于通知位置和方向变化更容易处理.TransformMassage对许多功能服务器都很有用.VisualisationServer需要它来更新GameObject在屏幕上的可视化表示.SoundServer更新3d声音位置等.
使用消息而不是调用方法的优点应该是清楚的.组件之间的耦合较低.调用方法时,调用者需要知道被调用者.但是通过使用消息,这完全是分离的.如果没有接收器,则没关系.此外,接收者如何处理消息,如果根本不是呼叫者的关注.也许委托是一个很好的选择,但Java错过了一个干净的实现,在网络游戏的情况下,你需要使用某种RPC,它具有相当高的延迟.低延迟对于交互式游戏至关重要.
这使我们了解如何通过网络传递消息.通过将GameObject/Feature交互封装到消息中,我们只需要担心如何通过网络传递消息.理想情况下,您将消息带入通用表单并将其放入UDP包并发送.接收器将消息解包到适当类的实例,并将其通告给接收器或广播它,具体取决于消息.我不知道Java的内置序列化是否能胜任这项任务.但即使不是,也有很多lib可以做到这一点.
GameObjects和组件通过属性使其持久状态可用(C++没有内置的Serialization.)它们具有类似于Java中的PropertyBag的接口,可以使用它来检索和恢复其状态.
脑转储:专业游戏开发者的博客.也是开源Nebula引擎的作者,这是一种用于商业成功游戏的游戏引擎.我在这里介绍的大多数设计都来自星云的应用层.
上面博客上值得注意的文章,它列出了引擎的应用层.我试图在上面描述的另一个角度.
关于如何布局游戏架构的冗长讨论.主要是食人魔,但一般也足以对其他人有用.
基于组件的设计的另一个论点,底部有用的参考.