WAYNETS.ORG

Game and Program

实体组件系统(ECS)

作者:

发表于

Context Polling System Post-Processing Renderer

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