程序中消息传递的数据结构?

我试图写一个简单的RPG。 到目前为止,每次我尝试开始时,它立即变得混乱,我不知道如何组织任何东西。 所以我重新开始,试图创build一个基本上是MVC框架的新结构。 我的应用程序开始执行在控制器,它将创build视图和模型。 然后进入游戏循环,游戏循环的第一步是收集用户input。

用户input将被视图的一部分收集,因为它可能会有所不同(3D视图将直接轮询用户input,而远程视图可能通过telnet连接接收它,或者命令行视图将使用System.in )。 input将被转换成消息,并且每个消息将被给予控制器(通过方法调用),然后可以解释该消息以修改模型数据,或通过networking发送数据(因为我希望有一个networking选项) 。

这种消息处理技术也可以在networking游戏中用于处理networking消息。 我保持MVC的精神到目前为止?

无论如何,我的问题是,代表这些消息的最佳方式是什么?

这里是一个用例,每条信息用斜体表示:假设用户启动游戏并select字符2 。 然后用户移动到坐标(5,2) 。 然后他对公众聊天说:“嗨! 。 然后他select保存并退出

该视图应该如何将这些消息整理成控制器可以理解的东西? 或者你认为我应该有像chooseCharacter(),moveCharacterTo(),publicChat()? 当我转向networking游戏时,我不确定这样简单的实现是否可行。 但在极端的另一端,我不想只发送string到控制器。 这很困难,因为select字符操作需要一个整数,移动需要两个整数,聊天需要一个string(和一个范围(公共私人全局),在私人,目的地用户的情况下)。 没有真正的设置数据types。

任何一般的build议都非常受欢迎。 我在适当的时候担心这个吗? 我是否走上了一条精心布置的MVC应用程序的正确道路? 有什么我忘记了吗?

谢谢!

(免责声明:我从来没有用Java编写游戏,只用C ++编写游戏,但是总的想法也应该适用于Java,我提出的想法不是我自己的,而是我在书籍或互联网上find的解决scheme的混搭“,请参阅参考资料部分,我自己使用了所有这些,并且到目前为止,这样做的结果是一个干净的devise,我确切知道在哪里添加新function。

恐怕这是一个很长的回答,第一次阅读可能不是很清楚,因为我不能很好地描述它,所以会有来回的引用,这是由于我的缺乏解释技巧,不是因为devise有缺陷。 事后看来,我超越了,甚至可能是无关紧要的。 但是现在我已经写完了这些,我不能把自己扔掉。 只要问一下有什么不清楚的地方。

在开始devise任何软件包和类之前,先从分析开始。 你想在游戏中拥有哪些function? 不要计划“稍后再添加”,因为在开始真正添加这个function之前,几乎可以肯定的是你所做的devise决定,你计划的这个存根是不够的。

而为了激励,我从这里讲经验,不要把你的任务当作写游戏引擎,写一个游戏! 无论你怎样思考未来的项目会是什么样的冷静,除非你把它放在现在正在写的游戏中,否则就会拒绝它。 没有未经validation的死代码,没有动力问题,因为没有能够解决一个问题,甚至不是即将到来的项目的问题。 没有完美的devise,但有一个足够好的。 值得记住这一点。

如上所述,我不相信MVC在devise游戏时是有用的。 模型/视图分离不是问题,控制器的东西是非常复杂的,太多了,所以被称为“控制器”。 如果你想有名为模型的子包,查看,控制,继续。 以下内容可以整合到这个包装scheme中,尽pipe其他方面至less是合理的。

我的解决scheme很难find一个起点,所以我只是从最高层开始:

在主程序中,我只是创build应用程序对象,初始化并启动它。 应用程序的init()将创buildfunction服务器(见下文)并将其join。 第一个游戏状态也被创build并推到最前面。 (也见下文)

function服务器封装了正交游戏function。 这些可以独立实现,并通过消息松散耦合。 示例特征:声音,视觉表示,碰撞检测,人工智能/决策制定,物理学等等。 如何组织function如下所述。

input,控制stream程和游戏循环

游戏状态呈现了一种组织input控制的方式。 我通常有一个类收集input事件或捕获input状态,并在稍后进行轮询(InputServer / InputManager)。 如果使用基于事件的方法,事件被赋予单个注册的活动游戏状态。

当开始游戏时,这将是主菜单游戏状态。 游戏状态有init/destroyresume/suspendfunction。 Init()会初始化游戏状态,如果是主菜单,它会显示最上面的菜单级别。 Resume()将控制这个状态,它现在接受来自InputServer的input。 Suspend()将清除屏幕上的菜单视图, destroy()将释放主菜单所需的任何资源。

游戏状态可以堆叠,当用户使用“新游戏”选项开始游戏时,MainMenu游戏状态被暂停,PlayerControlGameState将被放入堆栈并且现在接收input事件。 这样你就可以根据你的游戏状态来处理input。 在任何给定时间只有一个控制器处于活动状态,可以大大简化控制stream程。

input收集由游戏循环触发。 游戏循环基本上决定了当前循环的帧时间,更新特征服务器,收集input和更新游戏状态。 帧时间要么被赋予每个更新函数,要么由Timer单例提供。 这是用于确定自上次更新呼叫以来的持续时间的规范时间。

游戏对象和function

这个devise的核心是游戏对象和特征的交互。 如上所示,这个意义上的一个特征就是一个游戏function,可以彼此独立地实现。 游戏对象是任何以任何方式与玩家或任何其他游戏对象交互的东西。 示例:玩家头像本身就是一个游戏对象。 火炬是一个游戏对象,NPC是游戏对象,照明区域和声源或这些的任何组合。

传统的RPG游戏对象是一些复杂的类层次结构的顶级类,但实际上这种方法是错误的。 许多正交方面不能放入一个层次结构,甚至使用接口,最后你必须有具体的类。 一个物品是一个游戏物品,一个可拣选的物品是一个游戏物品,一个箱子是一个容器是一个物品,但是让一个箱子可以拣选或不可以是这个方法的一个或者决定,因为你必须有一个单一的层次结构。 当你想拥有一个只有在谜语被回答时才会打开的魔法谜语胸部,情况会变得更加复杂。 没有一个合适的层次结构。

一个更好的方法是只有一个游戏对象类,并将每个通常在类层次结构中表示的正交方面放到它自己的组件/要素类中。 游戏对象可以容纳其他物品吗? 然后将ContainerFeature添加到它,它是否可以通话,将TalkTargetFeature添加到它,等等。

在我的devise中,一个游戏对象只有一个固有的唯一ID,名称和位置属性,其他所有东西都被添加为一个function组件。 通过调用addComponent(),removeComponent(),可以在运行时通过GameObject接口添加组件。 所以为了使它可见,添加一个VisibleComponent,使它发出声音,添加一个AudableComponent,使它成为一个容器,添加一个ContainerComponent。

VisibleComponent对于您的问题很重要,因为这是提供模型和视图之间的链接的类。 不是所有的东西都需要古典意义上的观点。 触发区不可见,环境声区也不会。 只有具有VisibleComponent的游戏对象才可见。 当VisibleFeatureServer更新时,可视化表示在主循环中更新。 然后根据注册的VisibleComponents更新视图。 无论是查询每个消息的状态,还是仅仅对队列中的消息进行排队,都取决于您的应用程序和底层的可视化库。

在我的情况下,我使用Ogre3D。 在这里,当一个VisibleComponent被附加到一个游戏对象时,它会创build一个附加到场景graphics的场景节点,并且将场景节点附加到一个实体(表示一个3d网格)。 每个TransformMessage(见下面)都会立即处理。 VisibleFeatureServer然后使得Ogre3d将场景重绘到RenderWindow(本质上,细节总是比较复杂)

消息

那么这些特性和游戏状态和游戏对象又是如何相互通信的呢? 通过消息。 这个devise中的消息只是Message类的任何子类。 每个具体消息都可以有自己的接口,方便其任务。

消息可以从一个GameObject发送到其他GameObjects,从一个GameObject到它的组件,从FeatureServers到它们负责的组件。

当一个FeatureComponent被创build并被添加到一个游戏对象时,它会通过调用myGameObject.registerMessageHandler(this,MessageID)来注册它想要接收的每个消息。 它还会将自己注册到其function服务器,以获取从该处接收的每条消息。

如果玩家试图与其所关注的angular色交谈,则用户将以某种方式触发交谈行为。 例如:如果焦点中的焦点是一个友善的NPC,那么通过按下鼠标button就会触发标准的交互。 通过发送一个GetStandardActionMessage来查询目标游戏对象的标准动作。 目标游戏对象接收到该消息,并从第一个注册的游戏对象开始,通知其想要知道该消息的特征组件。 此消息的第一个组件将标准动作设置为将触发自身的标准动作(TalkTargetComponent将设置标准动作为Talk,它将首先接收),然后将消息标记为已消耗。 游戏对象将testing消耗,并看到它确实消耗并返回给调用者。 然后评估现在修改的消息并调用所产生的动作

是的,这个例子看起来很复杂,但它已经是一个比较复杂的例子了。 其他人喜欢TransformMessage通知有关位置和方向的变化更容易处理。 TransformMassage对许多function服务器都很有用。 VisualisationServer需要它在屏幕上更新GameObject的可视化表示。 SoundServer更新3D声音位置等等。

使用消息而不是调用方法的好处应该是清楚的。 组件之间的耦合度较低。 调用方法时,调用者需要知道被调用者。 但是通过使用消息,这是完全分离的。 如果没有接收器,那么没关系。 另外接收者如何处理消息,如果根本不是呼叫者的关心。 也许代表在这里是一个不错的select,但是Java错过了一个干净的实现,在networking游戏的情况下,你需要使用某种types的RPC,这个RPC有很高的延迟。 低延迟对交互式游戏至关重要。

持久性和编组

这使我们知道如何通过networking传递消息。 通过将GameObject / Feature交互封装到消息中,我们只需要担心如何通过networking传递消息。 理想情况下,您将消息转换为通用forms,并将其放入UDP包并发送。 接收器将消息解包到适当类的实例,并根据消息将其传送给接收者或广播它。 我不知道Java的内置序列化是否能够胜任。 但即使没有,也有很多的库可以做到这一点。

GameObjects和组件通过属性使得它们的持久化状态可用(C ++没有内置序列化)。它们有一个类似于Java中的PropertyBag的接口,可以检索和恢复它们的状态。

参考

  • 脑转储 :一个专业游戏开发者的博客。 同时也是开源的Nebula引擎的作者,这是一款在商业上成功的游戏中使用的游戏引擎。 我在这里介绍的大部分devise来自Nebula的应用层。
  • 上面的博客值得关注的文章 ,它列出了引擎的应用层。 我试图在上面描述的另一个angular度。
  • 关于如何布置游戏架构的冗长讨论 。 大多是OGG的具体,但一般来说也是有用的。
  • 另一个基于组件的devise的参数,在底部有用的参考。

我不确定一个MVC框架是否适合游戏,但是我会假设你正在创build一个游戏服务器,例如MUD或者简单的MMPROGOOGPRG,并且代码的可读性和可升级性对于你来说比生性能。

这取决于你想同时支持多less用户以及游戏服务器的function。 您可以从基于文本的I / O开始,然后在项目成熟时转到二进制或XML表示forms。

我肯定会有不同的行动,不同的class级在做每一个可能的命令。

您的前端parsing器将从networking/视图 – >控制器层创buildUserAction对象(实际上是子类,T扩展了UserAction)。 这使您可以改变您的networking如何在线运行,而不会破坏您的核心应用程序。 您可能已经在考虑对这些UserAction对象使用自定义序列化或类似的消息。 这个UserAction将通过工厂传递到它的UserActionHandler(Command)实现,或者只是检查一个交换机中的CommandEnum字段。 然后,所述处理程序将在模型上执行必要的魔术,并且控制器将注意到模型状态改变并将通知发送给其他玩家/视图,等等。

让我的另一个答案是“MVC被认为在游戏中可能有害”。 如果您的3D渲染是“视图”,而您的networkingstream量是“视图”,那么您是否最终将远程客户端视为模型? (networkingstream量可能看起来像是发送它时的另一个视图机制,但是在接收端,这是您的游戏所基于的确定性模型。)将MVC保留在它所属的位置 – 将视觉表示与逻辑分离。

一般来说,你想通过发送消息到服务器,并等待,直到你得到一个响应。 如果您以同样的方式处理服务器,则无论该服务器在另一个大陆上还是在同一个进程中,都无关紧要。

假设用户启动游戏并select字符2.然后用户移动到坐标(5,2)。 然后他对公众聊天说:“嗨!” 然后他select保存并退出。

把事情简单化。 MUD只是简单地以纯文本的forms发送命令(例如“SELECT character2”,“MOVE TO 5,2”,“SAY Hi”),如果你愿意的话,文本parsing器。

一个更加结构化的select是发送一个简单的XML对象,因为我知道你们的Java人喜欢XML;)

 <message> <choose-character number='2'/> </message> <message> <move-character x='5' y='2'/> </message> <!--- etc ---> 

在商业游戏中,我们倾向于拥有一个二进制结构,它包含一个消息types标识,然后是一个任意的有效载荷,在每一端用序列化来打包和解压缩这些消息。 不过,你不需要那种效率。

虽然我并不完全相信MVC适合游戏devise,但还是有一些文章介绍了如何使用MVC体系结构将不同位置的游戏逻辑放在哪里。 这里有一个快速参考可以回答你的很多问题:

游戏架构:模型 – 视图 – 控制器