

ECS框架的概念就不在此处详写。若有疑问,可以直接在知乎搜索其他关于ECS的文章,或者关注知乎用户BenzzZX。(此条五毛,括号内删除)
此框架名为Noodles(以下简称面条),通过捕获System对各种资源的读写和其提供的执行优先级,自动计算System之间的依赖关系图,并根据此图调度System的执行顺序,从而达到System的并行化。
在此之前,先说一下面条内的资源,面条内的资源如下所示:
- Entity:索引,代表着一系列的Component组合。
- Component:存数据用,需要绑定一个Entity。
- GobalComponent:单例模式的Component,不需要绑定Entity,同类型唯一。
- System:逻辑执行者,并储存中间数据,以供其他System访问,同类型唯一。
- Event:消息队列,储存这一帧的消息,访问上一帧的消息,不知道为啥,反正就加了。
其中,Component的储存与Unity的ECS框架一样,Entity根据其Component的类型组合生成一个TypeGroup(Untity里边的EntityArchetype),并将相同TypeGroup的几个Entity的所有Component储存在一个Block中,各个Block用一个双向链表进行管理。而在一个Block中的内存布局如下所示:
[索引描述表][Component1数组头指针]...[ComponentN数组头指针][对齐]
[Component1数组][对齐]...[ComponentN数组][对齐]
[Entity指针数组][Component1的析构函数与移动构造函数数组]...[ComponentN的析构函数与移动构造函数数组]
这样的好处是Entity所需要储存的信息只有一个TypeGroup指针,一个Block指针和一个Index的索引即可,并且Entity的大小固定就能通过一个很简单的内存池进行管理。当然坏处是创建Component的时候需要多几次移动构造。
言归正传,为了调度System,面条需要System的执行优先级与对资源的读写属性。System的执行优先级由以下三个成员函数返回:
TickPriority tick_layout();
TickPriority tick_priority();
TickOrder tick_order(const TypeLayout& layout, const TypeInfo* conflig_type, size_t* conflig_type_count);
以上三个函数分别返回Layout,Priority与Order三个属性。为了方便与美观,以上的三个函数均可以缺省,若缺省将按照预设的默认值。其具体的实现是SFINAE的一种应用,本文将不在对此加以说明。
除此之外,我们还需要捕获System对资源的读写。面条采取的办法是直接把读写属性直接写死在类型里边。System中对资源的访问要通过各式的过滤器来访问,并将其要访问的资源和读写属性写死在类型上,所以一个System一般来说长这样:
struct SystemA{
// 缺省的优先级设置
// System的Tick函数
void operator()(Filter<A, const B>& F, Filter<B, const C>& F2, Context& C);
};
// 该System需要访问带A,B两种Component的Entity,写A读B。
// 需要访问带B,C两种Component的Entity,写B读C。其中Filter就是访问Component的过滤器。
这样只要捕获到operator()的类型就能得到System对资源的读写。这部分用模板就能很好处理,只是比较繁琐,具体的实现不在本文说明。
在捕获到优先级与读写属性之后,需要对两两System之间计算出依赖关系,并构造出一个逻辑依赖图。其具体的执行顺序如下:
- 计算Layout值,数值高的System将会依赖数值低的,并称之为直接依赖。若相等,则执行下一个步骤。
- 计算读写冲突。具体实现为判断两个System是否同时写入或者一写一读两个同类型的资源。若没有,则称两个System之间无依赖,结束计算;若有,则称两个System存在间接依赖,并进入下一步。
- 计算Priority。同Layout一样,也是数值高的System将会依赖数值低的,但这个数值只有在同Layout且有读写冲突的情况下才有参考意义,所以有可能出现数值高的先于数值低的执行。若该数值不同,则按照数值决定依赖关系,若相同,则进入下一步。
- 至此,所有不依赖于特定System类型的依赖计算已经失效,需要根据特定的System类型和读写冲突的类型,调用tick_order函数进行最后一步的依赖计算。此时返回的结果有依赖,被依赖于,未定义,不关心,互斥五种结果,根据两个System返回的结果可以得到一个依赖关系(如果是相互矛盾的话则抛出异常以供修正)。若此时两个结果均为未定义,则按读依赖于写的默认规则进行计算。若同时写,则抛出异常。
在计算完依赖关系后,可以构建出一个依赖图,并搜索一下防止存在依赖环。若有,则抛个异常出来以供修正。至此,System之间的依赖图已经构建完毕,可以正式进入调度阶段。
在调度之前,先说明一下上文生成的直接依赖和间接依赖。所谓直接依赖表示该依赖条件并不会随着资源的增减而改变,而间接依赖则反之。间接依赖是一种优化的手段,能够忽视一些不存在的资源所形成的依赖,能够让System的并行程度更高。间接依赖的计算方式随着资源的不同而不同。例如当冲突为GobalComponent和System时,则直接查询当前的面条实例内是否存在该资源实例,若存在,则该间接依赖存在。而对Component和Entity的计算则比较复杂,因为使用Filter或EntityFilter时访问的是多个TypeGroup,所以在计算的时候需要计算其交集。若存在,则间接依赖存在。
当每帧的计算完间接依赖后,既开始根据依赖图,在多个线程中分别轮询每一个System查看是否可被执行,当所有System均被执行后,既完成了面条的一帧。
完。