# 一切皆实体
目前十分流行ECS设计，主要是守望先锋的成功，引爆了这种技术。守望先锋采用了状态帧这种网络技术，客户端会进行预测，预测不准需要进行回滚，由于组件式的设计，回滚可以只回滚某些组件即可。ECS最重要的设计是逻辑跟数据的完全分离。即EC是纯数据，System实际上就是逻辑，由数据驱动逻辑。数据驱动逻辑是什么意思呢？很简单通过Update检测数据变化，通过事件机制来订阅数据变化，这就是所谓的数据驱动了。其它的特点例如缓存命中，在编写逻辑上来说并不太重要，现代游戏都用脚本，连脚本的性能都能容忍怎么会在乎缓存命中那点性能提升？ET在设计的时候吸收了这些想法，但是并不完全照搬，目前的设计是我经过长期的思考跟重构得来的，还是有些自己特色。  

传统的ECS写逻辑作者看来存在不少缺陷，比如为了复用，数据必然要拆成非常小的颗粒，会导致组件非常非常多。但是游戏是多人合作开发的，每个人基本上只熟悉自己的模块，最后可能造成组件大量冗余。还有个问题，常见的ECS是扁平式的，Entity跟Component只有一层。组件一多，开发功能可能不知道该使用哪些Component。好比一家公司，最大的是老板，老板手下带几百个人，老板不可能认识所有的人，完成一项任务，老板没法挑出自己需要的人。合理的做法是老板手下应该有几个经理，每个经理手下应该有几个主管，每个主管管理几个工人，这样形成树状的管理结构才会容易管理。这类似ET的做法，Entity可以管理Component，Component管理Entity，甚至Component还可以挂载Component。例如：人由头，身体，手，脚组成，而头又由眼睛，耳朵，鼻子，嘴巴组成。
```csharp
    Head head = human.AddComponent<Head>();
    head.AddComponent<Eye>();
    head.AddComponent<Mouse>();
    head.AddComponent<Nose>();
    head.AddComponent<Ear>();
    human.AddComponent<Body>();
    human.AddComponent<Hand>();
    human.AddComponent<Leg>();
```
ET中，所有数据都是Entity，包括Entity，Entity既可以当成组件使用，也可以当做其它Entity的孩子。通用的数据放在Entity身上作为成员，不太通用的数据可以作为组件挂在Entity身上。比如物品的设计，所有物品都有配置id，数量，等级的字段，这些字段没有必要做成组件，放在Entity身上使用会更加方便。
```csharp
    class Item: Entity
    {
        // 道具的配置Id
        public int ConfigId { get; set; }
        // 道具的数量
        public int Count { get; set; }
        // 道具的等级
        public int Level { get; set; }
    }
```
ET的这种设计数据是一种树状的结构，非常有层次，能够非常轻松的理解整个游戏的架构。顶层Game.Scene，不同模块的数据都挂载在Game.Scene上面，每个模块自身下面又可以挂载很多数据。每开发一个新功能不用思考太多，类该怎么设计，数据放在什么地方，挂载这里会不会导致冗余等等。比如我玩家需要做一个道具系统，设计一个ItemsComponent挂在Player身上即可，需要技能开发一个SpellComponent挂在Player身上。全服需要做一个活动，搞个活动组件挂在Game.Scene上面。这种设计任务分派会很简单，十分的模块化。  

# 组件的一些细节
### 1.组件的创建
组件的创建不要自己去new，应该统一使用ComponentFactory创建。ComponentFactory提供了三组方法用来创建组件Create，CreateWithParent，CreateWithId。Create是最简单的创建方式，它做了几个处理  
a. 根据组件类型构造一个组件  
b. 将组件加入事件系统，并且抛出一个AwakeSystem  
c. 是否启用对象池  
CreateWithParent在Create的基础上提供了一个Parent对象，设置到Component.Parent字段上。CreateWithId是用来创建ComponentWithId或者其子类的，在Create的基础上可以自己设置一个Id, Component在创建的时候可以选择是否使用对象池。三类工厂方法都带有一个fromPool的参数，默认是true。
### 2.组件的释放
Component都继承了一个IDisposable接口，需要注意，Component有非托管资源，删除一个Component必须调用该接口。该接口做了如下的操作  
a. 抛出Destroy System  
b. 如果组件是使用对象池创建的，那么在这里会放回对象池  
c. 从全局事件系统(EventSystem)中删除该组件，并且将InstanceId设为0  
如果组件挂载Entity身上，那么Entity调用Dispose的时候会自动调用身上所有Component的Dispose方法。  

### 3.InstanceId的作用
任何Component都带有一个InstanceId字段，这个字段会在组件构造，或者组件从对象池取出的时候重新设置，这个InstanceId标识这个组件的身份。为什么需要这么一个字段呢？有以下几个原因  
1. 对象池的存在，组件未必会释放，而是回到对象池中。在异步调用中，很可能这个组件已经被释放了，然后又被重新利用了起来，这样我们需要一种方式能区分之前的组件对象是否已经被释放，例如下面这段代码：
```csharp
		public static async ETVoid UpdateAsync(this ActorLocationSender self)
		{
			try
			{
				long instanceId = self.InstanceId;
				while (true)
				{
					if (self.InstanceId != instanceId)
					{
						return;
					}
					ActorTask actorTask = await self.GetAsync();
					
					if (self.InstanceId != instanceId)
					{
						return;
					}
					if (actorTask.ActorRequest == null)
					{
						return;
					}

					await self.RunTask(actorTask);
				}
			}
			catch (Exception e)
			{
				Log.Error(e);
			}
		}
```
while (true)中是段异步方法，await self.GetAsync()之后很可能ActorLocationSender对象已经被释放了，甚至有可能这个对象又被其它逻辑从对象池中再次利用了起来。我们这时候可以通过InstanceId的变化来判断这个对象是否已经被释放掉。  
2. InstanceId是全局唯一的，并且带有位置信息，可以通过InstanceId来找到对象的位置，将消息发给对象。这个设计将会Actor消息中利用到。这里暂时就不讲了。