☰
Flecs 层级系统(Hierarchies)完全指南:ChildOf 与 Parent 两种存储、递归遍历与查询优化
2026/9/25 16:55:59 网站建设 项目流程
  • 游戏开发

【免费下载链接】flecs

A fast entity component system (ECS) for C & C++

项目地址:https://gitcode.com/gh_mirrors/fl/flecs
点击查看免费下载

层级结构是 Flecs 这个 C/C++ ECS(Entity Component System)引擎中最常用的组织手段之一。本指南以仓库中的 HierarchiesManual.md 为主体,结合 Flecs 源码与官方示例,系统讲解如何通过内建的(ChildOf, parent)关系对实体进行父子编排,深入剖析OrderedChildren有序存储、以及ChildOf与Parent两种层级存储的实现原理与性能取舍,并给出深度优先 / 广度优先遍历、命名空间解析与查询优化的完整实战方案。读完本文,你将掌握在 Flecs 中构建场景树、Prefab 资源树与 UI 树,并针对查询性能进行存储选型的完整能力。

引言:Flecs 层级结构的定位

在 Flecs 中,实体(Entity)可以通过内建的层级实现被组织成父/子层级结构。这种结构非常实用,典型用途包括:

  • 将实体组织成场景(Scene);
  • 将组件(Component)、系统(System)、观察器(Observer)、Prefab 组织成模块(Module);
  • 表达资源层级(Asset hierarchy,参见 Prefabs 手册);
  • 表达 UI 控件层级;
  • 表达层级化组织的数据(例如文件系统)。

从实现上看,层级结构是建立在 Flecs 关系(Relationship)之上的:内建的EcsChildOf(即flecs::ChildOf)关系对是层级的基石。这一点在官方示例中有直接印证,例如 examples/c/entities/hierarchy/src/main.c 中注释明确写到:“Hierarchies use ECS relationships and the builtin flecs::ChildOf relationship to create entities as children of other entities.”

创建简单的层级结构

最简单的层级创建方式,是给子实体挂上一个(ChildOf, parent)关系对:

// C ecs_entity_t spaceship = ecs_new(world); ecs_entity_t cockpit = ecs_new_w_pair(world, EcsChildOf, spaceship);
// C++ flecs::entity spaceship = world.entity(); flecs::entity cockpit = world.entity().child_of(spaceship);

在 C 中还可以通过实体描述符ecs_entity_desc_t的.parent字段一步到位地创建带名字的层级子实体,例如官方示例 examples/c/entities/hierarchy/src/main.c 中创建了一棵“太阳系”层级树(Sun → Mercury/Venus/Earth → Moon)。C++ 侧对应world.entity(parent, name)的写法,见 examples/cpp/entities/hierarchy/src/main.cpp。

获取父实体与遍历子实体

Flecs 为“找父亲”和“数孩子”提供了对称的 API:

// C:获取实体的父实体 ecs_entity_t parent = ecs_get_parent(world, entity); // C:遍历实体的所有子实体 ecs_iter_t it = ecs_children(world, parent); while (ecs_children_next(&it)) { for (int i = 0; i < it.count; i ++) { ecs_entity_t child = it.entities[i]; // ... } }
// C++:获取实体的父实体 flecs::entity parent = entity.parent(); // C++:遍历实体的所有子实体 parent.children([](flecs::entity child) { // ... });

对应 C API 的原型定义可以在 include/flecs.h(ecs_get_parent)、include/flecs.h(ecs_children/ecs_children_next)中查到。其中ecs_children默认使用EcsChildOf关系,也可以通过ecs_children_w_rel(world, rel, parent)指定自定义关系来遍历(如ecs_children_w_rel(world, EcsChildOf, parent),见 include/flecs.h 的说明)。

递归清理:删除父实体时的级联行为

当父实体被删除时,它的所有子实体会以“深度优先、后删除父”的顺序被递归删除(子实体先于父实体被删除):

// C ecs_entity_t spaceship = ecs_new(world); ecs_entity_t cockpit = ecs_new_w_pair(world, EcsChildOf, spaceship); ecs_entity_t pilot = ecs_new_w_pair(world, EcsChildOf, cockpit); ecs_delete(world, spaceship); ecs_is_alive(world, spaceship); // false ecs_is_alive(world, cockpit); // false ecs_is_alive(world, pilot); // false
// C++ flecs::entity spaceship = world.entity(); flecs::entity cockpit = world.entity().child_of(spaceship); flecs::entity pilot = world.entity().child_of(cockpit); spaceship.destruct(); spaceship.is_alive(); // false cockpit.is_alive(); // false pilot.is_alive(); // false

这条语义保证了场景树、UI 树等层级结构在整体销毁时不会留下“孤儿”实体或悬挂引用。值得注意的补充点:Parent层级存储(见下文)同样继承“子随父删”的递归清理行为,并且当父实体被删除时,子实体的删除是批量执行的,无需逐个扫描子实体向量,因此删除父实体的开销远低于逐个删除子实体。

遍历方式:深度优先与广度优先

深度优先遍历(Depth-First)

层级结构可以按深度优先方式遍历:递归地迭代每个父实体的子实体。完整的可运行示例参见:

  • C:examples/c/entities/hierarchy/src/main.c(其中的iterate_tree函数递归遍历整棵树并逐层累加坐标)
  • C++:examples/cpp/entities/hierarchy/src/main.cpp

C 示例中iterate_tree的核心是ecs_children+ecs_children_next的递归调用(examples/c/entities/hierarchy/src/main.c),C++ 侧则是e.children(...)回调内的递归(examples/cpp/entities/hierarchy/src/main.cpp)。

广度优先遍历(Breadth-First)

广度优先遍历通过查询(Query)的cascade特性实现。cascade会保证查询按“父在前、子在后”的顺序迭代实体。相关说明见 Queries 文档 的 relationship traversal 一节,可运行示例参见:

  • C:examples/c/queries/hierarchies/src/main.c
  • C++:examples/cpp/queries/hierarchy/src/main.cpp

以 C++ 示例为例,一条带cascade的层级查询如下(examples/cpp/queries/hierarchy/src/main.cpp):

flecs::query<const LocalTransform, const WorldTransform*, WorldTransform> q = ecs.query_builder<const LocalTransform, const WorldTransform*, WorldTransform>() // 从父实体上取第二个查询参数(const WorldTransform) // cascade() 保证先遍历父、再遍历子 .term_at(1).parent().cascade() .build();

该示例计算了太阳系各天体的世界坐标(Sun: {1,1}、Mercury: {2,2}、Venus: {3,3}、Earth: {4,4}、Moon: {4.1,4.1}),是最经典的“从局部坐标合成世界坐标”的层级系统用例。C 版本使用.src.id = EcsCascade表达同样的语义,并将父项设为EcsOptional以让根实体也能被匹配到,见 examples/c/queries/hierarchies/src/main.c。

命名空间(Namespacing)

实体是有名字的,可以在世界中按名字查找。名字根据实体层级进行命名空间限定:例如一个名为Cockpit的实体,其父实体名为SpaceShip,那么在 C 中需要以SpaceShip.Cockpit查找,C++ 中以SpaceShip::Cockpit查找。

对应示例(examples/c/entities/hierarchy/src/main.c 与 examples/cpp/entities/hierarchy/src/main.cpp)中,ecs_lookup(ecs, "Sun.Earth.Moon")/ecs.lookup("Sun::Earth::Moon")都能正确定位到月球实体。更完整的命名规则说明参见 Entities 与 Components 手册 的 names 一节。

从源码看,名字索引是按父实体分别维护的:flecs_on_reparent_update_name会在实体被重新挂到新父实体时,将实体从旧父的名字索引中移除并加入新父的名字索引(src/storage/non_fragmenting_childof.c),这也解释了为什么层级中的名字解析总是相对父实体进行的。

OrderedChildren:保持稳定的子实体顺序

默认情况下,ECS 的实体是存放在 Archetype(表)中的,而实体的表归属会随其组件集合的变化而改变。因此,如果子实体在创建后被添加/移除组件,ecs_children/children()返回的子实体顺序可能发生变化。

OrderedChildrentrait 用于解决这一问题:把它加到父实体上后,ecs_children/entity::children返回的实体 id 将保持创建顺序(或应用自定义的顺序),且子实体会在单个结果(一次迭代)中全部返回。该 trait 不影响查询(Query)返回实体的顺序。

// C ecs_entity_t parent = ecs_new(world); ecs_add_id(world, parent, EcsOrderedChildren); ecs_entity_t child_1 = ecs_new_w_pair(world, EcsChildOf, parent); ecs_entity_t child_2 = ecs_new_w_pair(world, EcsChildOf, parent); ecs_entity_t child_3 = ecs_new_w_pair(world, EcsChildOf, parent); // 添加/移除组件通常会改变子实体的迭代顺序, // 但有了 OrderedChildren trait 顺序会被保留。 ecs_set(world, child_2, Position, {10, 20}); ecs_iter_t it = ecs_children(world, parent); while (ecs_children_next(&it)) { // it.table 在迭代有序子实体时会被置为 NULL for (int i = 0; i < it.count; i ++) { ecs_entity_t e = it.entities[i]; // i == 0: child_1 // i == 1: child_2 // i == 2: child_3 } }
// C++ flecs::entity parent = world.entity().add(flecs::OrderedChildren); flecs::entity child_1 = world.entity().child_of(parent); flecs::entity child_2 = world.entity().child_of(parent); flecs::entity child_3 = world.entity().child_of(parent); child_2.set(Position{10, 20}); parent.children([](flecs::entity child) { // 1st result: child_1 // 2nd result: child_2 // 3rd result: child_3 });

应用还可以通过ecs_set_child_order/entity::set_child_order操作主动修改存储的子实体顺序。该操作对应的 C API 声明在 include/flecs.h。

源码实现视角

有序子实体的存储实现在 src/storage/ordered_children.c:

  • 每个(ChildOf, parent)关系的组件记录(ecs_component_record_t)中维护一个vector<ecs_entity_t>(字段pair->ordered_children),子实体按追加顺序存放(flecs_ordered_entities_append,见 src/storage/ordered_children.c);
  • 当实体添加了OrderedChildrentrait 时,首次访问会调用flecs_ordered_children_populate把该父实体现有的子实体按当前顺序灌入向量(src/storage/ordered_children.c);
  • 实体从表中移动(例如因组件变更导致换表)时会触发flecs_ordered_children_reparent,把该实体从旧父的有序向量中移除并追加到新父的向量中(src/storage/ordered_children.c);
  • ecs_set_child_order则直接对ordered_children向量做整段内存拷贝覆盖(src/storage/ordered_children.c),并且会校验传入的 children 数组必须与现有子实体完全一致(个数与成员都要匹配),否则抛出ECS_INVALID_PARAMETER。

一个值得注意的限制

从 src/storage/ordered_children.c 的断言可以看出:不能从“已有子实体使用 Parent 组件”的父实体上移除OrderedChildrentrait(会触发ECS_UNSUPPORTED)。这是因为Parent层级存储本身是建立在OrderedChildren之上的(详见下文),二者是绑定关系。

两种层级存储:ChildOf 与 Parent

Flecs 实现了两种针对不同场景优化的层级存储:ChildOf层级和Parent层级。它们的行为大体一致,但内存布局与性能特征截然不同。

总体对比与选型

  • ChildOf层级为大型、非结构化的层级优化。典型例子是场景(Scene):一个场景根节点可能拥有成百上千个动态创建、动态删除的子实体,运行期任何时刻存在哪些实体往往是未知的,因此属于“非结构化”层级。
  • Parent层级为小型、结构化的层级优化。典型例子是 Prefab:一个 Prefab 通常只有几十个子实体,在 Prefab 被实例化时创建、在实例被删除时销毁,任何时刻存在哪些实体通常是已知的。参见 Prefabs 手册。

选择ChildOf层级的条件:

  • 一个父实体拥有很多子实体;
  • 子实体的数量或种类事先未知。

选择Parent层级的条件:

  • 一个父实体只有少量(几十个)子实体;
  • 子实体的数量和种类事先已知;
  • 层级嵌套很深。

创建方式上,ChildOf层级通过在实体上添加(ChildOf, parent)关系对实现;Parent层级则通过给实体设置一个Parent组件(值为父实体)实现:

// C ecs_entity_t spaceship = ecs_new(world); // 使用 ChildOf 存储创建子实体 ecs_entity_t cockpit = ecs_new_w_pair(world, EcsChildOf, spaceship); // 使用 Parent 存储创建子实体 ecs_entity_t engine = ecs_new_w_parent(world, spaceship, NULL /* 可选名字 */);
// C++ flecs::entity spaceship = world.entity(); // 使用 ChildOf 存储创建子实体 flecs::entity cockpit = world.entity(spaceship, nullptr /* 可选名字 */); // 使用 Parent 存储创建子实体 flecs::entity engine = world.entity(flecs::Parent{spaceship}, nullptr /* 可选名字 */);

其中ecs_new_w_parent的 C API 声明在 include/flecs.h,文档明确说明:如果指定了名字且该子实体已存在,则直接返回已有实体。

混用规则

  • 一个父实体可以同时拥有两种存储的子实体:ChildOf子实体与Parent子实体可以挂在同一个父实体之下;
  • 单个实体不能同时拥有ChildOf关系对和Parent组件:
// C ecs_entity_t spaceship = ecs_new(world); // OK:同一个父实体下可以混用两种存储 ecs_entity_t cockpit = ecs_new_w_pair(world, EcsChildOf, spaceship); ecs_entity_t engine = ecs_new_w_parent(world, spaceship, NULL); // 不 OK:单个实体不能同时拥有两种存储 ecs_entity_t engineering = ecs_new_w_parent(world, spaceship, NULL); ecs_add_pair(world, engineering, EcsChildOf, spaceship);
// C++ flecs::entity spaceship = world.entity(); // OK:同一个父实体下可以混用两种存储 flecs::entity cockpit = world.entity(spaceship, nullptr); flecs::entity engine = world.entity(flecs::Parent{spaceship}, nullptr); // 不 OK:给已有 Parent 组件的实体再加 ChildOf 会移除 Parent 组件 flecs::entity engineering = world.entity(flecs::Parent{spaceship}, nullptr); engineering.child_of(spaceship); // 会移除 Parent 组件

C++ 的行为(child_of会覆盖并移除Parent组件)在源码中也有对应实现依据:flecs_on_replace_parent是EcsParent组件的on_replace钩子(src/storage/non_fragmenting_childof.c),当实体的Parent值被替换(例如从 Parent 存储切换到 ChildOf 存储)时,它会执行从旧父的有序子实体向量中移除、向新父记录中追加、更新名字索引、更新ParentDepth等一系列内部操作。

两种存储的共同行为

无论使用哪种存储,以下行为是一致的:

  • 父实体被删除时,子实体被递归删除;
  • 带ChildOf项的查询对两种存储都有效;
  • 关系遍历(Position(up)或Position(cascade))对两种存储都有效;
  • 名字查找(parent.lookup("child_name"))对两种存储都有效;
  • ecs_get_parent/e.parent()可以获取两种存储下的父实体;
  • ecs_children/e.children()可以迭代两种存储下的子实体;
  • Prefab 实例化(prefab hierarchies 一节)对两种存储都有效;
  • JSON 序列化(json serialization 一节)对两种存储都有效。

两种存储的差异

  • 使用Parent层级时,父实体不会出现在子实体的类型中(ecs_get_type()/e.type()看不到(ChildOf, parent)对);
  • Parent层级构建在OrderedChildren特性之上,因此子实体以明确定义的顺序存储;
  • 实例化带Parent层级的 Prefab 时,实例子实体会继承Prefab 子实体的组件;而实例化带ChildOf层级的 Prefab 时,Prefab 子实体的组件会被复制到实例子实体上;
  • 实例化带Parent层级的 Prefab 时,Prefab 子实体的名字不会被复制到实例子实体上;
  • Or和Not查询运算符(operator overview 一节)暂不支持Parent层级。

ChildOf 存储的实现细节

ChildOf层级本质上是普通“会碎片化”的 Flecs 关系:每个唯一的(ChildOf, parent)对在概念上类似于一个组件。因此ChildOf层级的性能是可预期的——几乎所有的组件操作都具有 O(1) 时间复杂度。同时,查询某个父实体子实体的组件也非常快,因为组件可以像普通数组一样连续访问:

ecs_query_t *q = ecs_query(world, { .expr = "(ChildOf, parent), Position" }); ecs_iter_t it = ecs_query_iter(world, q); while (ecs_query_next(&it)) { Position *p = ecs_field(&it, Position, 0); for (int i = 0; i < it.count; i ++) { p[i].x ++; // Position 可以作为数组高效访问 } }

这些优势在“一个父实体有很多子实体”或“应用只需要访问特定父实体的子实体”时最为明显。例如,应用可能在内存中同时加载多个场景,但只渲染和更新其中一个场景。使用ChildOf层级时,不同父实体的子实体被存储在相互独立的表中,因此过滤掉其他场景的实体几乎是零成本的。

然而,当应用同时拥有大量父实体,并且需要同时访问不同父实体的子实体时,ChildOf层级就会变得昂贵,代价来自以下几个因素:

  • 不同父实体的子实体存放在不同的表(Archetype)中,碎片化了内存访问。迭代 1 张表 vs. 1000 张表的性能差距可以超过一个数量级;
  • 每张表都会消耗内存,内存量随表中组件数量线性增长;
  • 带缓存的查询会缓存匹配的表;表越多,查询的内存占用也越大;
  • 动态层级会引起表的创建与销毁,其开销相对于实体的创建与销毁而言是昂贵的;此外表可能需要在查询中被匹配/取消匹配,进一步抬高了动态层级的成本。

这些缺点在“父实体很多、每个父实体只有少量子实体”时最明显。极端情况下,如果父实体的每个子实体组件集合都不同,每个表中可能只有一个子实体。

决定ChildOf层级是否合适时,可以考虑以下问题:

  • 应用是否只访问特定父实体的子实体?
  • 父实体的数量是否合理(不超过数千)?
  • 内存开销是否可接受(可以使用统计 API 或 explorer 仪表盘查看详细的内存统计)?

如果以上有一个或多个答案为“否”,建议考虑使用Parent层级。

Parent 存储的实现细节

Parent层级通过设置值为父实体的Parent组件启用。与ChildOf层级不同,使用Parent层级时,多个父实体的子实体可以存放在同一张表中。Parent层级中的子实体存储在为(ChildOf, parent)组件索引记录准备的vector<ecs_entity_t>中——这正是OrderedChildren存储所使用的同一个向量(对应源码中的pair->ordered_children,见 src/storage/ordered_children.c)。

当Parent组件被设置时,一个(ParentDepth, depth)关系对会被自动添加到子实体上。查看子实体的类型时,会看到类似:

Parent, Position, Velocity, (ParentDepth, @3)

这表示该子实体存储在层级中 3 层深的位置。层级深度被查询用于高效实现Parent层级下的cascade/group_by特性。@表示这是一个值对(value pair),而3指定的是实体 id。用户自定义查询同样可以利用ParentDepth,例如下面的查询按广度优先顺序迭代实体:

// C ecs_query_t *q = ecs_query(world, { .terms = { { ecs_id(Position) } }, .group_by = EcsParentDepth });
// C++ auto q = world.query_builder<Position>() .group_by(flecs::ParentDepth) .build();

EcsParentDepth是 Flecs 内建的实体(声明于 include/flecs.h)。从源码看,ParentDepth的维护在flecs_on_replace_parent中完成:当子实体的父实体发生变化、且新旧父实体的深度不同时,会更新(ParentDepth, depth)值对,并递归更新其自身子实体的缓存深度(src/storage/non_fragmenting_childof.c)。同时,父实体的删除/重挂也会触发深度缓存更新(flecs_component_update_childof_w_depth)。

相比 ChildOf 的优势

Parent层级最大的好处是:它不会显著改变 ECS 存储中组件的内存布局。一条不涉及层级的查询,无论实体是否被组织进层级,执行性能都相同。这一点尤其利好包含大量小型(Prefab)层级的应用——切换到Parent层级可以将性能提升、内存占用降低一个数量级以上。

除了不碎片化存储之外,Parent层级在与 Prefab 组合使用时还有额外的性能收益:

  • Prefab 层级实例化不会导致表创建(使用Parent层级时);
  • 因为没有新表创建,也就不存在表与查询的匹配开销;应用的查询越多,性能提升越明显;
  • 使用Parent层级的 Prefab 会构建一个 “TreeSpawner”,它缓存 Prefab 层级的结构与表;这对ChildOf层级是不可能的,因为各实例间的表并不相同;
  • 实例子实体可以从 Prefab 子实体继承组件,从而降低内存占用和复制开销。

TreeSpawner 的存在同样有源码佐证:flecs_tree_spawner_assert_not_instantiated在Parent层级被修改时会被调用(src/storage/non_fragmenting_childof.c),用于阻止已实例化的 Prefab 层级被动态修改。

相比 ChildOf 的劣势

大多数情况下Parent层级会优于ChildOf层级,但存在几个例外:

  • Parent层级中的子实体存储在一个向量中,因此删除一个子实体是 O(n) 操作。如果父实体拥有大量(成千上万个)子实体,这可能非常昂贵;此时应考虑改用ChildOf层级;
  • 多词项查询中如果包含ChildOf词项,性能可能更差。这主要是因为多个父实体的子实体存放在同一张表中,查询需要做更多工作来过滤匹配的实体。

注意,删除子实体的 O(n) 代价不适用于删除父实体的场景:删除父实体时子实体是批量删除的,无需扫描子实体向量(对应源码中的批量清理路径)。

查询性能对比:不同查询形态的取舍

新层级存储在特定查询下的表现高度依赖查询的形态。以下逐类说明各类查询相对ChildOf层级的性能表现。

Position, Velocity(不涉及层级的查询)

这类查询几乎总是用Parent层级表现更好。ChildOf层级会产生更多表,从而降低查询性能;使用Parent层级时,组件更可能被紧密地打包在内存中。

(ChildOf, my_parent)(按特定父实体过滤)

这类查询对Parent层级通常表现相同或更好。原因在于:查询直接返回父实体的子实体向量,因此永远不需要迭代多张表——而多表迭代正是ChildOf层级下可能发生的情况。

(ChildOf, my_parent), Position

这类查询在Parent层级下表现更差。因为它无法在表级别求值,查询必须逐个检查my_parent的每个子实体是否拥有Position。

(ChildOf, *), Position

这类查询在Parent层级下表现更差。查询首先找到所有带有ChildOf关系对或Parent组件的表,然后检查结果表是否拥有Position。对于带Parent组件的表,查询必须逐个返回每个实体,因为它需要返回通配符实际匹配到的关系对。这是为了支持使用ecs_field_id、it.id()或it.pair()的代码:

auto q = world.query_builder<Position>() .with(flecs::ChildOf, flecs::Wildcard) .each([](flecs::iter& it, size_t, Position&) { flecs::entity parent = it.pair().second() // value is set by query });

两种存储下,将词项顺序反转都会带来更好的性能:第一个词项匹配的表通常远多于第二个词项,这会增加非缓存查询的成本,以及填充查询缓存的成本。

Position, (ChildOf, my_parent)

这类查询在Parent层级下表现更差。查询先找到所有带Position的表,然后对表中每个实体逐一检查它是否为my_parent的子实体。如果表包含大量实体,这可能是一个非常昂贵的操作。即使表中恰好只有my_parent的一个子实体,查询会走一条无需逐实体扫描的快速路径,但整体仍比ChildOf层级更差。

Position, (ChildOf, *)

这类查询在Parent层级下表现更差。查询先找到所有带Position的表,再检查表是否有Parent组件。如果有,查询就知道表中的每个实体都匹配;但由于必须把匹配到的父实体传达给应用,它仍必须逐实体返回。

Position, (ChildOf, _)

这类查询在Parent层级下表现相同。查询先找到所有带Position的表,再检查表是否有Parent组件。由于任意(_)通配符不需要向应用传达父实体,整张表可以被一次性返回。

如果应用还需要找出实体的父实体,可以在查询中追加flecs::Parent组件:

auto q = world.query_builder<Position, const flecs::Parent>() .with(flecs::ChildOf, flecs::Wildcard) .each([](flecs::iter& it, size_t, Position&, const flecs::Parent& p) { flecs::entity parent = it.world().entity(p.value); });

Position, Position(up)(向上关系遍历)

这类查询在Parent层级下表现更差。查询先找到所有带Position的表;如果发现表带Parent组件(而非ChildOf关系对),查询必须迭代每个实体并向上遍历层级。该结果无法缓存,进一步损害性能。

之所以为Parent层级支持 up 遍历,主要是为了提供从ChildOf层级迁移的更平滑路径。性能关键的查询通常不应在Parent层级上使用 up 遍历,应参见下文改用替代查询。

Position(up), Position

这类查询在Parent层级下表现更差。虽然不如上一类严重,但同样因为使用Parent层级的查询结果无法缓存而劣于ChildOf层级。

优化关系遍历:拆分查询

如上一节所述,使用关系遍历的查询在Parent层级下可能更慢。解决方案是:把一条使用关系遍历的查询拆分成两条——一条针对ChildOf层级,一条针对Parent层级。

以查询Position, Position(up)为例,可以拆分为:

// 针对 ChildOf 层级:使用关系遍历 Position, Position(up), !Parent // 针对 Parent 层级:不使用关系遍历 Position, Parent

然后在迭代逻辑中手动从父实体获取Position组件:

// C ecs_query_t *q = ecs_query(world, { .terms = { { ecs_id(Position) }, { ecs_id(EcsParent) } }, .cache_kind = EcsQueryCacheAuto }); ecs_iter_t it = ecs_query_iter(world, q); while (ecs_query_next(&it)) { Position *p = ecs_field(&it, Position, 0); EcsParent *parents = ecs_field(&it, EcsParent, 1); for (int i = 0; i < it.count; i ++) { const Position *p_parent = ecs_get(world, parents[i].value, Position); // 逻辑照常 } }
// C++ auto q = world.query<Position, const flecs::Parent>(); q.each([](Position& p, const flecs::Parent& parent) { const Position& p_parent = parent.get<Position>(); // 逻辑照常 });

对于使用cascade的查询,应用可以使用group_by(flecs::ParentDepth)替代,按层级深度顺序迭代实体。下面的代码展示了如何对每种存储各写一条查询、实现广度优先遍历。注意实现中按深度逐层迭代,这可以防止较深的ChildOf层级在较浅的Parent实体之前被处理:

// C ecs_query_t *q_childof = ecs_query(world, { .terms = { { ecs_id(Position) }, { ecs_id(Position), .src.id = EcsCascade }, { ecs_id(EcsParent), .oper = EcsNot } }, .cache_kind = EcsQueryCacheAuto }); ecs_query_t *q_parent = ecs_query(world, { .terms = { { ecs_id(Position) }, { ecs_id(EcsParent) } }, .group_by = EcsParentDepth, .cache_kind = EcsQueryCacheAuto }); // 将 16 替换为应用中的最大层级深度 for (int depth = 0; depth < 16; depth ++) { // 迭代当前深度的 ChildOf 层级 { ecs_iter_t it = ecs_query_iter(world, q_childof); ecs_iter_set_group(&it, depth); while (ecs_query_next(&it)) { Position *p = ecs_field(&it, Position, 0); Position *p_parent = ecs_field(&it, Position, 1); for (int i = 0; i < it.count; i ++) { p[i].x += p_parent->x; // ... } } } // 迭代当前深度的 Parent 层级 { ecs_iter_t it = ecs_query_iter(world, q_parent); ecs_iter_set_group(&it, depth); while (ecs_query_next(&it)) { Position *p = ecs_field(&it, Position, 0); EcsParent *parents = ecs_field(&it, EcsParent, 1); for (int i = 0; i < it.count; i ++) { const Position *p_parent = ecs_get( world, parents[i].value, Position); p[i].x += p_parent->x; // ... } } } }
// C++ auto q_childof = world.query_builder<Position, const Position>() .term_at(1).cascade() .without<flecs::Parent>() .build(); auto q_parent = world.query_builder<Position, const flecs::Parent>() .group_by(flecs::ParentDepth) .build(); // 将 16 替换为应用中的最大层级深度 for (int depth = 0; depth < 16; depth ++) { q_childof.set_group(depth) .each([](Position& p, const Position& p_parent) { p.x += p_parent.x; }); q_parent.set_group(depth) .each([](Position& p, const flecs::Parent& parent) { const Position& p_parent = parent.get<Position>(); p.x += p_parent.x; }); }

注意上述查询均显式指定了.cache_kind = EcsQueryCacheAuto(C++ 侧由查询构建器默认处理),这确保了拆分后两条查询各自都能享受查询缓存,从而把Parent层级“不可缓存 up 遍历”的短板消解在拆分结构之中。

总结与进一步阅读

Flecs 的层级系统是一个建立在关系之上的通用、高性能机制:(ChildOf, parent)关系对负责常见的场景树与 UI 树,Parent组件 +OrderedChildren向量存储则服务于需要固定顺序、深层嵌套、Prefab 密集复用的结构化层级。选型的关键在于评估“父实体数量与子实体规模”以及“查询形态是否与层级深度交互”:

  • 父实体少、子实体多、只需按父访问 → 优先ChildOf(O(1) 组件操作 + 表级过滤);
  • 父实体多、子实体少、Prefab 实例化密集、层级深 → 优先Parent(消除表碎片化 + TreeSpawner 缓存 + 组件继承);
  • 需要稳定子实体顺序 → 加OrderedChildrentrait,必要时用ecs_set_child_order自定义顺序;
  • 需要广度优先迭代 →cascade(ChildOf)或group_by(flecs::ParentDepth)(Parent),或按上述方式拆分查询。

可以继续深入阅读的仓库资源:

  • 文档:Prefabs 手册(Prefab 层级)、Queries 文档(关系遍历与运算符)、Entities 与 Components 手册(命名规则)、Flecs Remote API(JSON 序列化);
  • 示例:examples/c/entities/hierarchy/src/main.c、examples/cpp/entities/hierarchy/src/main.cpp(深度优先遍历)、examples/c/queries/hierarchies/src/main.c、examples/cpp/queries/hierarchy/src/main.cpp(cascade 广度优先遍历);
  • 源码:src/storage/ordered_children.c(有序子实体向量与重排序)、src/storage/non_fragmenting_childof.c(Parent 组件、ParentDepth 深度维护与 prefab 子实体索引);
  • 公共 API:include/flecs.h(ecs_get_parent、ecs_children、ecs_set_child_order、ecs_new_w_parent、EcsParentDepth等声明)。
  • 游戏开发

【免费下载链接】flecs

A fast entity component system (ECS) for C & C++

项目地址:https://gitcode.com/gh_mirrors/fl/flecs
点击查看免费下载

相关推荐

上一篇:QuickRecorder:不到 10MB 的 macOS 录屏工具
下一篇:LayoutXLM KIE Algorithm in PaddleOCR: Multimodal SER & RE Training, Evaluation and Python Inference

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询