【Unity】 HTFramework框架(三十五)ECS模式

更新日期: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)
        {
        }
    }

看下代碼,你會發現RotateSystemStarComponent特性標記了兩個類型,分別是PositionComponentRotateComponent,這表示RotateSystem只會關注同時擁有這兩個組件的實體,RotateSystemOnUpdate會每幀呼叫(除非這個系統已禁用),他的參數entities就是他所關注的所有實體,換句話說,entities中的每一個實體均包含有PositionComponentRotateComponent組件。

再新建一個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一樣。

我們直接使用鼠標拖拽將PositionComponentRotateComponent添加到我們生成的實體上:
在這裏插入圖片描述

Inspector檢視面板

也可以點擊ECS_Entity面板的Open In Inspector按鈕打開檢視面板,在檢視面板添加或刪除組件,更可以直觀的在檢視面板查看此實體將會被哪些系統所關注(對於實體掛載的組件非常多的情況下,這可以快速的判斷出實體的功效)。
在這裏插入圖片描述
窗口的Components面板檢索所有組件,Systems面板檢索所有系統,比如此處,我們可以看到當前實體掛載的所有組件,以及將來會關注這個實體的系統,點擊系統欄右側的Apply To Star按鈕,可以快捷應用當前實體到此係統所關注的狀態,也即是將此係統所關注的組件全部附加到當前實體上。

旋轉實體

1.編寫系統邏輯

接下來我們在RotateSystemOnUpdate中加入如下代碼:

        /// <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.運行

我們直接運行入口場景,便可以發現我們的實體(掛載PositionComponentRotateComponent的)已經自動旋轉起來了,接下來我們想要讓輸入來控制實體的旋轉。

輸入控制旋轉實體

1.指令

HTECS爲了降低多系統之間功能代碼相互覆蓋的耦合度(比如這裏輸入系統將會關聯到旋轉系統),統一使用Order(指令)來驅動各個系統,指令分爲ID指令對象,對於一些簡單的指令,可能只需要ID就可以了,不需要獨立的指令對象(當然這不是強制性的,如果不想用,你可以完全當做沒看到這個東西)。

新建指令類:(快捷創建方式)

在這裏插入圖片描述

/// <summary>
/// 新建指令
/// </summary>
public sealed class RotateOrder : ECS_Order
{
	
}

比如這裏簡單的旋轉指令,他就不需要指令對象,只需要一個代表旋轉指令的ID即可。

    /// <summary>
    /// 指令ID
    /// </summary>
    public enum OrderID
    {
        Rotate = 1
    }

指令可以發送到一個實體上,也可以隨時給這個實體撤銷指令。

2.編寫系統邏輯

接下來我們來改寫RotateSystemOnUpdate方法:

        /// <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);
                }
            }
        }

同時編寫InputSystemOnUpdate邏輯:

        /// <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.運行

我們直接運行入口場景,按住空格鍵,便可以發現我們的實體(掛載PositionComponentRotateComponentInputComponent的)已經自動旋轉起來了,釋放空格鍵,停止旋轉。

除此之外,我們會發現,未掛載InputComponent組件的不會旋轉,因爲輸入系統不會關注他,自然不會給他發送旋轉指令,未同時掛載PositionComponentRotateComponent組件的也不會旋轉,因爲旋轉系統不會關注他,自然也不會爲他附加旋轉邏輯。

ECS Dirty

由於整個ECS環境是實時變化的(當然對於一些特殊的項目,也可能場景只有幾個實體且不會增刪),無論組件的增刪,還是實體的增刪,這些都會導致一個系統很可能不再繼續關注他之前所關注的實體(比如我在運行時動態移除了某個實體上的InputComponent組件,那麼輸入系統將不再關注這個實體),所以這就需要ECS環境重新去檢測所有的系統,找到他們所關注的實體,這個過程叫做ECS Dirty,只有在ECS環境處於Dirty狀態時,纔會觸發這個過程,且框架底層會盡可能少的去觸發ECS Dirty,除非在必要的時刻。

運行時檢視面板

在編輯器中運行時將會出現運行時檢視面板(Runtime Data),主要用以調試或數據監測,目前面板如下:
在這裏插入圖片描述
1.展示當前環境中的所有ECS系統。

  • IsEnabled:是否激活系統。
  • 顯示當前系統所關注的所有實體列表。
  • Remove按鈕:移除當前系統對此實體的關注。
  • Set Dirty按鈕:手動設置ECS環境爲Dirty狀態。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章