学习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()内的各种变量数据的使用顺序。

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