Entity Component System与Entity Component

参考:entity component system vs entity component
参考:Entity Component System FAQ
参考:EC vs ECS for Roguelikes
参考:ECS概述

在学习这章之前,先提出几个问题:

  • 什么是ECS
  • 什么是EC
  • ECS和EC有何区别
  • Unity里用的是不是ECS?
  • UE4里用的是不是ECS?

什么是ECS?

我学习写游戏引擎的时候,学过这一部分的内容,相关知识参考:Entity Component System | Game Engine series

Entity-Component-System (ECS) is a software architectural pattern mostly used on video game development for the storage of game world objects. An ECS follows the pattern of “entities” with “components” of data.
An ECS follows the principle of composition over inheritance, meaning that every entity is defined not by a “type”, but by the components that are associated with it. The design of how components relate to entities depend upon the Entity Component System being used.

先不谈ECS架构,先来思考思考,假设我们有一个场景Scene,里面一个是正方体,那么我可以设计这么几个类:

class Entity
{
	float[3] m_Position;
};

class Box : public Enitty {};

class Scene
{
	std::vector<Entity> m_Entities;
};

在C++里,public继承是is-a的关系,private继承是has-a的关系,那么如果我需要两个正方体,一个可以播放音频,一个可以播放动画,那么代码是这样:

class Entity 
{
	float[3] m_Position;
};

class Box : public Enitty {};

class Scene
{
	std::vector<Entity> m_Entities;
};

class BoxWithAudio : public Box {};
class BoxWithAnimator : public Box {};

当如果Box既可以播放音频、又可以播放动画,那么一共会有这么些类:

class Box : public Enitty {};
class BoxWithAudio : public Box {};
class BoxWithAnimator : public Box {};
// 出现了multiple inheritance
class BoxWithAnimatorAndAudio : public BoxWithAnimator, public BoxWithAudio {};

这里很容易就出现了一个类继承于两个类的情况,而BoxWithAnimatorBoxWithAudio 又同时继承于Box,所以这是个菱形继承。Box类才添加了两个功能就会造成这么复杂的类设计,所以说这种写法,很容易造成类混乱,更何况其他的很多语言里根本不支持多重继承。

所以,这里有个更好的设计思路,代码如下:

class Entity 
{
	std::vector<Component> m_Components;
	float[3] m_Position;
};

class Scene
{
	std::vector<Entity> m_Entities;
};

class BoxComponent : public Component {};
class AudioComponent : public Component {};
class AnimatorComponent : public Component {};

// 创建Entity
Entity boxWithAnimatorAndAudio;
boxWithAnimatorAndAudio.AddComponent(new BoxComponent());
boxWithAnimatorAndAudio.AddComponent(new AudioComponent());
boxWithAnimatorAndAudio.AddComponent(new AnimatorComponent());

此时的Entity,它需要什么功能,就会在其m_Components里添加对应的Component组件,这样代码设计就不混乱了。


上面的代码,是ECS架构的雏形,但还远远不够。假设我们的Scene里有一堆Entity,每个Entity都有自己的Mesh和自己的Position,那么我们渲染的时候,大概会这么写代码:

// 遍历Scene里的entities
for(Entity& e: m_Entities)
{
	if(e.GetMeshComponent() ! = null)
	{
		PrepareToDrawMesh(e.GetMeshComponent().mesh, e.GetPosition());
	}
}

// 批处理一起绘制Mesh
DrawMesh();

上面的代码,最大的问题是,没有Cache Friendly,由于Mesh是存在每个Entity里的,它们的内存必然是散布于计算机各个位置的,那么计算机必然经常会产生缺页情况。没有遵循计算机的局部访问性的代码,效率是很差的。

为了解决这个问题,更好的思路产生了,这里变成了data-oriented programming,Scene里相同Component的数据,应该是连续存储的,大概思路如下:

class Scene
{
	std::vector<Entity> m_Entities;
	// 真正的数据连续的存在于Scene的Data里
	std::vector<MeshComponent> m_MeshComponents;
	std::vector<AudioComponent> m_AudioComponents;
	std::vector<AnimatorComponent> m_AnimatorComponents;
};

// Entity本质就是一个UniqueId, 加上对一堆Component位置的记录
class Entity 
{
	uint32_t m_UniqueId;
	uint32_t m_AnimatorId;// 代表Entity对应的Animator在Scene里m_AnimatorComponents的id
	uint32_t m_AudioId;
	
	float[3] m_Position;
};

class BoxComponent : public Component {};
class AudioComponent : public Component {};
class AnimatorComponent : public Component {};

实际绘制的时候,代码就变成了:

// 遍历Scene里的entities
for(int i = 0; i < m_MeshComponents.count; i++)
{
	// 遍历Scene里的Entity数组, 找到包含当前遍历的MeshComponent的Entity
	const Entity& entity = FindEntityWhoseMeshIdEquals(i);
	if(entity ! = null)
	{
		...// 需要的话, 可以根据entity取得一些别的Component数据
		
		PrepareToDrawMesh(m_MeshComponents[i], entity.GetPosition());
	}
}

// 批处理一起绘制Mesh
DrawMesh();

一般来说, 一个Entity只会有一个某种类型的Component, 比如Animator、Audio组件等,所以前面的Entity里都是用单个uint32_t记录Component在对应数组里的id,但ScriptComponent一般可以有多个挂在一个Entity上,不过思路是一样的。

顺便提一句,为了方便说明,我在Entity里存了三个float,但这里的Entity.GetPosition可能仍然会产生缺页的情况。实际上应该把Transform信息也作为Component存到Scene里,Entity里不会存储任何实际数据,它的作用是把特定的Components组合起来,然后实际绘制Mesh的时候,内存可能是这样的:

Mesh | Transform | Mesh | Transform

Mesh并不是直接连续的,可能是Mesh | Transform 两两一组,分布于Memory里。



什么是EC?

EC frameworks, as typically found in game engines, are similar to ECS in that they allow for the creation of entities and the composition of components. However, in an EC framework, components are classes that contain both data and behavior, and behavior is executed directly on the component.

EC框架跟ECS非常类似,无非ECS框架里,Component只负责存Data,而EC框架里,Component除了存Data,还要存Behavior相关的逻辑代码。


Unity和UE4里用的是EC还是ECS?

网上直接看到这么个图:
在这里插入图片描述

参考:https://medium.com/ingeniouslysimple/entities-components-and-systems-89c31464240d
参考:https://forums.unrealengine.com/t/ue4-vs-entity-component-system/307744/3

核心内容都抄在这里,总之就是说Unity和UE4里都是用的EC架构,而不是ECS

Unity and Unreal are examples of popular game engines with an EC (entity-component) architecture. Annoyingly, these architectures are sometimes called Entity-Component Systems, which are easily confused with Entity-Component-System Architectures (talked about in the next section)

Unreal like Unity is a EC (notice missing the “S”) Entity-Component based Game Engine, the difference with the well-known ECS pattern is that logic is still present in components in UE4, I think it is actually possible use a full ECS approach with UE4 (even if not natively supported), the main problem would be for networked code, for which at some point you’ll need to pass from an Actor, but apart that you can treat certain Actors as components (pure data) and certain actors as systems (pure logic) without too much issues in UE4. However be aware that I saw no major experimentations with UE4 and ECS pattern so there could be some subtle issue that you will it along the way if you decide to do ECS with UE4.


顺便说一句,Unity和UE4里的Component都可以写逻辑代码,如下图所示,划线部分是可以自定义的代码,甚至UE4的Actor本身就可以在内部写逻辑代码(Unity则不可以),而且UE4里的Component可以形成父子层级关系:
在这里插入图片描述



结论

所以说,ECS里,Components基本上都是存Data的,对Components处理的逻辑代码都放到System里;而EC里,Components里不仅存Data,也存放代码逻辑,所以没有System这个概念。UE4和Unity里基本都是用的EC架构,而不是ECS系统。

posted @ 2022-12-03 13:07  弹吉他的小刘鸭  阅读(49)  评论(0编辑  收藏  举报