學習Unity 2019 ECS 框架(概念)

申明:該篇是學習筆記,內容多處複製引用。

ECS(Entity,Component,System)架構其實已經不是新鮮事物,只是在GDC 2017守望先鋒講座後,才真正流行或者說是被大衆所知,我接觸已經是非常晚的2019年,Unity 出了自帶ECS框架。

守望先鋒使用ECS是用來降低不停增長的代碼庫的複雜度(譯註,代碼複雜度的概念需要讀者自行查閱)。爲了達到這個目的我們遵循了一套嚴謹的架構。最後會通過討論網絡同步(netcode)這個本質很複雜的問題,來說明具體如何管理複雜性。

 

ECS架構簡述

ECS架構看起來就是這樣子的。先有個World,它是系統(譯註,這裏的系統指的是ECS中的S,不是一般意義上的系統,爲了方便閱讀,下文統稱System)和實體(Entity)的集合。而實體就是一個ID,這個ID對應了組件(Component)的集合。組件用來存儲遊戲狀態並且沒有任何的行爲(Behavior)。System有行爲但是沒有狀態。

- Entity是實例,作爲承載組件的載體,也是框架中維護對象的實體.
- Component只包含數據,具備這個組件便具有這個功能.
- System作爲邏輯維護,維護對應的組件執行相關操作.

 

System都是加入隊列中輪詢執行的,組件沒有處理邏輯,沒有數據,只包含狀態,而物體掛上組件即包含該功能,在Unity中是否可以將系統組件,比如Image\Mesh\Render等認爲是組件ComponentA\B\C,這些子組件共同組成了上圖的Component,但是Component不可包含邏輯,所以一些Move\Jump\Run,是作爲C#的子組件掛載在物體上,和Image\Mesh\Render等系統組件一併合爲Component? 先留着這個問題,不着急慢慢看。

System隊列輪詢執行各種內部系統物理、網絡,這個系統不包含數據變量System不知道實體到底是什麼,它只關心組件集合的小切片(slice,譯註:可以理解爲特定子集合),然後在這個切片上執行一組行爲。有些實體有多達30個組件,而有些只有2、3個,System不關心數量,它只關心執行操作行爲的組件的子集。System也不用關係這些Component是什麼做了哪些事情,它只需要讓這些組件的子集執行而已。

System這種按時序進行的操作(輪詢),不用關心內部的執行,只需要關心狀態,拿MMO人物施法距離,我不需要知道待機,前搖,蓄能,施法,表現,結束的時間節點或者設置回調執行某個後置事件,我只需要知道當前技能進行到某個狀態,用狀態維護System的執行進度,只要狀態正確,就不用管當前的邏輯在當前幀或者下一幀執行。

 

處理數據

EntityAdmin是個World,存儲了一個所有System的集合,和一個所有實體的哈希表。表鍵是實體的ID。ID是個32位無符號整形數,用來在實體管理器(Entity Array)上唯一標識這個實體。另一方面,每個實體也都存了這個實體ID和資源句柄(resource handle),後者是個可選字段,指向了實體對應的Asset資源(譯註:這需要依賴暴雪的另一套專門的Asset管理系統),資源定義了實體。

 

 實體只是一個概念上的定義,指的是存在你遊戲世界中的一個獨特物體,是一系列組件的集合。爲了方便區分不同的實體,在代碼層面上一般用一個ID來進行表示。所有組成這個實體的組件將會被這個ID標記,從而明確哪些組件屬於該實體。由於其是一系列組件的集合,因此完全可以在運行時動態地爲實體增加一個新的組件或是將組件從實體中移除。比如,玩家實體因爲某些原因(可能陷入昏迷)而喪失了移動能力,只需簡單地將移動組件從該實體身上移除,便可以達到無法移動的效果了。

  • Player(Position, Sprite, Velocity, Health)
  • Enemy(Position, Sprite, Velocity, Health, AI)
  • Tree(Position, Sprite)

以Player爲例,Player在做什麼,是否處於某個位置,時間節點什麼,是由System來判定/記錄,Entity沒有任何數據處理,單純只是保存這個物件執行所需的數值。

 

系統中的邏輯

一個系統就是對擁有一個或多個相同組件的實體集合進行操作的工具,它只有行爲,沒有狀態,即不應該存放任何數據。舉個例子,遊戲中玩家要操作對應的角色進行移動,由上面兩部分可知,角色是一個實體,其擁有位置和速度組件,那麼怎麼根據實體擁有的速度去刷新其位置呢,MoveSystem(移動系統)登場,它可以得到所有擁有位置和速度組件的實體集合,遍歷這個集合,根據每一個實體擁有的速度值和物理引擎去計算該實體應該所處的位置,並刷新該實體位置組件的值,至此,完成了玩家操控的角色移動了。

 

System從Entity中讀取數據,交由Component處理,System自己維護這些數據存取/Component處理執行的狀態,如果是一幀內執行完畢的行爲,System甚至不需要緩存它的狀態就已經設爲完成狀態從System隊列中移除。

 

Singleton Component (單例組件) ,明白了系統的概念更容易說明,還是玩家操作角色的例子,該實體速度組件的值從何而來,一般情況下是根據玩家的操作輸入去賦予對應的數值。這裏就涉及到一個新組件InputComponent(輸入組件)和一個新系統ChangePlayerVelocitySystem(改變玩家速度系統),改變玩家速度系統會根據輸入組件的值去改變玩家速度,假設還有一個系統FireSystem(開火系統),它會根據玩家是否輸入開火鍵進行開火操作,那麼就有 2 個系統同時依賴輸入組件,真實遊戲情況可能比這還要複雜,有無數個系統都要依賴於輸入組件,同時擁有輸入組件的實體在遊戲中僅僅需要有一個,每幀去刷新它的值就可以了,這時很容易讓人想到單例模式(便捷地訪問、只有一個引用),同樣的,單例組件也是指整個遊戲世界中有且只有一個實體擁有該組件,並且希望各系統能夠便捷的訪問到它,經過一些處理,在任何系統中都能通過類似world->GetSingletonInput()的方法來獲得該組件引用。

 

 

用組件實現行爲

 ECS中的組件更加像是一堆數據集合,它的目的是協助真實的遊戲引擎component實現各種行爲功能,也就是說

Component(組件)只包含數據

ComponentSystem 則包含行爲,一個 ComponentSystem 更新所有與之組件類型匹配的GameObject。

維基上對component的解釋:

Component: the raw data for one aspect of the object, and how it interacts with the world. "Labels the Entity as possessing this particular aspect". Implementations typically use structs, classes, or associative arrays.

舉個例子:

using Unity.Entities;
using UnityEngine;

// 數據:可以在Inspector窗口中編輯的旋轉速度值
class Rotator : MonoBehaviour
{
    public float Speed;
}

// 行爲:繼承自ComponentSystem來處理旋轉操作
class RotatorSystem : ComponentSystem
{
    struct Group
    {
        // 定義該ComponentSystem需要獲取哪些components
        public Transform Transform;
        public Rotator   Rotator;
    }

    override protected void OnUpdate()
    {
        // 這裏可以看第一個優化點:
        // 我們知道所有Rotator所經過的deltaTime是一樣的,
        // 因此可以將deltaTime先保存至一個局部變量中供後續使用,
        // 這樣避免了每次調用Time.deltaTime的開銷。
        float deltaTime = Time.deltaTime;

        // ComponentSystem.GetEntities<Group>可以高效的遍歷所有符合匹配條件的GameObject
        // 匹配條件:即包含Transform又包含Rotator組件(在上面struct Group中定義)
        foreach (var e in GetEntities<Group>())
        {
            e.Transform.rotation *= Quaternion.AngleAxis(e.Rotator.Speed * deltaTime, Vector3.up);
        }
    }
}

上面的代碼實現了一個包含game component的組件,而ComponentSystem則是system對衆多組件的處理,System對每一個ComponentSysten都有單獨的OnUpdate()方法,不需要再像傳統MonoBehaviour那樣順序執行各種邏輯大雜燴,也不需維護OnUpdate()內的各種變量數據的使用順序。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章