我建议您先阅读迈克·阿克顿的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搜索是您的朋友。