使用Entitas构建游戏框架之Entitas源码解读
前言
解耦在编程时是很重要的一个考量因素,但是如果能在框架上就增加约束,让代码看上去更清晰,那么即便有一些新手,在这种约束下,也不会耦合太严重。暴雪在GDC 2017年分享了《守望先锋》关于ECS系统的实施方案,给出了一种框架上解耦的答案。虽然这种面向数据编程的思想其实很早就有了,但是《守望先锋》成功让ECS模式更广为人知。 Entitas其实早在《守望先锋》之前就已经有了,是一个超快的实体组件系统框架(ECS),专门为c#和Unity制作。内部缓存和惊人的快速组件访问使它成为首屈一指的框架。
一、为什么要ECS
在ECS之前,大部分游戏框架都是基于面向对象的,面向对象有个特点就是封装,即把一个对象的相关的属性以及对象的行为方法放到一起。大多数游戏都有主角,在使用面向对象编程时,一般游戏主角的class都有,属性上百个,方法上百个,更是能达到上万行的代码,这就导致一个新来的程序员看到后,很难阅读,访问class的方法也散布到游戏的各个地方,耦合在了各个模块里。如下图所示,存在大量的otherClass调用MainCharacter的方法以及访问属性,代码散布到各个otherClass里面,在清理时也面临复杂的引用链难以处理的问题。 而ECS不同,ECS将面向对象中的属性跟方法分离,属性放在各个component里面,这些component包含在一个Entity里面,而方法被封装成一个一个的system,这些system处理拥有相同一些component的一组Enetity。这样找属性就去找component,找逻辑就去看相关的system,这就不需要去遍历一个上万行的文件,是多么轻松的一件事。如下图所示,整个游戏是一个world,包含一组system以及所有的entity,每个entity是部分component的集合。这样清晰的结构,即便写出一些耦合的代码,也只在某个system或者某个组件里,写功能的人不需要去阅读所有的逻辑代码以及去遍历component,在清理对象的时候,由于关系链清晰,因此也变得很容易。 ??????
二、Entitas介绍
Entitas是一个开源的,轻量级的,超快的实体组件系统框架(ECS),专门为c#和Unity制作。内部缓存和惊人的快速组件访问使它成为首屈一指的框架。
源码地址:https://github.com/sschmid/Entitas-CSharp
源码介绍与使用:
先给出github上entitas的图说明:
1. Component
Entitas中,所有的逻辑属性都要求保存在Component中,在Entitas中所有声明的component都要继承IComponent接口,而Component除了一些功能型的util方法以外,是不能有逻辑代码的。
IComponent源码如下:
namespace Entitas {
public interface IComponent {
}
}
这里我们需要添加一个interface:IResetable,原因是component都是池化对象,防止dirty:
namespace Entitas
{
public interface IResetable
{
void Reset();
}
}
因此添加一个抽象类:
using Entitas;
namespace SHH.Share.Component
{
public abstract class BaseComponent : IComponent, IResetable
{
public virtual void Reset()
{
}
}
}
我们声明一个组件可以像下面一样:
using SHH.Math;
using Entitas;
using SHH.Share.Component;
namespace SHH
{
public partial class PlayerPositionComponent : BaseComponent
{
public float3 Position;
public override void Reset()
{
base.Reset();
Position = float3.zero;
}
}
public partial class PlayerAimRotComponent : BaseComponent
{
public float3 AimRotation;
public override void Reset()
{
base.Reset();
AimRotation = float3.zero;
}
}
}
2. Entity
实体是存储数据的容器,用于表示应用程序中的某些对象。您可以以iccomponent的形式从实体中添加、替换或删除component。实体有相应的事件让您知道组件是否被添加、替换或删除。Entity继承了IEntity,其实这里IEntity也只有Entity继承。
IEntity部分源码如下:
public delegate void EntityComponentChanged(
IEntity entity, int index, IComponent component, ushort opFlags
);
public delegate void EntityComponentReplaced(
IEntity entity, int index, IComponent previousComponent, IComponent newComponent, ushort opFlags
);
public delegate void EntityEvent(IEntity entity);
public interface IEntity : IAERC {
event EntityComponentChanged OnComponentAdded;
event EntityComponentChanged OnComponentRemoved;
event EntityComponentReplaced OnComponentReplaced;
event EntityEvent OnEntityReleased;
event EntityEvent OnDestroyEntity;
int totalComponents { get; }
int creationIndex { get; }
bool isEnabled { get; }
Stack<IComponent>[] componentPools { get; }
ContextInfo contextInfo { get; }
IAERC aerc { get; }
void Initialize(int creationIndex,
int totalComponents,
Stack<IComponent>[] componentPools,
ContextInfo contextInfo = null,
IAERC aerc = null);
void Reactivate(int creationIndex);
void AddComponent(int index, IComponent component, ushort opFlags);
void RemoveComponent(int index,ushort opFlags);
void ReplaceComponent(int index, IComponent component, ushort opFlags);
IComponent GetComponent(int index);
IComponent[] GetComponents();
int[] GetComponentIndices();
bool HasComponent(int index);
bool HasComponents(int[] indices);
bool HasAnyComponent(int[] indices);
void RemoveAllComponents();
Stack<IComponent> GetComponentPool(int index);
IComponent CreateComponent(int index, Type type);
T CreateComponent<T>(int index) where T : new();
void Destroy();
void InternalDestroy();
void RemoveAllOnEntityReleasedHandlers();
}
IEntity主要描述了Entity的基本描述,比方说组件数量,component的行为,以及池化相关等
而Entity继承自IEntity,实现了上述方法,增加了字段
IComponent[] _components;
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);
}
这就是我们关键的数据组,其他的操作都是辅助_components行为的,只初始化Entity的时候,就会创建一个IComponent数组,大小由totalComponents决定。也就是说,每个entity的组件数量最开始就决定了,而每一个数组index就表示了特定的组件,这就给我们筛选相同组件的entity带来了可能。
3. Matcher
ECS中的每个System其实是对拥有相同一组component的Entity的处理,那么要匹配Entity的哪些组件呢,由Matcher指定。
Matcher有如下字段
public int[] indices {
get {
if (_indices == null) {
_indices = mergeIndices(_allOfIndices, _anyOfIndices, _noneOfIndices);
}
return _indices;
}
}
public int[] allOfIndices { get { return _allOfIndices; } }
public int[] anyOfIndices { get { return _anyOfIndices; } }
public int[] noneOfIndices { get { return _noneOfIndices; } }
public string[] componentNames { get; set; }
int[] _indices;
int[] _allOfIndices;
int[] _anyOfIndices;
int[] _noneOfIndices;
这些字段保存了一个Entity需要的Component index数组,其中
- allOfIndices :表示需要每个component index代表的组件都要有
- anyOfIndices :表示有其中的一个就行
- noneOfIndices :表示这里面的index都没有
通过这些index进行Entity 筛选:
public bool Matches(Entity entity) {
return (_allOfIndices == null || entity.HasComponents(_allOfIndices))
&& (_anyOfIndices == null || entity.HasAnyComponent(_anyOfIndices))
&& (_noneOfIndices == null || !entity.HasAnyComponent(_noneOfIndices));
}
4. Group
Group支持对上下文中的实体进行超快速过滤。当实体发生变化时,它们会不断更新,并可以立即返回实体组。假设你有成千上万个实体,你只想要那些有一个PlayerPositionComponent 的实体-只要询问这个组的上下文,它已经有结果在等待你。 Group继承自IGroup,还是先看interface:
namespace Entitas {
public delegate void GroupChanged(IGroup group, Entity entity, int index, IComponent component);
public delegate void GroupUpdated(IGroup group, Entity entity, int index, IComponent previousComponent, IComponent newComponent);
public interface IGroup : IEnumerable<Entity> {
int count { get; }
void RemoveAllEventHandlers();
event GroupChanged OnEntityAdded;
event GroupChanged OnEntityRemoved;
event GroupUpdated OnEntityUpdated;
IMatcher matcher { get; }
void HandleEntitySilently(Entity entity);
void HandleEntity(Entity entity, int index, IComponent component);
GroupChanged HandleEntity(Entity entity);
void UpdateEntity(Entity entity, int index, IComponent previousComponent, IComponent newComponent);
bool ContainsEntity(Entity entity);
Entity[] GetEntities();
Entity GetSingleEntity();
}
}
可以看到IGroup中有个matcher,一个entity的count,以及一组entity变化后的行为,在这里就应该知道,Group的主要功能是通过matcher缓存了一组Entity 下面看下Group的部分源码:
namespace Entitas {
public class Group : IGroup {
/// Returns the number of entities in the group.
public int count { get { return _entities.Count; } }
/// Returns the matcher which was used to create this group.
public IMatcher matcher { get { return _matcher; } }
readonly IMatcher _matcher;
readonly HashSet<Entity> _entities = new HashSet<Entity>(
EntityEqualityComparer.comparer
);
Entity[] _entitiesCache;
Entity _singleEntityCache;
string _toStringCache;
/// Use context.GetGroup(matcher) to get a group of entities which match
/// the specified matcher.
public Group(IMatcher matcher) {
_matcher = matcher;
}
}
}
可以看到,Group构造函数传进来了一个matcher,然后使用_entities保存了能通过matcher的Entity,而在OnEntityAdded,OnEntityRemoved的时候会对_entities进行更新
5. Collector
一个Collector可以从相同的Context中观察一个或多个Group,并且根据指定的groupEvent收集更改的实体。 其中groupEvent有如下事件:
public enum GroupEvent : byte {
Added,
Removed,
AddedOrRemoved,
Modified,
}
Collector部分源码:
public HashSet<Entity> collectedEntities { get { return _collectedEntities; } }
public int count { get { return _collectedEntities.Count; } }
readonly HashSet<Entity> _collectedEntities;
readonly IGroup[] _groups;
readonly GroupEvent[] _groupEvents;
public Collector(IGroup[] groups, GroupEvent[] groupEvents) {
_groups = groups;
_collectedEntities = new HashSet<Entity>(EntityEqualityComparer.comparer);
_groupEvents = groupEvents;
if (groups.Length != groupEvents.Length) {
throw new CollectorException(
"Unbalanced count with groups (" + groups.Length +
") and group events (" + groupEvents.Length + ").",
"Group and group events count must be equal."
);
}
_addEntityCache = addEntity;
_updateEntityCache = updateEntity;
Activate();
}
在Collector构造函数中指定了IGroup[] groups, GroupEvent[] groupEvents,每一个group对应注册一个groupEvent。随后调用Activate方法:
public void Activate() {
for (int i = 0; i < _groups.Length; i++) {
var group = _groups[i];
var groupEvent = _groupEvents[i];
switch (groupEvent) {
case GroupEvent.Added:
group.OnEntityAdded -= _addEntityCache;
group.OnEntityAdded += _addEntityCache;
break;
case GroupEvent.Removed:
group.OnEntityRemoved -= _addEntityCache;
group.OnEntityRemoved += _addEntityCache;
break;
case GroupEvent.AddedOrRemoved:
group.OnEntityAdded -= _addEntityCache;
group.OnEntityAdded += _addEntityCache;
group.OnEntityRemoved -= _addEntityCache;
group.OnEntityRemoved += _addEntityCache;
break;
case GroupEvent.Modified:
group.OnEntityUpdated -= _updateEntityCache;
group.OnEntityUpdated += _updateEntityCache;
break;
}
}
}
这样在每个groupEntity发生改变的时候,就在addEntity或者updateEntity中收集Entity到_collectedEntities中,然后就可以通过如下代码,对这些改变进行处理:
foreach (var e in collector.collectedEntities) {
// do something with all the entities
// that have been collected to this point of time
}
collector.ClearCollectedEntities();
6. Context
Context是创建和销毁实体的工厂。他保存了所有的entity对象以及group,可以使用它来过滤感兴趣的实体。 先看下Context继承的接口IContext:
public interface IContext {
event ContextEntityChanged OnEntityCreated;
event ContextEntityChanged OnEntityWillBeDestroyed;
event ContextEntityChanged OnEntityDestroyed;
event ContextGroupChanged OnGroupCreated;
int totalComponents { get; }
Stack<IComponent>[] componentPools { get; }
ContextInfo contextInfo { get; }
int count { get; }
int reusableEntitiesCount { get; }
int retainedEntitiesCount { get; }
void DestroyAllEntities();
void ResetCreationIndex();
void ClearComponentPool(int index);
void ClearComponentPools();
void Reset();
Entity CreateEntity(bool isTemp = false);
// TODO Obsolete since 0.42.0, April 2017
[Obsolete("Please use entity.Destroy()")]
void DestroyEntity(Entity entity);
bool HasEntity(Entity entity);
Entity[] GetEntities();
IGroup GetGroup(IMatcher matcher);
}
根据接口的内容基本可以确定这个类的作用,对entity的管理,component池化管理,group缓存。 然后看一下Context类的属性跟构造函数:
public class Context : IContext
{
/// The total amount of components an entity can possibly have.
/// This value is generated by the code generator,
/// e.g ComponentLookup.TotalComponents.
public int totalComponents { get { return _totalComponents; } }
/// Returns all componentPools. componentPools is used to reuse
/// removed components.
/// Removed components will be pushed to the componentPool.
/// Use entity.CreateComponent(index, type) to get a new or reusable
/// component from the componentPool.
public Stack<IComponent>[] componentPools { get { return _componentPools; } }
/// The contextInfo contains information about the context.
/// It's used to provide better error messages.
public ContextInfo contextInfo { get { return _contextInfo; } }
/// Returns the number of entities in the context.
public int count { get { return _entities.Count; } }
/// Returns the number of entities in the internal ObjectPool
/// for entities which can be reused.
public int reusableEntitiesCount { get { return _reusableEntities.Count; } }
/// Returns the number of entities that are currently retained by
/// other objects (e.g. Group, Collector, ReactiveSystem).
public int retainedEntitiesCount { get { return _retainedEntities.Count; } }
readonly int _totalComponents;
readonly Stack<IComponent>[] _componentPools;
readonly ContextInfo _contextInfo;
readonly Func<IEntity, IAERC> _aercFactory;
readonly HashSet<Entity> _entities = new HashSet<Entity>(EntityEqualityComparer.comparer);
readonly Stack<Entity> _reusableEntities = new Stack<Entity>();
readonly HashSet<Entity> _retainedEntities = new HashSet<Entity>(EntityEqualityComparer.comparer);
readonly Dictionary<string, IEntityIndex> _entityIndices;
readonly Dictionary<IMatcher, IGroup> _groups = new Dictionary<IMatcher, IGroup>();
readonly List<IGroup>[] _groupsForIndex;
readonly IGroup[] _groupForSingle;
readonly ObjectPool<List<GroupChanged>> _groupChangedListPool;
readonly Dictionary<int, Entity> _entitiesLookup = new Dictionary<int, Entity>();
int _creationIndex;
Entity[] _entitiesCache;
/// The prefered way to create a context is to use the generated methods
/// from the code generator, e.g. var context = new GameContext();
public Context(int totalComponents, int startCreationIndex, ContextInfo contextInfo, Func<IEntity, IAERC> aercFactory)
{
_totalComponents = totalComponents;
_creationIndex = startCreationIndex;
if (contextInfo != null)
{
_contextInfo = contextInfo;
if (contextInfo.componentTypeInfo.componentNames.Length != totalComponents)
{
throw new ContextInfoException(this, contextInfo);
}
}
else
{
_contextInfo = createDefaultContextInfo();
}
_aercFactory = aercFactory == null
? (entity) => new SafeAERC(entity)
: aercFactory;
_groupsForIndex = new List<IGroup>[totalComponents];
_groupForSingle = new IGroup[totalComponents];
_componentPools = new Stack<IComponent>[totalComponents];
_entityIndices = new Dictionary<string, IEntityIndex>();
_groupChangedListPool = new ObjectPool<List<GroupChanged>>(
() => new List<GroupChanged>(),
list => list.Clear()
);
// Cache delegates to avoid gc allocations
_cachedEntityChanged = updateGroupsComponentAddedOrRemoved;
_cachedComponentReplaced = updateGroupsComponentReplaced;
_cachedEntityReleased = onEntityReleased;
_cachedDestroyEntity = onDestroyEntity;
// Add listener for updating lookup
OnEntityCreated += (c, entity) => _entitiesLookup.Add(entity.creationIndex, (Entity)entity);
OnEntityDestroyed += (c, entity) => _entitiesLookup.Remove(entity.creationIndex);
}
}
代码虽然有点多,但是其实还是很清楚的,Context 中保存了的关键信息:
- readonly Stack[] _componentPools; :component Pool数组,用来缓存component
- HashSet _entities : 保存所有的entity
- readonly Stack _reusableEntities : 保存回收的Entity,再次创建时,可以从这里取
- readonly HashSet _retainedEntities : 保存还有引用的Entity,虽然被销毁了,但是还有引用,在DestroyAllEntities时会判断,如果有,证明之前有没有释放引用的情况。
- readonly Dictionary<IMatcher, IGroup> _groups : 缓存了所有Matcher对应的Group,减少GC
- readonly List[] _groupsForIndex :index表示每个组件的index,对应一个 List保存了引用这个组件的所有Group
- Entity[] _entitiesCache : 外部访问_entities ,同时保证不会修改到_entities
- int _creationIndex : 每个entity都有个唯一的creationIndex,在创建时指定
然后分析一下构造函数: 参数:
- component的总数totalComponents,
- entity标识index起始值startCreationIndex,
- 上下文的说明信息contextInfo,
- 以及自动entity引用计数工厂aercFactory,
然后构造函数对之前的变量进行初始化,以及对一些委托初始化,之后就可以根据Context处理了。
7. System
主要的逻辑处理的地方,遍历一组具有相同组件的Entity,所有System 都继承了ISystem
namespace Entitas {
/// This is the base interface for all systems.
/// It's not meant to be implemented.
public interface ISystem {
bool IsEnable();
void SetEnable(bool value);
}
}
然后可以看到ICleanupSystem,IExecuteSystem,IInitializeSystem,IReactiveSystem,ITearDownSystem这些接口继承了ISystem : 然后有个Systems类,可以注册这些system:
using System.Collections.Generic;
namespace Entitas {
/// Systems provide a convenient way to group systems.
/// You can add IInitializeSystem, IExecuteSystem, ICleanupSystem,
/// ITearDownSystem, ReactiveS
/// ystem and other nested Systems instances.
/// All systems will be initialized and executed based on the order
/// you added them.
public class Systems : IInitializeSystem, IExecuteSystem, ICleanupSystem, ITearDownSystem {
protected readonly List<IInitializeSystem> _initializeSystems;
protected readonly List<IExecuteSystem> _executeSystems;
protected readonly List<ICleanupSystem> _cleanupSystems;
protected readonly List<ITearDownSystem> _tearDownSystems;
protected bool m_IsEnable = true;
/// Creates a new Systems instance.
public Systems() {
_initializeSystems = new List<IInitializeSystem>();
_executeSystems = new List<IExecuteSystem>();
_cleanupSystems = new List<ICleanupSystem>();
_tearDownSystems = new List<ITearDownSystem>();
}
/// Adds the system instance to the systems list.
public virtual Systems Add(ISystem system) {
var initializeSystem = system as IInitializeSystem;
if (initializeSystem != null) {
_initializeSystems.Add(initializeSystem);
}
var executeSystem = system as IExecuteSystem;
if (executeSystem != null) {
_executeSystems.Add(executeSystem);
}
var cleanupSystem = system as ICleanupSystem;
if (cleanupSystem != null) {
_cleanupSystems.Add(cleanupSystem);
}
var tearDownSystem = system as ITearDownSystem;
if (tearDownSystem != null) {
_tearDownSystems.Add(tearDownSystem);
}
return this;
}
/// Calls Initialize() on all IInitializeSystem and other
/// nested Systems instances in the order you added them.
public virtual void Initialize() {
for (int i = 0; i < _initializeSystems.Count; i++) {
_initializeSystems[i].Initialize();
}
}
/// Calls Execute() on all IExecuteSystem and other
/// nested Systems instances in the order you added them.
public virtual void Execute() {
for (int i = 0; i < _executeSystems.Count; i++) {
_executeSystems[i].Execute();
}
}
/// Calls Cleanup() on all ICleanupSystem and other
/// nested Systems instances in the order you added them.
public virtual void Cleanup() {
for (int i = 0; i < _cleanupSystems.Count; i++) {
_cleanupSystems[i].Cleanup();
}
}
/// Calls TearDown() on all ITearDownSystem and other
/// nested Systems instances in the order you added them.
public virtual void TearDown() {
for (int i = 0; i < _tearDownSystems.Count; i++) {
_tearDownSystems[i].TearDown();
}
}
/// Activates all ReactiveSystems in the systems list.
public void ActivateReactiveSystems() {
//...见源码
}
/// Deactivates all ReactiveSystems in the systems list.
/// This will also clear all ReactiveSystems.
/// This is useful when you want to soft-restart your application and
/// want to reuse your existing system instances.
public void DeactivateReactiveSystems() {
//...见源码
}
/// Clears all ReactiveSystems in the systems list.
public void ClearReactiveSystems() {
//...见源码
}
public virtual bool IsEnable() { return m_IsEnable; }
public virtual void SetEnable(bool value) { m_IsEnable = value; }
}
}
三、为什么不用unity自带的ECS
- 这么多年了,unity的ECS还是preview版
- 我们ECS用于client以及server,因此逻辑核打算独立出来,不依赖unityEngine,且打算做竞技开房间类型的游戏,在服务器并不能依赖burst并发多线程去处理。
总结
这是一个系列文章,将逐步使用Entitas搭建整个游戏框架,当然对Entitas有部分改造,如去掉了TEntity泛型,全直接用Entity,后面将围绕ECS搭建整个游戏。
|