【问题标题】:Data structures for message passing within a program?程序内消息传递的数据结构?
【发布时间】:2010-11-14 10:11:51
【问题描述】:

我正在尝试编写一个简单的 RPG。到目前为止,每次我尝试启动它都会立即变得一团糟,我不知道如何组织任何事情。所以我要重新开始,尝试构建一个基本上是 MVC 框架的新结构的原型。我的应用程序开始在控制器中执行,它将在其中创建视图和模型。然后会进入游戏循环,游戏循环的第一步就是收集用户输入。

用户输入将由视图的一部分收集,因为它可以变化(3D 视图将直接轮询用户输入,而远程视图可能会通过 telnet 连接接收它,或者命令行视图会使用系统.in)。输入将被转换为消息,并且每条消息将被提供给控制器(通过方法调用),然后控制器可以解释消息以修改模型数据,或通过网络发送数据(因为我希望有一个网络选项) .

在网络游戏的情况下,这种消息处理技术也可用于处理网络消息。到目前为止,我是否保持了 MVC 的精神?

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

这是一个用例,每条消息都用斜体表示:假设用户开始游戏并选择角色 2。然后用户移动到坐标(5,2)。然后他对公共聊天说,“嗨!”。然后他选择保存并退出

视图应该如何将这些消息包装成控制器可以理解的东西?或者你认为我应该有单独的控制器方法,比如 chooseCharacter()、moveCharacterTo()、publicChat()?当我转向网络游戏时,我不确定这种简单的实现是否可行。但在极端的另一端,我不想只向控制器发送字符串。这很困难,因为选择字符操作需要一个整数,移动需要两个整数,而聊天需要一个字符串(和一个范围(公共私有全局),在私有的情况下,一个目标用户);没有真正的集合数据类型。

也非常欢迎任何一般性建议;我在正确的时间担心这个吗?我是否正朝着布局合理的 MVC 应用程序前进?有什么我忘记了吗?

谢谢!

【问题讨论】:

  • 为什么要使用 MVC?不知何故,stackoverflow 上的游戏开发讨论都是关于 MVC 的,但仅限于此,其他任何地方都没有。游戏不是数据库 GUI,认为 MVC 会掩盖游戏编程的真正问题。
  • 好吧,我说得有点松散......当我说“视图”和“模型”时,我并不是说这些是我的字面类名称,也不是说它们甚至只是一个类。它更像是一个命名空间。不过,将视图与数据分开允许可互换的接口是有道理的;不像我期望同时创建 3D 和 2D 界面,但即使使用 Java 序列化,我也可以序列化模型并知道所有内容都保存到磁盘。到目前为止,我的游戏组织起来充其量是杂乱无章的,我正在努力解决这个问题,但我不明白为什么 MVC 如此糟糕。
  • 模型和视图的分离对于游戏来说并不是真正的问题。这是容易的部分。更困难的部分是奠定基础。组织游戏对象及其能力的方式,解决用户ai环境交互的更大问题。用 MVC 术语来看游戏可能会导致另一个混乱的架构,因为忽略了更大的问题。我将在今天晚些时候列出一个解决方案的粗略草图......

标签: java model-view-controller


【解决方案1】:

(免责声明:我从未用 Java 编写过游戏,只用 C++ 编写过游戏。但总体思路也应该适用于 Java。 我提出的想法不是我自己的,而是我在书籍或“互联网”上找到的解决方案的混搭,请参阅参考资料部分。 我自己使用了所有这些,到目前为止,它产生了一个简洁的设计,我确切地知道在哪里添加我添加的新功能。)

恐怕这会是一个很长的答案,第一次阅读时可能不清楚,因为我不能很好地描述它只是自上而下,所以会来回参考,这是由于我缺乏解释技巧,而不是因为设计有缺陷。事后看来,我做得太过分了,甚至可能跑题了。但是现在我已经写了这一切,我不能让自己把它扔掉。有什么不清楚的就问一下。

在开始设计任何包和类之前,请先进行分析。您希望在游戏中拥有哪些功能。不要计划“也许我稍后会添加”,因为几乎可以肯定,在您开始认真添加此功能之前预先做出的设计决策,您为它计划的存根是不够的。

为了激励,我从这里的经验谈起,不要把你的任务看作是写一个游戏引擎,写一个游戏!无论你对未来项目有什么很酷的想法,都要拒绝它,除非你把它放在你现在正在编写的游戏中。没有未经测试的死代码,也没有由于无法解决对当前项目来说甚至不是问题的问题而导致的动机问题。没有完美的设计,但有一个足够好的。值得牢记这一点。

如上所述,我认为 MVC 在设计游戏时没有任何用处。模型/视图分离不是问题,控制器的东西相当复杂,以至于被称为“控制器”。 如果您想拥有名为模型、视图、控制的子包,请继续。以下内容可以集成到此封装方案中,尽管其他内容至少同样合理。

我的解决方案很难找到起点,所以我从最顶层开始:

在主程序中,我只是创建 Application 对象,初始化它并启动它。应用程序的init() 将创建功能服务器(见下文)并初始化它们。还创建了第一个游戏状态并将其推送到顶部。 (另见下文)

功能服务器封装正交游戏功能。这些可以独立实现,并通过消息松散耦合。示例功能:声音、视觉表示、碰撞检测、人工智能/决策、物理等等。下面描述了功能本身的组织方式。

输入、控制流和游戏循环

游戏状态提供了一种组织输入控制的方法。我通常有一个类来收集输入事件或捕获输入状态并稍后轮询它(InputServer/InputManager)。如果使用基于事件的方法,则将事件分配给单个已注册的活动游戏状态。

开始游戏时,这将是主菜单游戏状态。游戏状态具有init/destroyresume/suspend 函数。 Init() 将初始化游戏状态,如果是主菜单,它将显示最顶层的菜单级别。 Resume() 将控制此状态,它现在从 InputServer 获取输入。 Suspend() 将从屏幕上清除菜单视图,destroy() 将释放主菜单所需的任何资源。

GameStates 可以堆叠,当用户使用“新游戏”选项启动游戏时,MainMenu 游戏状态将暂停,PlayerControlGameState 将被放入堆栈并接收输入事件。这样您就可以根据游戏的状态处理输入。在任何给定时间都只有一个控制器处于活动状态,您可以极大地简化控制流程。

输入集合由游戏循环触发。游戏循环基本上确定当前循环的帧时间,更新特征服务器,收集输入并更新游戏状态。帧时间要么提供给它们中的每一个的更新函数,要么由 Timer 单例提供。这是用于确定自上次更新调用以来持续时间的规范时间。

游戏对象和功能

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

传统上,RPG 游戏对象是一些复杂类层次结构的顶层,但实际上这种方法是错误的。许多正交方面不能放入层次结构中,即使最终使用接口也必须有具体的类。一个项目是一个游戏对象,一个可拾取的项目是一个游戏对象,一个箱子是一个容器是一个项目,但是用这种方法是否可以选择一个箱子是一个非此即彼的决定,因为你必须有一个等级制度。当你想要一个只有在回答了谜语时才会打开的会说话的魔法谜语箱时,它会变得更加复杂。没有一个完全适合的层次结构。

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

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

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

就我而言,我使用 Ogre3D。在这里,当 VisibleComponent 附加到游戏对象时,它会创建一个附加到场景图的 SceneNode 和一个实体(3d 网格的表示)到场景节点。每个 TransformMessage(见下文)都会立即处理。 VisibleFeatureServer 然后让 Ogre3d 将场景重绘到 RenderWindow(本质上,细节更复杂,一如既往)

消息

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

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

当一个 FeatureComponent 被创建并添加到一个游戏对象时,它通过为它想要接收的每条消息调用 myGameObject.registerMessageHandler(this, MessageID) 将自己注册到游戏对象。它还会为它想要从那里接收的每条消息将自己注册到其功能服务器。

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

是的,这个例子看起来很复杂,但它已经是更复杂的例子之一。其他诸如用于通知位置和方向变化的 TransformMessage 更容易处理。许多功能服务器都对 TransformMassage 感兴趣。 VisualisationServer 需要它来更新 GameObject 在屏幕上的视觉表示。 SoundServer 用于更新 3d 声音位置等。

使用消息而不是调用方法的优势应该很明显。组件之间的耦合度较低。调用方法时,调用者需要知道被调用者。但是通过使用消息,这是完全解耦的。如果没有接收器,那也没关系。如果根本不是呼叫者关心的,那么接收者如何处理消息也是如此。 也许委托在这里是一个不错的选择,但是 Java 缺少一个干净的实现,在网络游戏的情况下,您需要使用某种具有相当高延迟的 RPC。低延迟对于互动游戏至关重要。

持久性和编组

这使我们了解如何通过网络传递消息。通过将 GameObject/Feature 交互封装到消息中,我们只需要担心如何通过网络传递消息。理想情况下,您将消息带入通用形式并将它们放入 UDP 包中并发送。 Receiver 将消息解包到适当类的实例并将其引导到接收器或广播它,具体取决于消息。 我不知道Java的内置序列化是否能胜任这项任务。但即使没有,也有很多库可以做到这一点。

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

参考文献

  • The Brain Dump:专业游戏开发者的博客。也是开源星云引擎的作者,这是一种用于商业成功游戏的游戏引擎。我在这里介绍的大部分设计都取自 Nebula 的应用层。
  • Noteworthy article上面的博客,它对引擎的应用层进行了布局。我在上面试图描述的另一个角度。
  • A lengthy discussion 关于如何布局游戏架构。大部分是 Ogre 特有的,但也足够通用,对其他人也有用。
  • Another argument for component based designs,底部有有用的参考资料。

【讨论】:

  • 哇。这需要考虑很多。对我来说更多的困惑,耶!就在我以为我已经确定了 MVC 模式的时候……哈哈。但这对我有好处。我还有 3 年的大学时间来解决所有这些问题。 :)
  • 非常有帮助的答案(我目前正在为游戏引擎设计消息传递系统)只是想感谢 Haffax 的那个......:D
  • 这太棒了!尽管我非常感谢有关如何实现您提到的设计模式的类图或快速教程。谢谢大家!
  • 致未来的读者:这种方法现在称为组件实体系统,你可以google一下。
  • @cubuspl42 不是真的。这种方法的一部分确实是组件实体系统,但并非所有此类系统都使用消息传递方法进行通信。事实上,我想说的是,用于保护数据免受多线程访问的简单方法调用和锁定要普遍得多。这种方法有可能更好地扩展多线程,但您确实需要支付潜在的高额消息复制成本。
【解决方案2】:

我不太确定 MVC 框架是否适合游戏,但我假设您正在为例如 MUD 或简单的 MMPROGOOGPRG 创建游戏服务器,并且代码可读性和可升级性对于你比原始表现。

这取决于您希望同时支持多少用户,以及您的游戏服务器的功能。您可以从基于文本的 I/O 开始,然后随着项目的成熟转向二进制或 XML 表示。

我当然会有不同的动作,不同的班级执行每个可能的命令。

您的前端解析器将从网络/视图->控制器层创建 UserAction 对象(实际上是子类,T 扩展 UserAction)。这使您可以更改网络的运行方式,而不会破坏您的核心应用程序。您可能已经在考虑可以对带有这些 UserAction 对象的消息使用自定义序列化或类似方法。此 UserAction 将通过工厂或仅检查开关中的 CommandEnum 字段传递到其 UserActionHandler(命令)实现。然后,所述处理程序将对模型执行必要的魔法,控制器会注意到模型状态的变化并向其他玩家/视图发送通知,等等。

【讨论】:

  • 投票赞成“您的前端解析器将从网络/视图->控制器层创建 UserAction 对象(实际上是子类,T 扩展 UserAction)”
【解决方案3】:

让我的另一个答案是“MVC 被认为在游戏中可能有害”。如果您的 3D 渲染是“视图”并且您的网络流量是“视图”,那么您最终不会让远程客户端最终将视图视为模型吗? (当您发送网络流量时,它可能看起来只是另一种视图机制,但在接收端,这是您游戏所基于的最终模型。)将 MVC 保留在它所属的位置 - 将视觉呈现与逻辑分离。

通常,您希望通过向服务器发送消息并等待直到收到响应来工作。如果您以相同的方式处理它,那么该服务器是在另一个大陆上还是在同一进程中都没有关系。

假设用户开始游戏并 选择字符 2。然后用户 移动到坐标 (5,2)。随后他 对公共聊天说,“嗨!”。随后他 选择保存并退出。

保持简单。 MUD 过去只是简单地以纯文本形式发送命令(例如“SELECT character2”、“MOVE TO 5,2”、“SAY Hi”),如果您愿意编写文本解析器。

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

<message>
    <choose-character number='2'/>
</message>


<message>
    <move-character x='5' y='2'/>
</message>

<!--- etc --->

在商业游戏中,我们倾向于采用二进制结构,其中包含一个消息类型 ID,然后是一个任意有效负载,并通过序列化在每一端打包和解包此类消息。但是,您在这里不需要那种效率。

【讨论】:

  • 网络很特别,因为它既发送又接收。它被视为输入设备,因为网络消息被转换为游戏消息并发送到控制器。但是它还需要监听实际输入设备生成的游戏消息,并通过网络中继它们,以便远程服务器可以广播它们并回复。实际上没有发送和等待......这一切都需要异步。纯文本在网络连接上非常有用,但在应用程序中却不行,将按钮按下转换为字符串是没有意义的,
  • 将其交给控制器,将其解析回某物,然后将其发送给模型。这类似于取一个数字,将其乘以 2,然后除以 2,然后使用该数字。就XML而言,没必要小看Java,我什至不明白你的意思。我非常反对 XML。不管怎样,你我都知道这不是一个好建议。
  • 我认为我更想要的是分离演示。 martinfowler.com/eaaDev/SeparatedPresentation.html - 如果您仍然反对游戏,那么我不知道该告诉您什么。但如果你只是出于其他原因反对 MVC,那我还是不明白。
  • 恐怕我的建议从头到尾都是认真的。如果您确定以后必须支持网络输入,纯文本是一种完全合理的方法。当 1 可以做时,编写 2 个系统是没有意义的。您不需要最高效的系统,您需要最可靠和最灵活的系统。至于 MVC/分离演示,这对您的图形来说很好,但是将非视觉方面强加到网络(以及后来可能是 AI)中只会给您带来麻烦。这就是为什么这里几乎没有人为此推荐 MVC。
  • 至于 Java 评论,它是故意轻率的,只是重申了其他语言程序员的信念,即 Java 在比它实际适用的地方更多的地方使用 XML。但是,此示例可以很好地工作,简化您的消息解析并提供经过测试的可序列化格式。
【解决方案4】:

虽然我并不完全相信 MVC 非常适合游戏设计,但有一些文章介绍了使用 MVC 架构将不同的游戏逻辑放置在何处的基础知识。下面是一个快速参考,可以回答您的许多问题:

Game Architecture: Model-View-Controller

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2016-10-20
    • 2012-10-14
    • 1970-01-01
    • 2015-02-13
    • 2015-10-18
    • 2010-12-20
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多