概述
ECS全稱Entity-Component-System,即物體-組件-系統,是一種面向資料(Data-Oriented Programming)的編程架構模式,
這種架構思想是在GDC的一篇演講《Overwatch Gameplay Architecture and Netcode》(翻成:守望先鋒的游戲架構和網路代碼)后受到了廣泛的學習討論,在代碼設計上有一個原則“組合優于繼承”,它的核心設計思想是基于這一思想的“組件式設計”,
ECS的基本型別

- Entity(物體):在ECS架構中表示“一個單位”,可以被ECS內部標識,可以掛載若干組件,
- Component(組件):掛載在Entity上的組件,負載物體某部分的屬性,是純資料結構不包含函式,
- System(系統):純函式不包含資料,只關心具有某些特定屬性(組件)的Entity,對這些屬性進行處理,
運行邏輯
某個業務系統篩選出擁有這個業務系統相關組件的物體,對這些物體的相關組件進行處理更新,
基本特點
Entity資料結構抽象:
| PosiComp | MoveComp | AttrComp | ... |
|---|---|---|---|
| Pos | Velocity | Hp | ... |
| Map | - | Mp | ... |
| - | - | ATK | ... |
- 組件內聚本業務相關的屬性,某個物體不同業務的屬性通過組件聚合在一起,
- 從資料結構角度上看,Entity類似一個2維的稀疏表,如上述Entity資料結構抽象
- OOP的思路知道型別就知道了這個物件的屬性,ECS的物體是知道了有哪些組件知道這個物體大概是什么,有點像鴨子理論:如果走路像鴨子、說話像鴨子、長得像鴨子、啄食也像鴨子,那它肯定就是一只鴨子,
- 業務系統收集所有具有本業務要求組件的Entity,集中批量的處理這些Entity的相關組件
推論
- ECS的組件式設計,是高內聚、低耦合的,對千變萬化的業務需求十分友好
- 批量處理資料在這些資料在連續記憶體的場合下對CPU快取機制友好
- 低資料耦合可以減少資源競爭對并行友好
- ECS處理資料的方式是批量處理的,一個物體需要連續處理的場合十分不友好
個人見解
個人認為ECS架構的核心是為了解決物件中復雜的聚合問題,能有效的管理代碼的復雜度,至于某些場合下的性能的提升,在大多數情況下只是錦上添花的作用(一些SLG游戲具有大量單位可能會有提升吧),它沒有傳統OOP編程模式的復雜的繼承關系造成的不必要的耦合,結構更加扁平化,相比之下更易于業務的閱讀理解和拓展,但這種技術并非是完美無缺的,它十分不擅長單個物體需要連續處理業務(如序列化等)或物體之間相互關聯等場合(如更新兩個物體的距離),而且對于一些業務邏輯相對固定的模塊或者一些底層模塊來說,松耦合和管理復雜度可能不是首要問題,有可能在設計上硬拗ECS組件式設計反而帶來困擾,對于游戲來說,ECS架構在GamePlay上的實用程度相對較高,在其他符合其特性的模塊如(網路模塊)也能提供一些不同以往的解題思路,
細節討論
單例組件
Q:有些資料只需要一份或被全域訪問等情況下,沒必要掛載在Entity上和篩選
A:使用單例組件,和其他組件一樣是純資料,但是可以通過單例全域訪問,即可以被任意系統任意訪問,
工具方法
Q:有些處理方法,不適合進行批量處理(例如計算兩個單位的距離,沒必要弄個系統每個單位都相互計算距離)
A:用工具方法,它通常是無副作用的,不會改變任何狀態,只回傳計算結果
System之間的依賴關系
Q: 假設有渲染系統和碰撞系統,要像在這一幀正確的渲染目標的位置,就需要碰撞系統先更新位置資訊,渲染系統在進行位置,需要正確處理系統間的前后依賴關系,
A:一個很自然的思路就是分層,根據不同層級的優先級進行處理,由此提出流水線(Piepline)的抽象,定義一顆樹和相關節點,系統掛載在其節點上,運行時以某種順序(先序遍歷)展開,同一個節點的系統可以并行(沒有依賴),有需要的話流水線還可以定義系統/物體/組件的初始化等其他問題,
System對Entity的篩選
Q:“原教旨主義”的ECS框架有ECS幀的概念,系統會在每一幀重新篩選需要處理的Entity,這種處理方式引起了很大的爭論,大家認為是有一些優化空間,
A:社區中幾乎沒人贊同“原教旨主義”的做法,原因很簡單:很多Entity在整個生命周期中都沒組件的增刪操作,還有相當部分有的有增刪操作的Entity其操作頻率也很低,每幀都遍歷重新篩選代價相對太過昂貴,所以有人提出了快取、分類、延遲增刪操作等思路,一種思路是:Entity的增刪/組件的增刪的操作進行快取,延遲到該系統運行時在進行評估篩選,以減少遍歷和重復操作,
Entity是否在運行期動態更改組件分類&System是否每幀篩選Entity分類
Q:并不是每個Entity運行期都會改變動態變更組件,有些Entity在運行期壓根就不變更組件,甚至它只被編譯期就知道的指定System處理,也有些System不在運行期篩選Entity,要么編譯期就知道處理哪些Entity,要么是處理一些單例組件,所以有人提出要不要對Entity和System對它們是否在運行期動態操作進行分類,以提升效率,
A:個人認為,Entity不變更組件,本身變動訊息就很少只有增刪,配合一些快取、延遲篩選等方法其實沒什么影響,不動態篩選Entity的System倒是可以分型別關閉Entity篩選,
是否加入回應式處理
Q:ECS是“自驅式”的更新,就像是U3D的Mono的Update方法更新,還有一種回應式的更新,即基于訊息事件的通知,“原教旨主義”式的ECS框架是完全自驅的,沒有訊息機制,系統之間“訊息傳遞”是通過組件的資料傳遞的,所以在處理“當進入地圖時”這種場合,只能使用“HasEnterMap”或者“Enum.EnterMap”之類的標簽,或者添加一個“EnterMapComponent"來處理,
A:個人傾向于加入一些訊息的處理機制,可以更靈活些,基本思路是:給System添加一個收件箱,收到的訊息放在收件箱的佇列里,Entity相關變更(增刪、變更組件)的一些訊息單獨使用一個佇列管道,在系統重繪的時候首先處理Entity變更訊息,進行評估篩選Entity,然后處理信箱里的其他訊息,然后在處理System的更新邏輯,
記憶體效率優化
Q:批量處理資料在物理記憶體連續的場合有利于CPU快取機制,關鍵是如何讓資料的記憶體連續,首先想到的是使用陣列,那么是組件使用陣列還是Entity使用陣列呢?
A:如果是組件使用陣列,那么當系統處理的Entity包含多個組件的話,那么記憶體訪問會在不同的陣列中“跳來跳去”,優化效果十分有限,個人認為若是一定要優化記憶體訪問,關鍵是保證組件一樣的Entity存放在連續記憶體(Chuck)中,這樣保證System訪問Entity的記憶體連續,具體實作方案可以參考U3D的ECS設計Archetype和Chuck,另外,也有物件池的優化空間,上面提到,ECS并不是主要解決性能問題的,只是順帶的,不必太過于執著,當然有也是極好的~,
Unity ECS引入了Archetype和Chuck兩個概念,Archetype即為Entity對應的所有組件的一個組合,然后多個Archetypes會打包成一個個Archetype chunk,按照順序放在記憶體里,當一個chunck滿了,會在接下來的記憶體上的位置創建一個新的chunk,因此,這樣的設計在CPU尋址時就會更容易找到Entity相關的component

原型Demo示例
using System;
using System.Collections.Generic;
using System.Threading;
namespace ECSDemo
{
public class Singleton<T> where T : Singleton<T>, new()
{
private static T inst;
public static T Inst
{
get
{
if (inst == null)
inst = new T();
return inst;
}
}
}
#region Component 組件
public class Component
{
}
public class SingleComp<T> : Singleton<T> where T : Singleton<T>, new()
{
//
}
#endregion
#region Entity 物體
public class EntityFactory
{
static long eid = 0;
public static Entity Create()
{
Entity e = new Entity(eid);
eid++;
EntityChangedMsg.Inst.Pub(e);
return e;
}
public static Entity CreatePlayer()
{
var e = Create();
e.AddComp(new PosiComp());
e.AddComp(new NameComp() { name = "Major" });
return e;
}
public static Entity CreateMonster(string name)
{
var e = Create();
e.AddComp(new PosiComp());
e.AddComp(new NameComp() { name = name });
return e;
}
}
public class Entity
{
long instID = 0;
public long InstID { get => instID; }
public Entity(long id) { instID = id; }
// 預計一個Entity組件不會很多,故使用鏈表...
List<Component> comps = new();
public void AddComp<T>(T t) where T : Component
{
comps.Add(t);
EntityChangedMsg.Inst.Pub(this);
}
public void RemoveComp<T>(T t) where T : Component
{
comps.Remove(t);
EntityChangedMsg.Inst.Pub(this);
}
public T GetComp<T>() where T : Component
{
foreach (var comp in comps)
if (comp is T) return comp as T;
return default(T);
}
public bool ContrainComp(Type type)
{
foreach (var comp in comps)
if (comp.GetType() == type) return true;
return false;
}
}
#endregion
#region System 系統
public class System
{
protected SystemMsgBox msgBox = new();
public virtual void Run()
{
msgBox.Each();
OnRun();
}
public virtual void OnRun()
{
}
}
public class SSystem : System
{
//
}
public class DSystem : System
{
protected Dictionary<long, Entity> entities = new();
protected List<Type> conds = new();
HashSet<Entity> evalSet = new();
public DSystem()
{
msgBox.Sub(EntityChangedMsg.Inst, (msg) => {
var body = (EntityChangedMsg.MsgBody)msg;
var e = body.Value;
evalSet.Add(e);
});
}
public void Evalute(Entity e)
{
var id = e.InstID;
bool test = true;
foreach (var cond in conds)
if (!e.ContrainComp(cond))
{
test = false;
break;
}
Entity cache;
entities.TryGetValue(id, out cache);
if (test)
if (cache == null) entities.Add(id, e);
else
if (cache != null) entities.Remove(id);
}
public override void Run()
{
msgBox.EachEntityMsg();
foreach (var e in evalSet)
Evalute(e);
evalSet.Clear();
msgBox.Each();
OnRun();
}
}
#endregion
#region Pipline 流水線
public class Pipeline<ENode, V>
{
public class Node<NENode, NV>
{
List<NV> items = new();
NENode node;
Node<NENode, NV> parent;
List<Node<NENode, NV>> childern = new();
public List<Node<NENode, NV>> Childern { get => childern; }
public List<NV> Items { get => items; }
public Node(NENode n)
{
node = n;
}
public void AddChild(Node<NENode, NV> c)
{
childern.Add(c);
c.parent = this;
}
public void RemoveChild(Node<NENode, NV> c)
{
childern.Remove(c);
c.parent = null;
}
public void AddItem(NV v)
{
items.Add(v);
}
public void RemoveItem(NV v)
{
items.Remove(v);
}
}
Node<ENode, V> root;
Dictionary<ENode, Node<ENode, V>> dict = new();
public Pipeline(ENode node)
{
root = new Node<ENode, V>(node);
dict.Add(node, root);
}
public void AddNode(ENode n)
{
Node<ENode, V> p = root;
AddNode(n, p);
}
public void AddNode(ENode n, Node<ENode, V> p)
{
var node = new Node<ENode, V>(n);
p.AddChild(node);
dict.Add(n, node);
}
public void AddNode(ENode n, ENode p)
{
Node<ENode, V> node;
dict.TryGetValue(p, out node);
if (node != null)
AddNode(n, node);
}
public void AddItem(ENode n, V item)
{
Node<ENode, V> node;
dict.TryGetValue(n, out node);
if (node != null)
node.AddItem(item);
}
public void RemoveItem(ENode n, V item)
{
Node<ENode, V> node;
dict.TryGetValue(n, out node);
if (node != null)
node.RemoveItem(item);
}
protected void Traveral(Action<V> action)
{
TraveralInner(root, action);
}
protected void TraveralInner(Node<ENode, V> node, Action<V> action)
{
var childern = node.Childern;
var items = node.Items;
foreach (var child in childern)
TraveralInner(child, action);
foreach (var item in items)
action(item);
}
}
public class SystemPipeline : Pipeline<ESystemNode, System>
{
public SystemPipeline(ESystemNode en) : base(en)
{
//
}
public void Update()
{
Traveral((sys) => sys.Run());
}
}
public enum ESystemNode : int
{
Root = 0,
Base = 1,
FrameWork = 2,
GamePlay = 3,
}
#endregion
#region World 世界
public class World : Singleton<World>
{
SystemPipeline sysPipe;
public void Init()
{
sysPipe = SystemPipelineTemplate.Create();
}
public void Update()
{
sysPipe.Update();
}
}
#endregion
#region Event 事件
public class Event<T> : Singleton<Event<T>>
{
List<Action<T>> actions = new();
public void Sub(Action<T> action)
{
actions.Add(action);
}
public void UnSub(Action<T> action)
{
actions.Remove(action);
}
public void Pub(T t)
{
foreach (var action in actions)
action(t);
}
}
public class EveEntityChanged : Event<Entity> { }
public interface IMsgBody
{
Type Type();
}
public interface IMsg
{
void Sub(MsgBox listener);
void UnSub(MsgBox listener);
}
public class Msg<T> : Singleton<Msg<T>>, IMsg
{
public class MsgBody : IMsgBody
{
public MsgBody(T v, Type ty) { Value = https://www.cnblogs.com/hggzhang/archive/2023/03/06/v; type = ty; }
Type type;
public T Value { private set; get; }
public Type Type()
{
return type;
}
}
List listeners = new();
public void Sub(MsgBox listener)
{
listeners.Add(listener);
}
public void UnSub(MsgBox listener)
{
listeners.Remove(listener);
}
public void Pub(T t)
{
var msgBody = new MsgBody(t, this.GetType());
foreach (var listener in listeners)
listener.OnMsg(msgBody);
}
}
public class EntityChangedMsg : Msg { }
public class MsgBox
{
protected Queue msgs = new();
protected Dictionary> handles = new();
public virtual void OnMsg(IMsgBody body)
{
msgs.Enqueue(body);
}
public void Sub(IMsg msg, Action cb)
{
msg.Sub(this);
handles.Add(msg.GetType(), cb);
}
public void UnSub(IMsg msg, Action cb)
{
msg.UnSub(this);
handles.Remove(msg.GetType());
}
public virtual void Each()
{
while (msgs.Count != 0)
{
var msg = msgs.Dequeue();
var type = msg.Type();
Action handle;
handles.TryGetValue(type, out handle);
if (handle != null)
handle(msg);
}
}
}
public class SystemMsgBox : MsgBox
{
Queue entityMsgs = new();
public override void OnMsg(IMsgBody body)
{
if (body.Type() == typeof(EntityChangedMsg))
entityMsgs.Enqueue(body);
else
msgs.Enqueue(body);
}
public void EachEntityMsg()
{
while (entityMsgs.Count != 0)
{
var msg = entityMsgs.Dequeue();
var type = msg.Type();
Action handle;
handles.TryGetValue(type, out handle);
if (handle != null)
handle(msg);
}
}
public override void Each()
{
while (msgs.Count != 0)
{
var msg = msgs.Dequeue();
var type = msg.Type();
Action handle;
handles.TryGetValue(type, out handle);
if (handle != null)
handle(msg);
}
}
}
#endregion
#region AppTest
public class AppComp : SingleComp
{
public bool hasInit;
}
public class MapComp : SingleComp
{
public bool hasInit;
public int monsterCnt = 2;
}
public class PosiComp : Component
{
public int x;
public int y;
}
public class NameComp : Component
{
public string name ="";
}
public class AppSystem : SSystem
{
public override void OnRun()
{
if (!AppComp.Inst.hasInit)
{
AppComp.Inst.hasInit = true;
Console.WriteLine("App 啟動");
}
}
}
public class SystemPipelineTemplate
{
public static SystemPipeline Create()
{
SystemPipeline pipeline = new(ESystemNode.Root);
// 基本系統
pipeline.AddNode(ESystemNode.Base, ESystemNode.Root);
pipeline.AddItem(ESystemNode.Base, new AppSystem());
pipeline.AddNode(ESystemNode.GamePlay, ESystemNode.Root);
pipeline.AddItem(ESystemNode.GamePlay, new PlayerSystem());
pipeline.AddItem(ESystemNode.GamePlay, new MapSystem());
return pipeline;
}
}
public class MapSystem : DSystem
{
public MapSystem() : base()
{
conds.Add(typeof(PosiComp));
conds.Add(typeof(NameComp));
}
public override void OnRun()
{
if (!MapComp.Inst.hasInit)
{
MapComp.Inst.hasInit = true;
for (int i = 0; i < MapComp.Inst.monsterCnt; i++)
EntityFactory.CreateMonster($"Monster{i + 1}");
Console.WriteLine($"進入地圖 生成{MapComp.Inst.monsterCnt}只小怪");
}
foreach (var (id, e) in entities)
{
var name = e.GetComp<NameComp>().name;
var x = e.GetComp<PosiComp>().x;
var y = e.GetComp<PosiComp>().y;
Console.WriteLine($"【{name}】 在地圖的 x = {x}, y = {y}");
}
}
}
public class PlayerComp : SingleComp<PlayerComp>
{
public Entity Major;
}
public class PlayerSystem : SSystem
{
public override void OnRun()
{
base.OnRun();
if (PlayerComp.Inst.Major == null)
PlayerComp.Inst.Major = EntityFactory.CreatePlayer();
if (Console.KeyAvailable)
{
int dx = 0;
int dy = 0;
ConsoleKeyInfo key = Console.ReadKey(true);
switch (key.Key)
{
case ConsoleKey.A:
dx = -1;
break;
case ConsoleKey.D:
dx = 1;
break;
case ConsoleKey.W:
dy = 1;
break;
case ConsoleKey.S:
dy = -1;
break;
default:
break;
}
if (dx != 0 || dy != 0)
{
var comp = PlayerComp.Inst.Major.GetComp<PosiComp>();
if (comp != null)
{
Console.WriteLine($"玩家移動 Delta X = {dx}, Delta Y = {dy}");
comp.x += dx;
comp.y += dy;
}
}
}
}
}
#endregion
class Program
{
static void Main(string[] args)
{
World.Inst.Init();
while (true)
Loop();
}
public static void Loop()
{
World.Inst.Update();
Console.WriteLine("--------------------------------------------");
Thread.Sleep(1000);
}
}
}
- Demo包含了ECS的基本定義和分層、篩選、訊息等機制,簡單的原型多看下應該可以看明白,
- 當XXX的訊息使用組件的資料HasInit實作,當然也可以使用訊息,思路是:給System加虛函式Awake、Start、End、Destory等虛函式,SystemPipeline初始化時兩次遍歷分別Awake、Start,同樣,清理時兩次遍歷呼叫End、Destory函式,可以在Start時監聽一些訊息,在End時清理,
- Pipeline流水線有一種更加自動化的系結節點的方法:使用C#的特性(Attribute)標記System,在程式啟動通過反射自動組裝,大概類似這樣:
[AttributeUsage(AttributeTargets.Class)]
public class SystemPipelineAttr : Attribute
{
public ESystemNode Type;
public SystemPipelineAttr(Type type = null)
{
this.Type = type;
}
}
[SystemPipelineAttr(ESystemNode.GamePlay)]
public class MapSystem {} // ...
// ...
public static Dictionary<string, Type> GetAssemblyTypes(params Assembly[] args)
{
Dictionary<string, Type> types = new Dictionary<string, Type>();
foreach (Assembly ass in args)
{
foreach (Type type in ass.GetTypes())
{
types[type.FullName] = type;
}
}
return types;
}
// ...
foreach (Type type in types[typeof (SystemPipelineAttr)])
{
object[] attrs = type.GetCustomAttributes(typeof(SystemPipelineAttr), false);
foreach (object attr in attrs)
{
SystemPipelineAttr attribute = attr as SystemPipelineAttr;
// ...
}
}
備注
- ECS的架構目前使用的非常的多,很多有名的框架設計都或多或少的受到了其影響,有:
- U3D的ECS架構:不是指原來的GameObj那套,有專門的插件,有記憶體優化
- UE4的組件設計:采用了特殊的組件實作父子關系
- ET框架:訊息 + ECS,采用ECS解耦,更注重訊息驅動的回應式設計,Entity和Comp的思路也獨特:Entity同時是組件,并有父子關系
- 云風大佬的引擎:好像未開源,只有一些blog在討論ECS,貌似連引擎層面和Lua側都涉及ECS的設計思想
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/545987.html
標籤:其他
