何时/何地更新组件


10

我不是使用通常的继承重型游戏引擎,而是使用基于组件的方法。但是,我很难说明让组件在哪里工作。

假设我有一个简单的实体,其中包含一个组件列表。当然,实体不知道这些组件是什么。可能存在一个使实体在屏幕上处于某个位置的组件,可能还有一个组件在屏幕上绘制该实体。

为了使这些组件正常工作,它们必须在每一帧进行更新,最简单的方法是遍历场景树,然后为每个实体更新每个组件。但是某些组件可能需要更多管理。例如,使实体可碰撞的组件必须由可以监视所有可碰撞组件的组件来管理。使实体成为可绘制实体的组件需要某人监督所有其他可绘制组件以找出绘制顺序,等等。

所以我的问题是,在哪里更新组件,将它们带给管理者的干净方法是什么?

我已经考虑过为每种组件类型使用单例管理器对象,但是它具有使用单例的通常缺点,一种缓解此问题的方法是使用依赖项注入,但这听起来像是针对此问题的过大杀伤力。我还可以遍历场景树,然后使用某种观察者模式将不同的组件收集到列表中,但是这样做似乎浪费每一帧。


1
您是否以某种方式使用系统?
Asakeron

组件系统是执行此操作的常用方法。我个人只是在所有实体上调用update,这在所有组件上调用update,并且有一些“特殊”情况(例如用于冲突检测的空间管理器,它是静态的)。
ashes999

组件系统?我以前从没听说过。我将开始使用Google搜索,但欢迎您使用任何推荐的链接。
Roy T.

1
实体系统是未来MMOG发展的巨大资源。而且,老实说,我总是对这些架构名称感到困惑。与建议的方法的区别在于,组件仅保存数据,而系统对其进行处理。这个答案也非常相关。
Asakeron

1
我在这里写了一篇关于这个主题的博客文章:gamedevrubberduck.wordpress.com/2012/12/26/…–
AlexFoxGill

Answers:


15

我建议您先阅读迈克·阿克顿的3个大谎言,因为您违反了其中两个。我是认真的,这将改变您设计代码的方式:http : //cellperformance.beyond3d.com/articles/2008/03/three-big-lies.html

那么您违反了哪一个?

谎言#3-代码比数据更重要

您谈论的是依赖注入,它在某些(和某些情况下)实例中可能很有用,但如果使用它,应该总是发出很大的警钟,尤其是在游戏开发中!为什么?因为它通常是不必要的抽象。在错误的地方进行抽象是可怕的。所以你有一个游戏。游戏具有用于不同组件的管理器。组件均已定义。因此,在您的主游戏循环代码中的某个地方“让”经理们上课。喜欢:

private CollissionManager _collissionManager;
private BulletManager _bulletManager;

给它一些getter函数以获取每个管理器类(getBulletManager())。此类本身可能是一个Singleton,或者可以从一个类到达(无论如何,您可能在某个地方有一个中央Game Singleton)。定义明确的硬编码数据和行为没有错。

请勿使用允许您使用密钥注册Manager的ManagerManager,其他想要使用该Manager的类可以使用该密钥检索Manager。这是一个很棒的系统,非常灵活,但是在这里谈论游戏的地方。您确切知道游戏中包含哪些系统。为什么要假装自己不喜欢?因为对于那些认为代码比数据更重要的人来说,这是一个系统。他们会说“代码是灵活的,数据就填满了它”。但是代码只是数据。我所描述的系统要容易得多,更可靠,易于维护并且更加灵活(例如,如果一个经理的行为与其他经理不同,则只需更改几行,而不必重新构建整个系统)

谎言#2-应围绕世界模型设计代码

因此,您在游戏世界中拥有一个实体。实体具有许多定义其行为的组件。因此,您将创建一个Entity类,其中包含Component对象的列表以及一个Update()函数,该函数调用每个Component的Update()函数。对?

不:)这是围绕世界模型设计的:您的游戏中有子弹,因此添加了Bullet类。然后,您更新每个项目符号并继续进行下一个。这绝对会损害您的性能,并且会给您带来令人费解的令人费解的代码库,其中到处都有重复的代码,而没有类似代码的逻辑结构。(在这里查看我的答案,以更详细地解释为什么传统的OO设计很烂,或者查找面向数据的设计)

让我们看看没有OO偏见的情况。我们需要以下内容,请不要少(请注意,没有必要为实体或对象创建类):

  • 你有一堆实体
  • 实体由许多定义实体行为的组件组成
  • 您希望每帧更新游戏中的每个组件,最好以受控方式
  • 除了将组件标识为属于一起之外,实体本身不需要执行任何操作。这是几个组件的链接/ ID。

让我们看一下情况。您的组件系统将帧更新游戏中每个对象的行为。这绝对是您发动机的关键系统。性能在这里很重要!

如果您熟悉计算机体系结构或面向数据的设计,那么您将知道如何实现最佳性能:紧密包装的内存和通过对代码执行进行分组。如果您像这样执行代码段A,B和C:ABCABCABC,则执行以下代码时将不会获​​得相同的性能:AAABBBCCC。这不仅是因为指令和数据高速缓存将得到更有效的利用,而且还因为如果您一个接一个地执行所有“ A”,则有很大的优化空间:删除重复的代码,预先计算要使用的数据所有“ A”等

因此,如果要更新所有组件,请不要使用更新功能使它们成为类/对象。我们不要为每个实体中的每个组件调用该更新函数。那就是“ ABCABCABC”解决方案。让我们将所有相同的组件更新分组在一起。然后,我们可以更新所有A组件,然后更新B,等等。我们需要做些什么?

首先,我们需要组件管理器。对于游戏中每种类型的组件,我们都需要一个经理类。它具有更新功能,将更新该类型的所有组件。它具有一个create函数,该函数将添加该类型的新组件,而remove函数将销毁指定的组件。可能还有其他帮助程序功能来获取和设置特定于该组件的数据(例如:为“模型组件”设置3D模型)。请注意,从某种程度上讲,经理是外界的黑匣子。我们不知道每个组件的数据是如何存储的。我们不知道每个组件如何更新。我们不在乎,只要组件的行为符合预期即可。

接下来,我们需要一个实体。您可以将其设为一堂课,但这几乎没有必要。实体不过是唯一的整数ID或哈希字符串(也就是整数)。当您为实体创建组件时,会将ID作为参数传递给Manager。要删除组件时,请再次传递ID。向实体添加更多数据而不只是使其成为ID可能有一些优点,但是这些将仅是辅助函数,因为正如我在需求中列出的那样,所有实体行为都由组件本身定义。它是您的引擎,所以对您来说有意义的事。

我们需要的是实体管理器。如果您使用仅ID的解决方案,则此类将生成唯一的ID,或者可用于创建/管理Entity对象。如果需要,它还可以保留游戏中所有实体的列表。实体管理器可以是组件系统的中心类,可以存储对游戏中所有ComponentManager的引用,并以正确的顺序调用它们的更新功能。这样,所有游戏循环所要做的就是调用EntityManager.update(),整个系统与引擎的其余部分完全分开。

那是鸟瞰图,让我们看一下组件管理器是如何工作的。这是您需要的:

  • 调用create(entityID)时创建组件数据
  • 调用remove(entityID)时删除组件数据
  • 在调用update()时更新所有(适用的)组件数据(即,并非所有组件都需要更新每一帧)

最后一个是定义组件行为/逻辑的地方,并且完全取决于要编写的组件的类型。AnimationComponent将根据其所在的帧更新Animation数据。DragableComponent将仅更新由鼠标拖动的组件。PhysicsComponent将更新物理系统中的数据。但是,由于您可以一次性更新相同类型的所有组件,因此您可以进行一些优化,而每个组件都是一个单独的对象,并且可以随时调用更新函数,这是不可能的。

请注意,我仍然从未呼吁创建XxxComponent类来保存组件数据。随你(由你决定。您喜欢面向数据的设计吗?然后为每个变量在单独的数组中构造数据。您喜欢面向对象的设计吗?(我不建议这样做,它仍然会在很多地方降低您的性能),然后创建一个XxxComponent对象,该对象将保存每个组件的数据。

管理器的优点是封装。现在,封装是编程领域中最可怕的滥用哲学之一。这是应该使用的方式。只有管​​理者知道哪些组件数据存储在何处,组件的逻辑如何工作。有一些功能可以获取/设置数据,仅此而已。您可以重写整个管理器及其基础类,并且如果不更改公共接口,甚至没有人注意。改变了物理引擎?只需重写PhysicsComponentManager即可完成。

最后有一件事:组件之间的通信和数据共享。现在这很棘手,还没有一种万能的解决方案。您可以在管理器中创建get / set函数,以允许例如碰撞组件从位置组件获取位置(即PositionManager.getPosition(entityID))。您可以使用事件系统。您可以在实体中存储一些共享数据(我认为这是最丑陋的解决方案)。您可以使用(经常使用)消息系统。或结合使用多个系统!我没有时间或经验去使用这些系统,但是google和stackoverflow搜索是您的朋友。


我觉得这个答案很有趣。只是一个问题(希望您或其他人可以回答我)。如何在基于DOD组件的系统上消除实体?即使是Artemis,也将Entity用作类,但我不确定这是不是很容易。
Wolfrevo Kcats

1
消除它是什么意思?您的意思是没有实体类的实体系统吗?Artemis之所以拥有Entity是因为在Artemis中,Entity类管理其自己的组件。在我提出的系统中,ComponentManager类管理组件。因此,您不需要一个Entity类,而只需一个唯一的整数ID。假设您有实体254,其中有一个位置组件。当您要更改位置时,可以使用254作为id参数调用PositionCompMgr.setPosition(int id,Vector3 newPos)。
2013年

但是,您如何管理ID?如果要从实体中删除组件以稍后将其分配给其他实体怎么办?如果要删除一个实体并添加一个新实体,该怎么办?如果要在两个或多个实体之间共享一个组件怎么办?我真的对此很感兴趣。
Wolfrevo Kcats

1
EntityManager可用于发出新的ID。它也可以用于基于预定义的模板创建完整的实体(例如,创建“ EnemyNinja”,该实体会生成新的ID,并创建构成敌方忍者的所有组件,例如可渲染,碰撞,AI,或者一些用于近战的组件等)。它还可以具有removeEntity函数,该函数自动调用所有ComponentManager的删除函数。ComponentManager可以检查它是否具有给定实体的组件数据,如果是,则删除该数据。
集市

1
将组件从一个实体移动到另一个实体?只需向每个ComponentManager添加一个函数swapComponentOwner(int oldEntity,int newEntity)。数据就在ComponentManager中,您只需要一个函数即可更改其所属的所有者。每个ComponentManager都有类似索引或映射的内容,以存储哪些数据属于哪个实体ID。只需将实体ID从旧ID更改为新ID。我不确定在我认为的系统中共享组件是否容易,但是有多难?索引表中有多个,而不是一个Entity ID <-> Component Data链接。
集市

3

为了使这些组件正常工作,它们必须在每一帧进行更新,最简单的方法是遍历场景树,然后为每个实体更新每个组件。

这是组件更新的典型幼稚方法(如果它适用于您,那么幼稚就不一定有错)。它实际上涉及到的最大问题之一-您正在通过组件的界面进行操作(例如IComponent),因此您对它刚刚被更新的内容一无所知。您可能也对实体中组件的顺序一无所知,因此

  1. 您可能会经常更新不同类型的组件(本质上是引用的代码本地性差)
  2. 该系统不能很好地进行并发更新,因为您无法识别数据依赖性,因此无法将更新分为不相关对象的本地组。

我已经考虑过为每种组件类型使用单例管理器对象,但是它具有使用单例的通常缺点,一种缓解此问题的方法是使用依赖项注入,但这听起来像是针对此问题的过大杀伤力。

这里实际上并不需要单身人士,因此应避免使用它,因为它确实有您提到的缺点。依赖注入并不是过分的-概念的核心是将对象需要的东西传递给该对象,最好是在构造函数中。您不需要为此使用重量级的DI框架(例如Ninject),只需将额外的参数传递给某个地方的构造函数。

渲染器是一个基本系统,它可能支持创建和管理与游戏中的视觉事物(可能是精灵或模型)相对应的一堆可渲染对象的生命周期。同样,物理引擎很可能对表示可以在物理模拟中移动的实体(刚性物体)的事物进行生命周期控制。每个相关系统都应以某种身份拥有这些对象,并负责对其进行更新。

您在游戏实体合成系统中使用的组件应该只是这些较低级别系统中的实例的包装器-您的位置组件可以仅包装刚体,可视组件仅可以包装可渲染的精灵或模型,等等。

然后,拥有较低级别对象的系统本身负责更新它们,并且可以批量进行更新,并且可以通过某种方式允许它对更新进行多线程处理。您的主游戏循环控制着这些系统的更新顺序(先是物理,然后是渲染器,或其他)。如果您的子系统没有生命周期或对其发出的实例没有更新控制权,则可以构建一个简单的包装器来处理与该系统相关的所有组件的更新,并决定将其放置在何处相对于系统其余部分的更新(我发现这种情况经常发生在“脚本”组件中)。

如果您需要更多详细信息,则有时将该方法称为舷外组件方法

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.