@component 构造函数_一个简单ECS(Entity-Component-System)框架的实现

文章介绍了名为Noodles的ECS框架,重点阐述了资源(Entity, Component, GlobalComponent, System, Event)的管理以及System调度的逻辑。Noodles通过资源的读写属性和执行优先级计算依赖关系,构建System执行顺序图,实现并行化执行。此外,还讨论了直接依赖和间接依赖的概念,以及如何处理依赖环问题。" 119608098,8313818,板绘初学者攻略:线条练习与临摹技巧,"['板绘', '游戏原画', 'CG原画', '数位板', '绘画技巧']

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

be2c1fda2dcb80c888710ffc882fed1b.png
BleedingChips/Noodles​github.com
39c4c18d850f132aeeb4be7e44f4884f.png

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之间计算出依赖关系,并构造出一个逻辑依赖图。其具体的执行顺序如下:

  1. 计算Layout值,数值高的System将会依赖数值低的,并称之为直接依赖。若相等,则执行下一个步骤。
  2. 计算读写冲突。具体实现为判断两个System是否同时写入或者一写一读两个同类型的资源。若没有,则称两个System之间无依赖,结束计算;若有,则称两个System存在间接依赖,并进入下一步。
  3. 计算Priority。同Layout一样,也是数值高的System将会依赖数值低的,但这个数值只有在同Layout且有读写冲突的情况下才有参考意义,所以有可能出现数值高的先于数值低的执行。若该数值不同,则按照数值决定依赖关系,若相同,则进入下一步。
  4. 至此,所有不依赖于特定System类型的依赖计算已经失效,需要根据特定的System类型和读写冲突的类型,调用tick_order函数进行最后一步的依赖计算。此时返回的结果有依赖,被依赖于,未定义,不关心,互斥五种结果,根据两个System返回的结果可以得到一个依赖关系(如果是相互矛盾的话则抛出异常以供修正)。若此时两个结果均为未定义,则按读依赖于写的默认规则进行计算。若同时写,则抛出异常。

在计算完依赖关系后,可以构建出一个依赖图,并搜索一下防止存在依赖环。若有,则抛个异常出来以供修正。至此,System之间的依赖图已经构建完毕,可以正式进入调度阶段。

在调度之前,先说明一下上文生成的直接依赖和间接依赖。所谓直接依赖表示该依赖条件并不会随着资源的增减而改变,而间接依赖则反之。间接依赖是一种优化的手段,能够忽视一些不存在的资源所形成的依赖,能够让System的并行程度更高。间接依赖的计算方式随着资源的不同而不同。例如当冲突为GobalComponent和System时,则直接查询当前的面条实例内是否存在该资源实例,若存在,则该间接依赖存在。而对Component和Entity的计算则比较复杂,因为使用Filter或EntityFilter时访问的是多个TypeGroup,所以在计算的时候需要计算其交集。若存在,则间接依赖存在。

当每帧的计算完间接依赖后,既开始根据依赖图,在多个线程中分别轮询每一个System查看是否可被执行,当所有System均被执行后,既完成了面条的一帧。

完。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值