1. 简介
ECS(Entity-Component-System)是一种现代游戏引擎常用的架构模式,用于高效管理大量游戏对象和行为。它特别适合用于构建高性能、易扩展的游戏逻辑系统,比如 Unity、Bevy、EnTT 等都使用ECS或其变种架构模式。
ECS把游戏对象的组织方式拆分为三个部分:
- Entity(实体):是游戏中某个对象的唯一标识(通常是一个整数 ID),它本身不存储数据。Entity将作为一个“锚点”,用于关联不同的组件。 在ECS架构中,Entity是一种中介者(或锚点),通过唯一ID将各种Component关联在一起,使它们之间能够协同工作。组件按类型集中存储,系统则批量处理满足条件的Entity,实现高效的游戏逻辑和渲染。
- Component(组件):描述某个Entity拥有的数据。ECS通常将相同类型的组件集中存放,比如:Mesh或Audio Clip等,以达到高效地批处理。
- System(系统):操作一组具有特定组件组合的 Entity,定义行为逻辑。
ECS的优点
- 高性能,缓存友好:避免继承结构频繁跳指针,遍历时是连续内存访问,对CPU Cache 更友好。
- 易于组合:只需添加/移除组件就能组合出不同的行为。
- 批处理高效:可以一口气把所有相同的组件(比如Mesh和Transform)提交给渲染器。
- 数据驱动,解耦:系统之间低耦合,各司其职,将逻辑和数据解耦。
- 易于序列化:数据是结构化的,易于保存与恢复。
为什么ECS是性能友好?
ECS的高性能来源于其内存布局,即所有同类型的组件通常在CPU中线性存储(如数组、结构体池)。当系统进行操作时,只需遍历一个数组,CPU 缓存命中率高。
应用场景举例
- 渲染系统:遍历所有有 MeshRenderer 和 Transform 的实体,发送绘制命令
- 物理系统:更新拥有 Rigidbody 和 Transform 的实体位置
- 输入系统:处理 PlayerInputComponent 的实体
额外内容
缓存行(Cache Line)
当你访问内存中的一个int(4 字节)时,CPU 实际不是只取这 4 个字节,而是一次性把它周围的一整块数据读进来。这块数据就是缓存行,通常是 64 字节。
也就是说,如果你访问 arr[0],CPU 实际把 arr[0] ~ arr[15] 一起加载到 L1 Cache(因为 16 个 int * 4 字节 = 64 字节)。
2. EnTT库
创建一个新的实体:
entt::entity entity = m_Registry.create();
向实体中添加一个组件Component,并传入构造参数mat4:
m_Registry.emplace<Component>(entity, glm::mat4(1.0f));
注册一个回调函数,在实体中添加组件Component时自动触发:
m_Registry.on_construct<Component>().connect<&OnTransformConstruct>();
查询在某个实体entity中组件Component是否存在并获取它:
if (m_Registry.has<Component>(entity))
Component& transform = m_Registry.get<Component>(entity);
返回一个只包含所有拥有Component组件的实体的视图view,并且可以遍历该视图:
auto view = m_Registry.view<Component>();
for (auto entity : view)
{
TransformComponent& transform = view.get<TransformComponent>(entity);
}
创建一个只包含具有A和B的实体组,且按A排列(数据紧凑存储):
auto group = m_Registry.group<TransformComponent>(entt::get<MeshComponent>);
for (auto entity : group)
{
auto&[transform, mesh] = group.get<TransformComponent, MeshComponent>(entity);
}
3. 应用
基本思路:在引入ECS之前,我们需要在当前Layer的函数OnUpdate()中,手动调用用于绘制四边形的函数DrawQuad()。而加入了ECS后,我们可以在当前Layer中保存并创建一个Scene,将所有的绘制指令放入Scene中的函数OnUpdate(),最后由Layer的函数OnUpdate()调用,即可完成逐帧渲染。
在Layer中,存储Scene和Entity的对象:
Ref<Scene> m_ActiveScene;
entt::entity m_SquareEntity;
在Layer的函数OnAttach()中,对它们实现初始化,并且为Entity添加两个Component,分别是TransformComponent和SpriteRendererComponent:
m_ActiveScene = CreateRef<Scene>();
auto square = m_ActiveScene->CreateEntity();
m_ActiveScene->Reg().emplace<TransformComponent>(square);
m_ActiveScene->Reg().emplace<SpriteRendererComponent>(square, glm::vec4{ 0.0f, 1.0f, 0.0f, 1.0f });
m_SquareEntity = square;
在Layer的函数OnUpdate()中,调用Scene的函数OnUpdate():
m_ActiveScene->OnUpdate(ts);
void Scene::OnUpdate(TimeStep ts)
{
auto group = m_Registry.group<TransformComponent>(entt::get<SpriteRendererComponent>);
for (auto entity : group)
{
auto& [transform, sprite] = group.get<TransformComponent, SpriteRendererComponent>(entity);
Renderer2D::DrawQuad(transform, sprite.Color);
}
}
自此,这个Scene中的所有带有组件Transform和SpriteRenderer的Entity都会在屏幕上绘制出四边形。
整个游戏引擎的宏观调用顺序为:
Application -> PushLayer()
|
Layer -> Layer::OnAttach() -> Layer::OnUpdate()
| |
Init Scene Scene::OnUpdate()
UUID(Universally Unique Identifier)组件
作用:通用唯一标识符,用于资源管理,场景序列化等方面。
在场景中创建一个新实体时,UUID组件是默认添加的。
struct IDComponentAdd commentMore actions
{
UUID ID;
IDComponent() = default;
IDComponent(const IDComponent&) = default;
};
Transform组件
作用:设置Entity的TRS值。
struct TransformComponent
{
glm::vec3 Translation = { 0.0f, 0.0f, 0.0f };
glm::vec3 Rotation = { 0.0f, 0.0f, 0.0f };
glm::vec3 Scale = { 1.0f, 1.0f, 1.0f };
TransformComponent() = default;
TransformComponent(const TransformComponent&) = default;
TransformComponent(const glm::vec3& translation) : Translation(translation) {}
glm::mat4 GetTransform() const
{
glm::mat4 translation = glm::translate(glm::mat4(1.0f), Translation);
glm::mat4 rotation = glm::toMat4(glm::quat(Rotation));
glm::mat4 scale = glm::scale(glm::mat4(1.0f), Scale);
return translation * rotation * scale;
}
};

Leave a comment