学习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做为逻辑维护,维护对应的组件执行相关操做.ide

 

System都是加入队列中轮询执行的,组件没有处理逻辑,没有数据,只包含状态,而物体挂上组件即包含该功能,在Unity中是否能够将系统组件,好比Image\Mesh\Render等认为是组件ComponentA\B\C,这些子组件共同组成了上图的Component,可是Component不可包含逻辑,因此一些Move\Jump\Run,是做为C#的子组件挂载在物体上,和Image\Mesh\Render等系统组件一并合为Component? 先留着这个问题,不着急慢慢看。工具

System队列轮询执行各类内部系统物理、网络,这个系统不包含数据变量System不知道实体究竟是什么,它只关心组件集合的小切片(slice,译注:能够理解为特定子集合),而后在这个切片上执行一组行为。有些实体有多达30个组件,而有些只有二、3个,System不关心数量,它只关心执行操做行为的组件的子集。System也不用关系这些Component是什么作了哪些事情,它只须要让这些组件的子集执行而已。学习

System这种按时序进行的操做(轮询),不用关心内部的执行,只须要关心状态,拿MMO人物施法距离,我不须要知道待机,前摇,蓄能,施法,表现,结束的时间节点或者设置回调执行某个后置事件,我只须要知道当前技能进行到某个状态,用状态维护System的执行进度,只要状态正确,就不用管当前的逻辑在当前帧或者下一帧执行。优化

 

处理数据

EntityAdmin是个World,存储了一个全部System的集合,和一个全部实体的哈希表。表键是实体的ID。ID是个32位无符号整形数,用来在实体管理器(Entity Array)上惟一标识这个实体。另外一方面,每一个实体也都存了这个实体ID和资源句柄(resource handle),后者是个可选字段,指向了实体对应的Asset资源(译注:这须要依赖暴雪的另外一套专门的Asset管理系统),资源定义了实体。this

 

 实体只是一个概念上的定义,指的是存在你游戏世界中的一个独特物体,是一系列组件的集合。为了方便区分不一样的实体,在代码层面上通常用一个ID来进行表示。全部组成这个实体的组件将会被这个ID标记,从而明确哪些组件属于该实体。因为其是一系列组件的集合,所以彻底能够在运行时动态地为实体增长一个新的组件或是将组件从实体中移除。好比,玩家实体由于某些缘由(可能陷入昏迷)而丧失了移动能力,只需简单地将移动组件从该实体身上移除,即可以达到没法移动的效果了。spa

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