EntitasLite源码分析(一)
EntitasLite是一个轻量级的ECS框架,代码量少,结构清晰,非常适合扩展和封装,创建适合自身项目实际情况的ECS系统。源码地址:https://github.com/rocwood/Entitas-Lite.git
IContext
在EntitasLite中,Context是一个非常重要的基础概念。Context可以理解为是一个划定一系列Component的范围,也可以理解成一系列业务逻辑发生的域。
IContext定义了Context的能力,其主要内容如下:
可以看出,IContext中主要包含三部分内容:Component、Entity、Group
Component决定了Context包含的数据范围,因而也决定了Context能够支持的业务范围。一个Context内有哪些Component在创建Context的时候即已确定,因此totalComponents字段的值是固定的,可以以此值作为数组长度,为该Context中的每种Component创建一个对象池,供后续复用。
Entity的管理也由Context负责,通过CreateEntity()方法创建Entity实例,并为该实例分配一个CreationIndex作为唯一ID。这个唯一ID是Context内的一个自增ID,初始值在创建Context的时候由外部指定,既没有分段也非引用传递,因此Entity的这个唯一ID只在Context内部唯一,也能反映出框架的思想就是通过Context来管理一切。除了创建销毁,Context对其内部的Entity同样做了池化管理。
Group可以理解为Context的子集,一个Group关心Context内的一部分Component,具体关心哪些Component则由Matcher指定,因此Matcher和Group是一一对应的Key-Value关系。当发生Component的添加和移除行为时,关心该Component的Group会根据自身Matcher的规则决定是否需要将此次发生Component改变的Entity添加进自身记录(或从自身记录中移除)。框架借此得以维护更新自身的内容视图。
Context
Context是IContext的具体实现类,其主要内容如下:
几个重要的方法
/// <summary>
/// 供外部调用的创建Entity的接口
/// </summary>
public Entity CreateEntity()
{
Entity entity;
//从池中POP出来复用的Entity只需要Reactive重新激活
//第一次创建的Entity则需要走Initialize方法进行初始化
//其中的原因在看到Entity的源码时自然会了解
//但无论是哪一种,都会通过 _creationIndex++ 重新分配唯一ID
if (_reusableEntities.Count > 0)
{
entity = _reusableEntities.Pop();
entity.Reactivate(_creationIndex++);
}
else
{
entity = (Entity)Activator.CreateInstance(typeof(Entity));
entity.Initialize(_creationIndex++, _totalComponents, _componentPools, _contextInfo, _aercFactory(entity));
}
//添加新创建(或复用)的Entity到自身的HashSet记录中
_entities.Add(entity);
entity.Retain(this);
_entitiesCache = null;
//三个组件回调,当该Entity发生组件变化时触发
//回调至当前Context的对应方法中,用以更新Group,维护视图
entity.OnComponentAdded += _cachedEntityChanged;
entity.OnComponentRemoved += _cachedEntityChanged;
entity.OnComponentReplaced += _cachedComponentReplaced;
//两个Entity生命函数回调
//当Entity的引用计数清0时触发OnEntityReleased,回调至Context,由Context对Entity真正执行自动销毁等操作
//当Entity向外部提供的手动Destroy接口被调用时触发OnDestroyEntity,回调至Context,由Context真正执行销毁流程
entity.OnEntityReleased += _cachedEntityReleased;
entity.OnDestroyEntity += _cachedDestroyEntity;
if (OnEntityCreated != null)
OnEntityCreated(this, entity);
return entity;
}
可以看到,当Entity添加和移除Component时,都会触发 _cachedEntityChanged 委托,该委托实际方法如下:
/// <summary>
/// entity发生组件的添加和移除时调用
/// </summary>
/// <param name="entity"></param>
/// <param name="index"></param>
/// <param name="component"></param>
void updateGroupsComponentAddedOrRemoved(IEntity entity, int index, IComponent component)
{
//参数中的index是Component的类型ID,一个index就代表一种Component
//找出所有关心这个Component的Group
List<IGroup> _IGroupList = _groupsForIndex[index];
if (null == _IGroupList) return;
List<GroupChanged> _groupChangedDelList = _groupChangedListPool.Get();
Entity tEntity = (Entity)entity;
//对每个相关的 Group 依次尝试添加或移除 entity的操作
//这个操作在Group的HandleEntity中由Group实现
//如果操作成功,意味着该 Group 发生变化
//当 Group 发生变化时,收集本次变化具体触发了什么委托(OnEntityAdded或 OnEntityRemoved)
for (int i = 0; i < _IGroupList.Count; i++)
{
_groupChangedDelList.Add(_IGroupList[i].HandleEntity(tEntity));
}
//依次执行本次Component增减所触发的Group委托
for (int i = 0; i < _groupChangedDelList.Count; i++)
{
GroupChanged groupChangedEvent = _groupChangedDelList[i];
if (null == groupChangedEvent) continue;
groupChangedEvent(_IGroupList[i], tEntity, index, component);
}
_groupChangedListPool.Push(_groupChangedDelList);
}
上面这段代码里涉及到一个 _groupsForIndex 数组,这个数组用Component的Index(也就是Component的类型)作为下标索引,记录着都有哪些Group关心该类型的Component,在创建Group时即将Group添加进了该数组的元素中,发生的地方在下面这个方法中:
/// <summary>
/// 通过Matcher获取Group
/// </summary>
/// <param name="matcher"></param>
/// <returns></returns>
public IGroup GetGroup(IMatcher matcher)
{
IGroup group;
if (!_groups.TryGetValue(matcher, out group))
{
//如果Matcher对应的Group不存在,则立刻创建一个新的Group
group = new Group(matcher);
//先对当前已经存在的所有Entity做一遍过滤,看哪些Entity应该被包含进新Group中
var entities = GetEntities();
for (int i = 0; i < entities.Length; i++)
{
group.HandleEntitySilently(entities[i]);
}
_groups.Add(matcher, group);
//对 Matcher 涉及到的所有 Component,都标记为 Group 相关
//原因是一旦Entity发生了这些Component的改变时,都有可能引起该 Group 的变化
//Group就是在这时候被添加进 _groupsForIndex 数组的元素中的
for (int i = 0; i < matcher.indices.Length; i++)
{
int index = matcher.indices[i];
if (_groupsForIndex[index] == null)
{
_groupsForIndex[index] = new List<IGroup>();
}
_groupsForIndex[index].Add(group);
}
//调用新 group 创建的回调
if (OnGroupCreated != null)
{
OnGroupCreated(this, group);
}
}
return group;
}
Entity提供了一个供外部调用进行手动销毁的接口方法Destroy(),该方法只是用来供外部通知Entity要进行销毁,但就像Entity的创建由Context管理一样,Entity的销毁也不由本身负责,同样通过委托回调至Context中,由Context真正执行,并在销毁流程中调用Entity的清理方法。
/// <summary>
/// Entity的Destroy方法被调用时调用
/// Destroy方法是Entity向外提供的主动销毁Entity的接口
/// 当外部调用这个接口时,Entity会从这里通知Context
/// 由Context执行 DestroyEntity((Entity)entity) 方法真正执行销毁流程
/// 销毁流程里除了移除Context的各种记录外,还会调用Entity的InternalDestroy方法,执行清理
/// </summary>
/// <param name="entity"></param>
void onDestroyEntity(IEntity entity)
{
DestroyEntity((Entity)entity);
}
/// <summary>
/// 对应CreateEntity方法,是真正执行Entity销毁流程的方法
/// OnEntityWillBeDestroyed -> entity.InternalDestroy -> OnEntityDestroyed
/// </summary>
/// <param name="entity"></param>
/// <exception cref="ContextDoesNotContainEntityException"></exception>
private void DestroyEntity(Entity entity)
{
//从当前Context的HashSet记录中移除该Entity
var removed = _entities.Remove(entity);
if (!removed)
{
throw new ContextDoesNotContainEntityException(
"'" + this + "' cannot destroy " + entity + "!",
"This cannot happen!?!"
);
}
_entitiesCache = null;
//Entity清理前触发点
if (OnEntityWillBeDestroyed != null)
{
OnEntityWillBeDestroyed(this, entity);
}
//调用Entity的清理方法
//Entity在这里会清空自身的Component
entity.InternalDestroy();
//Entity清理后触发点
if (OnEntityDestroyed != null)
{
OnEntityDestroyed(this, entity);
}
if (entity.retainCount == 1)
{
//entity没有别的owner了
//释放当前Context对Entity的引用
//放回复用池里
entity.OnEntityReleased -= _cachedEntityReleased;
_reusableEntities.Push(entity);
entity.Release(this);
entity.RemoveAllOnEntityReleasedHandlers();
}
else
{
//释放当前Context对Entity的引用
//entity的引用计数还没有清0
//此时不可以放回池里
//需要放到 Retained的Set里进行记录
_retainedEntities.Add(entity);
entity.Release(this);
}
}
除此之外,Context还扩展了三个创建Collector的方法,具体Collector是啥会在后文中介绍。