基于组件的游戏引擎中的通信

发布于 2024-09-08 09:17:37 字数 1313 浏览 2 评论 0原文

对于我正在制作的 2D 游戏(针对 Android),我使用基于组件的系统,其中 GameObject 包含多个 GameComponent 对象。游戏组件可以是输入组件、渲染组件、子弹发射组件等。目前,GameComponents 拥有对拥有它们的对象的引用,并且可以修改它,但 GameObject 本身只有一个组件列表,它并不关心组件是什么,只要它们可以在对象更新时更新即可。

有时,组件具有游戏对象需要知道的一些信息。例如,对于碰撞检测,游戏对象将自身注册到碰撞检测子系统,以便在与另一个对象碰撞时收到通知。碰撞检测子系统需要知道物体的边界框。我直接将 x 和 y 存储在对象中(因为它被多个组件使用),但宽度和高度只有保存对象位图的渲染组件知道。我希望 GameObject 中有一个方法 getBoundingBox 或 getWidth 来获取该信息。或者一般来说,我想将一些信息从组件发送到对象。但是,在我当前的设计中,游戏对象不知道列表中包含哪些特定组件。

我可以想出几种方法来解决这个问题:

  1. 我可以让 GameObject 对于一些重要组件具有特定的字段,而不是拥有一个完全通用的组件列表。例如,它可以有一个名为renderingComponent的成员变量;每当我需要获取对象的宽度时,我只需使用renderingComponent.getWidth()。该解决方案仍然允许使用通用的组件列表,但它以不同的方式对待其中一些组件,而且我担心随着需要查询更多组件,我最终会得到几个异常字段。有些对象甚至没有渲染组件。

  2. 将所需信息作为游戏对象的成员,但允许组件更新它。因此,对象的宽度和高度默认为 0 或 -1,但渲染组件可以在其更新循环中将它们设置为正确的值。这感觉像是一种黑客行为,为了方便起见,我最终可能会将许多东西推送到 GameObject 类,即使并非所有对象都需要它们。

  3. 让组件实现一个接口,该接口指示可以查询它们的信息类型。例如,渲染组件将实现 HasSize 接口,其中包括 getWidth 和 getHeight 等方法。当 GameObject 需要宽度时,它会循环检查其组件是否实现 HasSize 接口(在 Java 中使用 instanceof 关键字,或在 C# 中使用 is)。这似乎是一种更通用的解决方案,一个缺点是搜索组件可能需要一些时间(但是,大多数对象只有 3 或 4 个组件)。

这个问题不是针对特定问题。它经常出现在我的设计中,我想知道处理它的最佳方法是什么。由于这是一款游戏,性能有些重要,但每个对象的组件数量通常很少(最多 8 个)。

简短版本

在基于组件的游戏系统中,将信息从组件传递到对象同时保持设计通用的最佳方法是什么?

For a 2D game I'm making (for Android) I'm using a component-based system where a GameObject holds several GameComponent objects. GameComponents can be things such as input components, rendering components, bullet emitting components, and so on. Currently, GameComponents have a reference to the object that owns them and can modify it, but the GameObject itself just has a list of components and it doesn't care what the components are as long as they can be updated when the object is updated.

Sometimes a component has some information which the GameObject needs to know. For example, for collision detection a GameObject registers itself with the collision detection subsystem to be notified when it collides with another object. The collision detection subsystem needs to know the object's bounding box. I store x and y in the object directly (because it is used by several components), but width and height are only known to the rendering component which holds the object's bitmap. I would like to have a method getBoundingBox or getWidth in the GameObject that gets that information. Or in general, I want to send some information from a component to the object. However, in my current design the GameObject doesn't know what specific components it has in the list.

I can think of several ways to solve this problem:

  1. Instead of having a completely generic list of components, I can let the GameObject have specific field for some of the important components. For example, it can have a member variable called renderingComponent; whenever I need to get the width of the object I just use renderingComponent.getWidth(). This solution still allows for generic list of components but it treats some of them differently, and I'm afraid I'll end up having several exceptional fields as more components need to be queried. Some objects don't even have rendering components.

  2. Have the required information as members of the GameObject but allow the components to update it. So an object has a width and a height which are 0 or -1 by default, but a rendering component can set them to the correct values in its update loop. This feels like a hack and I might end up pushing many things to the GameObject class for convenience even if not all objects need them.

  3. Have components implement an interface that indicates what type of information they can be queried for. For example, a rendering component would implement the HasSize interface which includes methods such as getWidth and getHeight. When the GameObject needs the width, it loops over its components checking if they implement the HasSize interface (using the instanceof keyword in Java, or is in C#). This seems like a more generic solution, one disadvantage is that searching for the component might take some time (but then, most objects have 3 or 4 components only).

This question isn't about a specific problem. It comes up often in my design and I was wondering what's the best way to handle it. Performance is somewhat important since this is a game, but the number of components per object is generally small (the maximum is 8).

The short version

In a component based system for a game, what is the best way to pass information from the components to the object while keeping the design generic?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

等待我真够勒 2024-09-15 09:17:37

我们每周都会在 GameDev.net(其中游戏对象通常称为“实体”)上收到三到四次有关此问题的变体,但到目前为止,还没有就最佳方法达成共识。然而,几种不同的方法已被证明是可行的,所以我不会太担心。

然而,通常问题与组件之间的通信有关。人们很少担心从组件获取信息到实体 - 如果实体知道它需要什么信息,那么它大概知道它需要访问什么类型的组件以及它需要调用该组件的哪个属性或方法来获取数据。如果您需要响应式而不是主动式,则注册回调或为组件设置观察者模式,以便让实体知道组件中的某些内容何时发生更改,并读取此时的值。

完全通用的组件在很大程度上是无用的:它们需要提供某种已知的接口,否则它们的存在就没有什么意义。否则,您可能只拥有一个由无类型值组成的大型关联数组,然后就可以完成它了。在 Java、Python、C# 和其他比 C++ 稍高级的语言中,您可以使用反射为您提供使用特定子类的更通用的方法,而无需将类型和接口信息编码到组件本身中。

至于通信:

有些人假设一个实体将始终包含一组已知的组件类型(其中每个实例都是几个可能的子类之一),因此可以直接获取对另一个组件的引用并通过其读/写公共接口。

有些人使用发布/订阅、信号/槽等来在组件之间创建任意连接。这看起来更灵活一些,但最终您仍然需要了解这些隐式依赖关系。 (如果在编译时已知这一点,为什么不直接使用以前的方法?)

或者,您可以将所有共享数据放入实体本身并将其用作共享通信区域(与 AI 中的黑板系统),每个组件都可以读取和写入。这通常需要一定的稳健性,以应对某些属性在您期望时并不存在的情况。它也不适合并行性,尽管我怀疑这对于小型嵌入式系统来说是一个巨大的问题......?

最后,有些人的系统中根本不存在该实体。组件位于其子系统内,实体的唯一概念是某些组件中的 ID 值 - 如果渲染组件(在渲染系统内)和播放器组件(在玩家系统内)具有相同的 ID,那么您可以假设前者处理后者的绘图。但没有任何单个对象可以聚合这些组件中的任何一个。

We get variations on this question three or four times a week on GameDev.net (where the gameobject is typically called an 'entity') and so far there's no consensus on the best approach. Several different approaches have been shown to be workable however so I wouldn't worry about it too much.

However, usually the problems regard communicating between components. Rarely do people worry about getting information from a component to the entity - if an entity knows what information it needs, then presumably it knows exactly what type of component it needs to access and which property or method it needs to call on that component to get the data. if you need to be reactive rather than active, then register callbacks or have an observer pattern set up with the components to let the entity know when something in the component has changed, and read the value at that point.

Completely generic components are largely useless: they need to provide some sort of known interface otherwise there's little point them existing. Otherwise you may as well just have a large associative array of untyped values and be done with it. In Java, Python, C#, and other slightly-higher-level languages than C++ you can use reflection to give you a more generic way of using specific subclasses without having to encode type and interface information into the components themselves.

As for communication:

Some people are making assumptions that an entity will always contain a known set of component types (where each instance is one of several possible subclasses) and therefore can just grab a direct reference to the other component and read/write via its public interface.

Some people are using publish/subscribe, signals/slots, etc., to create arbitrary connections between components. This seems a bit more flexible but ultimately you still need something with knowledge of these implicit dependencies. (And if this is known at compile time, why not just use the previous approach?)

Or, you can put all shared data in the entity itself and use that as a shared communication area (tenuously related to the blackboard system in AI) that each of the components can read and write to. This usually requires some robustness in the face of certain properties not existing when you expected them to. It also doesn't lend itself to parallelism, although I doubt that's a massive concern on a small embedded system...?

Finally, some people have systems where the entity doesn't exist at all. The components live within their subsystems and the only notion of an entity is an ID value in certain components - if a Rendering component (within the Rendering system) and a Player component (within the Players system) have the same ID, then you can assume the former handles the drawing of the latter. But there isn't any single object that aggregates either of those components.

凉薄对峙 2024-09-15 09:17:37

正如其他人所说,这里没有永远正确的答案。不同的游戏会带来不同的解决方案。如果您正在构建一个包含许多不同类型实体的大型复杂游戏,那么为了获得可维护性,在组件之间具有某种抽象消息传递的更加解耦的通用架构可能值得您付出努力。对于具有类似实体的简单游戏,将所有状态推入 GameObject 可能是最有意义的。

对于您需要将边界框存储在某处并且只有碰撞组件关心它的特定场景,我会:

  1. 将其存储在碰撞组件本身中。
  2. 使碰撞检测代码直接与组件一起工作。

因此,不要让碰撞引擎迭代 GameObject 集合来解决交互问题,而是直接迭代 CollisionComponent 集合。一旦发生碰撞,组件将负责将其推送到其父游戏对象。

这给您带来了一些好处:

  1. 将特定于碰撞的状态排除在游戏对象之外。
  2. 使您无需迭代没有碰撞组件的游戏对象。 (如果您有很多非交互式对象,例如视觉效果和装饰,这可以节省相当多的周期。)
  3. 使您免于在对象及其组件之间行走而消耗大量周期。如果您迭代对象,然后对每个对象执行 getCollisionComponent(),则指针跟随可能会导致缓存未命中。对每个对象的每一帧执行此操作会消耗大量 CPU。

如果您有兴趣,我在此处提供了有关此模式的更多信息,尽管看起来您已经了解了其中的大部分内容在那一章中。

Like others have said, there's no always right answer here. Different games will lend themselves towards different solutions. If you're building a big complex game with lots of different kinds of entities, a more decoupled generic architecture with some kind of abstract messaging between components may be worth the effort for the maintainability you get. For a simpler game with similar entities, it may make the most sense to just push all of that state up into GameObject.

For your specific scenario where you need to store the bounding box somewhere and only the collision component cares about it, I would:

  1. Store it in the collision component itself.
  2. Make the collision detection code work with the components directly.

So, instead of having the collision engine iterate through a collection of GameObjects to resolve the interaction, have it iterate directly through a collection of CollisionComponents. Once a collision has occurred, it will be up to the component to push that up to its parent GameObject.

This gives you a couple of benefits:

  1. Leaves collision-specific state out of GameObject.
  2. Spares you from iterating over GameObjects that don't have collision components. (If you have a lot of non-interactive objects like visual effects and decoration, this can save a decent number of cycles.)
  3. Spares you from burning cycles walking between the object and its component. If you iterate through the objects then do getCollisionComponent() on each one, that pointer-following can cause a cache miss. Doing that for every frame for every object can burn a lot of CPU.

If you're interested I have more on this pattern here, although it looks like you already understand most of what's in that chapter.

花开半夏魅人心 2024-09-15 09:17:37

使用“事件总线”。 (请注意,您可能无法按原样使用代码,但它应该为您提供基本的想法)。

基本上,创建一个中央资源,每个对象都可以将自己注册为侦听器并说“如果 X 发生,我想知道”。当游戏中发生某些事情时,负责的对象可以简单地将事件 X 发送到事件总线,所有感兴趣的各方都会注意到。

[编辑] 有关更详细的讨论,请参阅消息传递(感谢snk_kid 指出了这一点)。

Use an "event bus". (note that you probably can't use the code as is but it should give you the basic idea).

Basically, create a central resource where every object can register itself as a listener and say "If X happens, I want to know". When something happens in the game, the responsible object can simply send an event X to the event bus and all interesting parties will notice.

[EDIT] For a more detailed discussion, see message passing (thanks to snk_kid for pointing this out).

花桑 2024-09-15 09:17:37

一种方法是初始化组件容器。每个组件可以提供服务,也可能需要其他组件的服务。根据您的编程语言和环境,您必须想出一种提供此信息的方法。

在最简单的形式中,组件之间具有一对一的连接,但您还需要一对多的连接。例如,CollectionDetector 将具有实现IBoundingBox 的组件列表。

在初始化期间,容器将连接组件之间的连接,并且在运行时不会产生额外的成本。

这与您的解决方案 3) 很接近,期望组件之间的连接仅连接一次,并且不会在游戏循环的每次迭代中进行检查。

.NET 托管扩展性框架 是解决此问题的一个很好的解决方案。我知道您打算在Android上进行开发,但您仍然可以从这个框架中获得一些灵感。

One approach is to initialize a container of components. Each component can provide a service and may also require services from other components. Depending on your programming language and environment you have to come up with a method for providing this information.

In its simplest form you have one-to-one connections between components, but you will also need one-to-many connections. E.g. the CollectionDetector will have a list of components implementing IBoundingBox.

During initialization the container will wire up connections between components, and during run-time there will be no additional cost.

This is close to you solution 3), expect the connections between components are wired only once and are not checked at every iteration of the game loop.

The Managed Extensibility Framework for .NET is a nice solution to this problem. I realize that you intend to develop on Android, but you may still get some inspiration from this framework.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文