ECS框架如何更新组件数据并通知系统?
How to update component data and inform systems in ECS framework?
我正在使用 ECS 框架开发自己的游戏引擎。
这是我使用的ECS框架:
- 实体:只是一个连接其组件的 ID。
- 组件:一个存储纯数据的结构,根本没有方法(所以我可以写.xsd来描述组件并自动生成C++结构代码)。
- 系统:处理游戏逻辑。
- EventDispacher:向订阅者发送事件(系统)
但我对系统应该如何更新组件成员并通知其他系统感到困惑?
例如,我有一个像这样的 TransformComponent:
struct TransformComponent
{
Vec3 m_Position;
Float m_fScale;
Quaternion m_Quaternion;
};
显然,如果可渲染实体的 TransformComponent 的任何成员发生变化,渲染系统也应该在渲染下一帧之前更新着色器统一 "worldMatrix"。
那么如果我在一个系统中做"comp->m_Position = ...",RenderSystem中的"notice"应该怎么改TransformComponent呢?我提出了 3 个解决方案:
成员更新后发送一个UpdateEvent,在相关System中处理该事件。这很难看,因为一旦系统修改组件数据,它必须发送这样的事件:
{
...;
TransformComponent* comp = componentManager.GetComponent<TransformComponent>(entityId);
comp->m_Position = ...;
comp->m_Quaternion = ...;
eventDispatcher.Send<TransformUpdateEvent>(...);
...;
}
将成员设为私有,并针对每个组件class,用set/get方法编写相关系统(在set方法中包装事件发送)。这样会带来很多繁琐的代码。
不做任何更改,但添加 "Movable" 组件。 RenderSystem 将在 Update() 方法中迭代地对具有 "Movable" 组件的可渲染实体进行更新。这可能无法解决其他类似问题,我不确定性能。
我想不出一个优雅的方法来解决这个问题。我应该改变我的设计吗?
我觉得这里要考虑的东西很多。我会分成几部分,先讨论你的解决方案。
关于您的解决方案 1. 考虑一下,您可以对布尔值做同样的事情,或者分配一个空组件作为标记。很多时候,在 ECS 中使用事件会使您的系统架构过于复杂。至少,我倾向于避免它,特别是在较小的项目中。请记住,充当标签的组件基本上可以被认为是一个事件。
您的解决方案 2 跟进了我们在 1 中讨论的内容。但它揭示了这种通用方法的一个问题。如果你在几个系统中更新你的 TransformComponent,你无法知道 TransformComponent 是否真的发生了变化,直到最后一个系统更新它,因为一个系统可以将它移到一个方向,而另一个系统可以将它移回,让它就像你打勾的开始一样。您可以通过在一个系统中仅更新一次 TransformComponent 来解决此问题...
这看起来像您的解决方案 3。但也许反过来。您可以在多个系统中更新 MovableComponent,稍后在您的 ECS 管道中,有一个系统读取您的 MovableComponent 并写入您的 TransformComponent。在这种情况下,重要的是只有一个系统允许在 TransformComponents 上写入。那时,有一个布尔值指示它是否已被移动,就可以完美地完成这项工作。
到这里为止,我们已经用性能(因为我们避免了在 TransformComponent 未更改时在 RenderSystem 上进行某些处理)换取内存(因为我们以某种方式复制了 TransformComponent 的内容。
- 无需添加事件、布尔值或组件即可执行相同操作的另一种方法是在 RenderSystem 中执行所有操作。基本上,在每个 RenderComponent 中,您可以保留上次更新 TransformComponent 的副本(或散列),然后进行比较。如果不一样,渲染它,并更新你的副本。
// In your RenderSystem...
if (renderComponent.lastTransformUpdate == transformComponent) {
continue;
}
renderComponent.lastTransformUpdate = transformComponent;
render(renderComponent);
最后一个是我的首选解决方案。但这也取决于您系统的特性和您的关注点。一如既往,不要试图盲目地优化性能。先测量,再比较。
我认为在这种情况下,最简单的方法将是最好的:您可以在 read/write 它的组件中保留指向 Transform
组件的指针。
我不认为使用事件(或其他间接方式,如观察者)可以解决这里的任何实际问题。
Transform
组件非常简单 - 它不会在开发过程中更改。抽象访问它实际上会使代码更复杂,更难维护。
Transform
是一个组件,对于许多对象来说会经常更改,甚至可能您的大多数对象都会在每一帧更新它。每次发生更改时发送事件都会产生成本 - 可能比简单地将 matrix/vector/quaternion 从一个位置复制到另一个位置要高得多。
我认为使用事件或其他一些抽象不会解决其他问题,例如多个组件更新同一个 Transform
组件,或组件使用过时的转换数据。
通常,渲染器只是在每一帧复制渲染对象的所有矩阵。将它们缓存在渲染系统中没有意义。
像Transform
这样的组件经常被使用。使它们过于复杂可能会在引擎的许多不同部分出现问题,而使用最简单的解决方案(指针)会给您更大的自由度。
顺便说一句,还有一种非常简单的方法可以确保 RenderComponent
在 更新后(例如 PhysicsComponent
)读取转换 - 你可以将工作分为两个步骤:
Update()
其中系统可能会修改组件,
PostUpdate()
其中系统只能从组件读取数据
例如 PhysicsSystem::Update()
可能会将转换数据复制到相应的 TransformComponent
组件,然后 RenderSystem::PostUpdate()
可以只从 TransformComponent
读取数据,而没有使用过时数据的风险。
我正在使用 ECS 框架开发自己的游戏引擎。 这是我使用的ECS框架:
- 实体:只是一个连接其组件的 ID。
- 组件:一个存储纯数据的结构,根本没有方法(所以我可以写.xsd来描述组件并自动生成C++结构代码)。
- 系统:处理游戏逻辑。
- EventDispacher:向订阅者发送事件(系统)
但我对系统应该如何更新组件成员并通知其他系统感到困惑? 例如,我有一个像这样的 TransformComponent:
struct TransformComponent
{
Vec3 m_Position;
Float m_fScale;
Quaternion m_Quaternion;
};
显然,如果可渲染实体的 TransformComponent 的任何成员发生变化,渲染系统也应该在渲染下一帧之前更新着色器统一 "worldMatrix"。 那么如果我在一个系统中做"comp->m_Position = ...",RenderSystem中的"notice"应该怎么改TransformComponent呢?我提出了 3 个解决方案:
成员更新后发送一个UpdateEvent,在相关System中处理该事件。这很难看,因为一旦系统修改组件数据,它必须发送这样的事件:
{ ...; TransformComponent* comp = componentManager.GetComponent<TransformComponent>(entityId); comp->m_Position = ...; comp->m_Quaternion = ...; eventDispatcher.Send<TransformUpdateEvent>(...); ...; }
将成员设为私有,并针对每个组件class,用set/get方法编写相关系统(在set方法中包装事件发送)。这样会带来很多繁琐的代码。
不做任何更改,但添加 "Movable" 组件。 RenderSystem 将在 Update() 方法中迭代地对具有 "Movable" 组件的可渲染实体进行更新。这可能无法解决其他类似问题,我不确定性能。
我想不出一个优雅的方法来解决这个问题。我应该改变我的设计吗?
我觉得这里要考虑的东西很多。我会分成几部分,先讨论你的解决方案。
关于您的解决方案 1. 考虑一下,您可以对布尔值做同样的事情,或者分配一个空组件作为标记。很多时候,在 ECS 中使用事件会使您的系统架构过于复杂。至少,我倾向于避免它,特别是在较小的项目中。请记住,充当标签的组件基本上可以被认为是一个事件。
您的解决方案 2 跟进了我们在 1 中讨论的内容。但它揭示了这种通用方法的一个问题。如果你在几个系统中更新你的 TransformComponent,你无法知道 TransformComponent 是否真的发生了变化,直到最后一个系统更新它,因为一个系统可以将它移到一个方向,而另一个系统可以将它移回,让它就像你打勾的开始一样。您可以通过在一个系统中仅更新一次 TransformComponent 来解决此问题...
这看起来像您的解决方案 3。但也许反过来。您可以在多个系统中更新 MovableComponent,稍后在您的 ECS 管道中,有一个系统读取您的 MovableComponent 并写入您的 TransformComponent。在这种情况下,重要的是只有一个系统允许在 TransformComponents 上写入。那时,有一个布尔值指示它是否已被移动,就可以完美地完成这项工作。
到这里为止,我们已经用性能(因为我们避免了在 TransformComponent 未更改时在 RenderSystem 上进行某些处理)换取内存(因为我们以某种方式复制了 TransformComponent 的内容。
- 无需添加事件、布尔值或组件即可执行相同操作的另一种方法是在 RenderSystem 中执行所有操作。基本上,在每个 RenderComponent 中,您可以保留上次更新 TransformComponent 的副本(或散列),然后进行比较。如果不一样,渲染它,并更新你的副本。
// In your RenderSystem... if (renderComponent.lastTransformUpdate == transformComponent) { continue; } renderComponent.lastTransformUpdate = transformComponent; render(renderComponent);
最后一个是我的首选解决方案。但这也取决于您系统的特性和您的关注点。一如既往,不要试图盲目地优化性能。先测量,再比较。
我认为在这种情况下,最简单的方法将是最好的:您可以在 read/write 它的组件中保留指向 Transform
组件的指针。
我不认为使用事件(或其他间接方式,如观察者)可以解决这里的任何实际问题。
Transform
组件非常简单 - 它不会在开发过程中更改。抽象访问它实际上会使代码更复杂,更难维护。Transform
是一个组件,对于许多对象来说会经常更改,甚至可能您的大多数对象都会在每一帧更新它。每次发生更改时发送事件都会产生成本 - 可能比简单地将 matrix/vector/quaternion 从一个位置复制到另一个位置要高得多。我认为使用事件或其他一些抽象不会解决其他问题,例如多个组件更新同一个
Transform
组件,或组件使用过时的转换数据。通常,渲染器只是在每一帧复制渲染对象的所有矩阵。将它们缓存在渲染系统中没有意义。
像Transform
这样的组件经常被使用。使它们过于复杂可能会在引擎的许多不同部分出现问题,而使用最简单的解决方案(指针)会给您更大的自由度。
顺便说一句,还有一种非常简单的方法可以确保 RenderComponent
在 更新后(例如 PhysicsComponent
)读取转换 - 你可以将工作分为两个步骤:
Update()
其中系统可能会修改组件,PostUpdate()
其中系统只能从组件读取数据
例如 PhysicsSystem::Update()
可能会将转换数据复制到相应的 TransformComponent
组件,然后 RenderSystem::PostUpdate()
可以只从 TransformComponent
读取数据,而没有使用过时数据的风险。