IEntity
Entity作为ECS的基础个体,是整个框架中最大的类。同样,通过对应的 IEntity 接口类定义了Entity的基本能力。IEntity的结构如下:
虽然内容多,但由于在ECS中Entity并不承载逻辑,只是Component的容器,所以可以发现,IEntity的结构还是比较简单的,基本只包含了以下四块:
- 引用计数器IAERC,负责记录当前Entity被外部哪些对象引用
- 一些生命周期函数,如初始化、销毁等
- 一系列对自身Component的增删改查方法
- 通过几个event,在特殊事件节点回调至外部结构(主要是Context),触发特定逻辑,如前文提到Component添加移除时触发Context中对Group的更新。
涉及到的委托形式为:
Entity
Entity是IEntity接口的实现类,内容如下:
创建和销毁
从Context里我们知道,Entity生命周期结束后不会真的销毁,而是被Context回收等待复用。那就需要区分开哪些内容是Entity结构统一的,哪些内容是单个Entity实例特异的。结构统一的内容只需要在第一次创建Entity时进行一次初始化,而单个实例特异的则需要每次复用的时候重新赋值。
结构统一的内容包括:
- _contextInfo - 所属Context的基础信息,由Context传入一次自身ContextInfo的引用即可
- _totalComponents - 最多可能会存在多少种Component,其实就是所属Context一共有多少种Component,由Context传入一次即可
- _componentPools - 每种Component的对象池,由Context传入一次自身的组件对象池数组即可
- _aerc - 引用计数器,第一次创建时由Context分配一次即可
- _components - 记录当前Entity实例实际添加了哪些Component,这个数组的长度固定为_totalComponents,然后每添加一个Component,就用Component的Index作为下标,记录到该数组种,因此,只需要分配一次内存空间,只要在Entity回收前清空数组即可
实例特异的内容包括:
- _creationIndex - 实例的唯一ID,需要每次复用时都重新分配
- _isEnabled - 实例当前是否处于激活状态,复用时需要重新激活
Entity种提供了Initialize和Reactive两个方法,分别在第一次创建时和被复用时调用,Reactive方法就是对实例特异的内容进行重新赋值,而Initialize方法负责对结构统一的内容进行第一次赋值,同时又在内部调用了Reactive。
/// <summary>
/// 第一次创建时调用
/// </summary>
/// <param name="creationIndex"></param>
/// <param name="totalComponents"></param>
/// <param name="componentPools"></param>
/// <param name="contextInfo"></param>
/// <param name="aerc"></param>
public void Initialize(int creationIndex, int totalComponents, Stack<IComponent>[] componentPools, ContextInfo contextInfo = null, IAERC aerc = null) {
Reactivate(creationIndex);
_totalComponents = totalComponents;
_components = new IComponent[totalComponents];
_componentPools = componentPools;
_contextInfo = contextInfo ?? createDefaultContextInfo();
_aerc = aerc ?? new SafeAERC(this);
}
/// <summary>
/// 激活Entity
/// 第一次创建时由Initialize方法调用
/// 后续复用时由Context直接调用
/// </summary>
/// <param name="creationIndex"></param>
public void Reactivate(int creationIndex)
{
_creationIndex = creationIndex;
_isEnabled = true;
}
Entity向外提供了一个主动销毁自身的接口方法Destroy()
/// <summary>
/// Entity对外提供的销毁自己的接口
/// 这个接口的作用是通知Entity我要销毁你了
/// 并不是Entity销毁时做清理工作的接口
/// 调用时会通知Context,由Context执行销毁流程
/// 在销毁流程中回调到Entity的 InternalDestroy() 方法,执行清理
/// </summary>
/// <exception cref="EntityIsNotEnabledException"></exception>
public void Destroy()
{
if (!_isEnabled)
{
throw new EntityIsNotEnabledException("Cannot destroy " + this + "!");
}
if (OnDestroyEntity != null)
{
OnDestroyEntity(this);
}
}
这个方法只负责触发起Entity的销毁,但Entity本身并不负责真正的销毁流程,而是通过 OnDestroyEntity 事件回调至Context的DetroyEntity方法中进行实际销毁,并在销毁流程中调用Entity的InternalDestroy() 方法执行Entity自身的销毁清理操作。当前Entity实例已经添加的组件也是在这里清空的。
/// <summary>
/// Entity自身的销毁清理操作
/// Context在DestroyEntity方法中,于销毁流程的适当时机下调用
/// 当前Entity实例添加的组件也是在这里清空的
/// </summary>
public void InternalDestroy()
{
_isEnabled = false;
_name = null;
_toStringCache = null;
RemoveAllComponents();
OnComponentAdded = null;
OnComponentReplaced = null;
OnComponentRemoved = null;
OnDestroyEntity = null;
}
引用和释放
除了Context会引用Entity,Entity还可能被添加进其他结构中,如Group、Collector等。通过引用计数器,确保Entity实例已经完全没有被引用后,才会由Context回收,以免错误的复用导致数据错乱。
/// <summary>
/// 当前引用计数
/// </summary>
public int retainCount => _aerc.retainCount;
/// <summary>
/// 添加引用
/// </summary>
/// <param name="owner"></param>
public void Retain(object owner)
{
_aerc.Retain(owner);
}
/// <summary>
/// 释放引用
/// 引用计数归0时,触发OnEntityReleased事件
/// OnEntityReleased 即调用至Context的onEntityReleased方法,放入对象池等待复用
/// </summary>
/// <param name="owner"></param>
public void Release(object owner)
{
_aerc.Release(owner);
//_toStringCache = null;
if (_aerc.retainCount == 0)
{
if (OnEntityReleased != null) OnEntityReleased(this);
}
}
Component操作
Component操作无非就是对Component的增删改查。并且都是以Component的Index作为key来进行。
添加组件很简单,主要使用Add方法,以index为下标,记录到数组中,并触发 OnComponentAdded 事件,回调至Context中更新Group。
/// <summary>
/// 添加Component
/// </summary>
/// <param name="index">Component的唯一ID(类型ID)</param>
/// <param name="component"></param>
/// <exception cref="EntityIsNotEnabledException"></exception>
/// <exception cref="EntityAlreadyHasComponentException"></exception>
public void AddComponent(int index, IComponent component)
移除组件同样以index为key
/// <summary>
/// 移除Component
/// </summary>
/// <param name="index"></param>
/// <exception cref="EntityIsNotEnabledException"></exception>
/// <exception cref="EntityDoesNotHaveComponentException"></exception>
public void RemoveComponent(int index)
另外,Entity还提供了替换组件的接口:
/// <summary>
/// 替换Component
/// 没有的话会添加
/// </summary>
/// <param name="index"></param>
/// <param name="component"></param>
/// <exception cref="EntityIsNotEnabledException"></exception>
public void ReplaceComponent(int index, IComponent component)
{
if (!_isEnabled)
{
throw new EntityIsNotEnabledException(
"Cannot replace component '" +
_contextInfo.componentNames[index] + "' on " + this + "!"
);
}
if (HasComponent(index))
{
replaceComponent(index, component);
}
else if (component != null)
{
AddComponent(index, component);
}
}
这是一个相对于单纯增删通用性更强的方法,其内部会先判断要替换的下标当前是否已经有记录,没有的话视为添加组件,如果有,才会走私有的替换方法:
/// <summary>
/// 替换Component的实际执行
/// 存在三种情况
/// 1.同一个Component的引用,整体刷一遍:
/// 不需要重新设置_components数组
/// 直接走OnComponentReplaced回调
/// 2.null替换掉旧的Component:
/// 将_components数组index下标位置设置为null
/// 这个方法只在内部调用,且在调用前都提前进行了判断,不存在旧的Component为null的情况
/// 相当于Remove,只走OnComponentRemoved回调,不走OnComponentReplaced回调
/// 旧的Component走Reset方法(如果实现了IResetable接口),放回池里
/// 3.同一类型的一个新的Component引用:
/// 将_components数组index下标位置设置为新的引用
/// 走OnComponentReplaced回调
/// 旧的Component走Reset方法(如果实现了IResetable接口),放回池里
/// </summary>
/// <param name="index"></param>
/// <param name="replacement"></param>
void replaceComponent(int index, IComponent replacement)
{
//_toStringCache = null;
var previousComponent = _components[index];
if (replacement != previousComponent)
{
_components[index] = replacement;
_componentsCache = null;
if (replacement != null)
{
if (OnComponentReplaced != null)
{
OnComponentReplaced(this, index, previousComponent, replacement);
}
}
else
{
_componentIndicesCache = null;
if (OnComponentRemoved != null)
{
OnComponentRemoved(this, index, previousComponent);
}
}
// 旧的Component调用Reset方法,并清空回池
var resetablePrevComponent = previousComponent as IResetable;
if (resetablePrevComponent != null) resetablePrevComponent.Reset();
var entityIdRef = previousComponent as IEntityIdRef;
if (entityIdRef != null) entityIdRef.entityId = 0;
var modifiable = previousComponent as IModifiable;
if (modifiable != null) modifiable.modified = false;
GetComponentPool(index).Push(previousComponent);
}
else
{
if (OnComponentReplaced != null)
{
OnComponentReplaced(this, index, previousComponent, replacement);
}
}
}
上面注释里已经对这个方法的内容写的很清楚了,只多提一句,其实RemoveComponent方法内部也是调用的ReplaceComponent,只不过是将指定index替换为null。
最后,Entity还提供了几个查询当前Entity实例是否添加了指定Component的方法,除了供业务逻辑查询使用,也是Matcher进行筛选时调用的方法。
/// <summary>
/// 是否已经添加了指定类型的Component
/// </summary>
/// <param name="index"></param>
/// <returns></returns>
public bool HasComponent(int index)
/// <summary>
/// 是否已经添加了所有指定类型的Component
/// </summary>
/// <param name="indices"></param>
/// <returns></returns>
public bool HasComponents(int[] indices)
/// <summary>
/// 是否已经添加了指定类型的Component之一
/// </summary>
/// <param name="indices"></param>
/// <returns></returns>
public bool HasAnyComponent(int[] indices)