更新日期:2020年6月8日。
Github源碼:[點我獲取源碼]
索引
ECS
ECS - 實體-組件-系統,此ECS非Unity的ECS,並不一定會帶來性能的提升,只是基於ECS的思想,建立在Unity現有的組件模式之上,以ECS模式進行開發可以避開項目後期繁重的繼承鏈,提升開發速度和質量、以及項目穩定性。
HTFramework的ECS(HTECS)保持與Unity官方的ECS相同的開發模式,在HTECS中可以嘗試將編碼習慣逐漸脫離OOP,在未來Unity DOTS盛起的大趨勢下可以無縫轉接,且HTECS基於Unity源生的組件結構,使得HTECS的組件和實體足夠透明,你可以像控制MonoBehaviour那樣去控制他們。
使用ECS
新建組件(ECS中的C)
ECS的組件類必須滿足以下條件:
1.繼承至ECS_Component。
2.標記ComponentName特性(選擇性,用來在檢視面板快速識別一個組件的功能)。
3.標記DisallowMultipleComponent特性(選擇性,但建議始終標記,因爲同類型的組件即使掛在一個實體上,也只會有一個組件生效)。
推薦使用快捷創建方式,所有條件會自動幫你填充:
如下,我新建了一個RotateComponent旋轉組件:
/// <summary>
/// 旋轉組件
/// </summary>
[DisallowMultipleComponent]
[ComponentName("旋轉組件")]
public sealed class RotateComponent : ECS_Component
{
/// <summary>
/// 旋轉軸
/// </summary>
public Vector3 Axle = new Vector3(0, 1, 0);
/// <summary>
/// 旋轉速度
/// </summary>
public float Speed = 1;
}
我再新建一個PositionComponent位置組件:
/// <summary>
/// 位置組件
/// </summary>
[DisallowMultipleComponent]
[ComponentName("位置組件")]
public sealed class PositionComponent : ECS_Component
{
}
位置組件裏什麼也沒有,這裏偷懶一下暫時就用Unity源生的Transform替換了。
再新建一個InputComponent輸入組件,用來接收輸入:
/// <summary>
/// 輸入組件
/// </summary>
[DisallowMultipleComponent]
[ComponentName("輸入組件")]
public sealed class InputComponent : ECS_Component
{
}
新建系統(ECS中的S)
ECS的系統類必須滿足以下條件:
1.繼承至ECS_System。
2.標記SystemName特性(選擇性,用來在檢視面板快速識別一個系統的功能)。
3.標記StarComponent特性(表明此係統所關注的組件類型,如無此特性標記,此係統自動無效)。
推薦使用快捷創建方式,所有條件會自動幫你填充:
如下,我新建了一個RotateSystem旋轉系統:
/// <summary>
/// 旋轉系統(只關注擁有PositionComponent、RotateComponent組件的實體)
/// </summary>
[StarComponent(typeof(PositionComponent), typeof(RotateComponent))]
[SystemName("旋轉系統")]
public sealed class RotateSystem : ECS_System
{
/// <summary>
/// 系統邏輯更新
/// </summary>
/// <param name="entities">系統關注的所有實體</param>
public override void OnUpdate(HashSet<ECS_Entity> entities)
{
}
}
看下代碼,你會發現RotateSystem的StarComponent特性標記了兩個類型,分別是PositionComponent和RotateComponent,這表示RotateSystem只會關注同時擁有這兩個組件的實體,RotateSystem的OnUpdate會每幀呼叫(除非這個系統已禁用),他的參數entities就是他所關注的所有實體,換句話說,entities中的每一個實體均包含有PositionComponent和RotateComponent組件。
再新建一個InputSystem輸入系統,用來處理我們的輸入:
/// <summary>
/// 輸入系統(只關注擁有InputComponent組件的實體)
/// </summary>
[StarComponent(typeof(InputComponent))]
[SystemName("輸入系統")]
public sealed class InputSystem : ECS_System
{
/// <summary>
/// 系統邏輯更新
/// </summary>
/// <param name="entities">系統關注的所有實體</param>
public override void OnUpdate(HashSet<ECS_Entity> entities)
{
}
}
新建實體(ECS中的E)
任何GameObject只要掛上ECS_Entity組件,他就成爲了一個實體,可以通過以下兩種方式生成實體:
1.編輯模式下靜態生成:
爲一個GameObject直接掛載組件ECS_Entity,然後點擊按鈕Generate ID生成ID即可,此ID在整個ECS環境中將是獨一無二的。
也可以選中一個GameObject後,點擊菜單欄選項 HTFramework -> ECS -> Mark As To Entity,快捷完成此操作。
2.運行模式下動態生成:
動態生成實體必須調用如下接口:
ECS_Entity entity = ECS_Entity.CreateEntity(target);
傳入的參數target爲此實體掛載的GameObject對象。
不能直接使用AddComponent來掛載實體組件,這會導致ID無效,從而實體無效。
爲實體掛載組件
爲實體掛載組件可以使用靜態方式也可以使用動態方式,就像添加一個MonoBehaviour一樣。
我們直接使用鼠標拖拽將PositionComponent和RotateComponent添加到我們生成的實體上:
Inspector檢視面板
也可以點擊ECS_Entity面板的Open In Inspector按鈕打開檢視面板,在檢視面板添加或刪除組件,更可以直觀的在檢視面板查看此實體將會被哪些系統所關注(對於實體掛載的組件非常多的情況下,這可以快速的判斷出實體的功效)。
窗口的Components面板檢索所有組件,Systems面板檢索所有系統,比如此處,我們可以看到當前實體掛載的所有組件,以及將來會關注這個實體的系統,點擊系統欄右側的Apply To Star按鈕,可以快捷應用當前實體到此係統所關注的狀態,也即是將此係統所關注的組件全部附加到當前實體上。
旋轉實體
1.編寫系統邏輯
接下來我們在RotateSystem的OnUpdate中加入如下代碼:
/// <summary>
/// 系統邏輯更新
/// </summary>
/// <param name="entities">系統關注的所有實體</param>
public override void OnUpdate(HashSet<ECS_Entity> entities)
{
foreach (var entity in entities)
{
RotateComponent component = entity.Component<RotateComponent>();
entity.transform.Rotate(component.Axle, component.Speed);
}
}
2.運行
我們直接運行入口場景,便可以發現我們的實體(掛載PositionComponent和RotateComponent的)已經自動旋轉起來了,接下來我們想要讓輸入來控制實體的旋轉。
輸入控制旋轉實體
1.指令
HTECS爲了降低多系統之間功能代碼相互覆蓋的耦合度(比如這裏輸入系統將會關聯到旋轉系統),統一使用Order(指令)來驅動各個系統,指令分爲ID和指令對象,對於一些簡單的指令,可能只需要ID就可以了,不需要獨立的指令對象(當然這不是強制性的,如果不想用,你可以完全當做沒看到這個東西)。
新建指令類:(快捷創建方式)
/// <summary>
/// 新建指令
/// </summary>
public sealed class RotateOrder : ECS_Order
{
}
比如這裏簡單的旋轉指令,他就不需要指令對象,只需要一個代表旋轉指令的ID即可。
/// <summary>
/// 指令ID
/// </summary>
public enum OrderID
{
Rotate = 1
}
指令可以發送到一個實體上,也可以隨時給這個實體撤銷指令。
2.編寫系統邏輯
接下來我們來改寫RotateSystem的OnUpdate方法:
/// <summary>
/// 系統邏輯更新
/// </summary>
/// <param name="entities">系統關注的所有實體</param>
public override void OnUpdate(HashSet<ECS_Entity> entities)
{
foreach (var entity in entities)
{
//如果目標實體存在旋轉指令,才執行旋轉邏輯
if (entity.IsExistOrder((int)OrderID.Rotate))
{
RotateComponent component = entity.Component<RotateComponent>();
entity.transform.Rotate(component.Axle, component.Speed);
}
}
}
同時編寫InputSystem的OnUpdate邏輯:
/// <summary>
/// 系統邏輯更新
/// </summary>
/// <param name="entities">系統關注的所有實體</param>
public override void OnUpdate(HashSet<ECS_Entity> entities)
{
//空格鍵發出旋轉指令
if (Main.m_Input.GetKeyDown(KeyCode.Space))
{
foreach (var entity in entities)
{
entity.GiveOrder((int)OrderID.Rotate);
}
}
//釋放時撤銷指令
if (Main.m_Input.GetKeyUp(KeyCode.Space))
{
foreach (var entity in entities)
{
entity.RecedeOrder((int)OrderID.Rotate);
}
}
}
GiveOrder向一個實體發起指令,RecedeOrder撤銷該實體的指令,這兩個方式都支持傳入一個Order(指令)對象,用以描述指令的具體細節或參數(比如發起攻擊指令了,該攻擊誰?)。
3.運行
我們直接運行入口場景,按住空格鍵,便可以發現我們的實體(掛載PositionComponent和RotateComponent和InputComponent的)已經自動旋轉起來了,釋放空格鍵,停止旋轉。
除此之外,我們會發現,未掛載InputComponent組件的不會旋轉,因爲輸入系統不會關注他,自然不會給他發送旋轉指令,未同時掛載PositionComponent和RotateComponent組件的也不會旋轉,因爲旋轉系統不會關注他,自然也不會爲他附加旋轉邏輯。
ECS Dirty
由於整個ECS環境是實時變化的(當然對於一些特殊的項目,也可能場景只有幾個實體且不會增刪),無論組件的增刪,還是實體的增刪,這些都會導致一個系統很可能不再繼續關注他之前所關注的實體(比如我在運行時動態移除了某個實體上的InputComponent組件,那麼輸入系統將不再關注這個實體),所以這就需要ECS環境重新去檢測所有的系統,找到他們所關注的實體,這個過程叫做ECS Dirty,只有在ECS環境處於Dirty狀態時,纔會觸發這個過程,且框架底層會盡可能少的去觸發ECS Dirty,除非在必要的時刻。
運行時檢視面板
在編輯器中運行時將會出現運行時檢視面板(Runtime Data),主要用以調試或數據監測,目前面板如下:
1.展示當前環境中的所有ECS系統。
- IsEnabled:是否激活系統。
- 顯示當前系統所關注的所有實體列表。
- Remove按鈕:移除當前系統對此實體的關注。
- Set Dirty按鈕:手動設置ECS環境爲Dirty狀態。