Skip to content

沿树行走:层级遍历

Listing 9-2 里我们手写了递归来打印树。一两层的树这么干没问题,但“点名整支商队”“从一件装备追查它在哪辆车上”这类需求太常见,Bevy 直接把走法做成了 Query 的方法。这次把树搭满三层——大旗、车、人、随身物:

rust
fn setup(mut commands: Commands) {
    commands.spawn((
        Name::new("商队大旗"),
        Caravan,
        children![
            (
                Name::new("青篷车"),
                children![
                    Name::new("老姜"),
                    (Name::new("小芙"), children![Name::new("长戟")]),
                ],
            ),
            (
                Name::new("铁皮货车"),
                children![(Name::new("罗兰"), children![Name::new("铜灯")])],
            ),
        ],
    ));
}

Listing 9-7(节选):三层嵌套的 children!(examples/listing-09-07.rs)

向下:iter_descendants

向下遍历靠 Query<&Children>——毕竟要沿着每个节点的名单往下走:

rust
/// 全队点名:同一棵树,两种走法
fn roll_call(
    caravan: Single<Entity, With<Caravan>>,
    children_query: Query<&Children>,
    names: Query<&Name>,
) {
    let breadth: Vec<&str> = children_query
        .iter_descendants(*caravan)
        .map(|entity| names.get(entity).unwrap().as_str())
        .collect();
    println!("广度优先:{}", breadth.join("、"));

    let depth: Vec<&str> = children_query
        .iter_descendants_depth_first(*caravan)
        .map(|entity| names.get(entity).unwrap().as_str())
        .collect();
    println!("深度优先:{}", depth.join("、"));
}

Listing 9-7(续):广度优先与深度优先两种点名

console
cargo run -p ch09-relationships --example listing-09-07
text
广度优先:青篷车、铁皮货车、老姜、小芙、罗兰、长戟、铜灯
深度优先:青篷车、老姜、小芙、长戟、铁皮货车、罗兰、铜灯

两个迭代器都吐出全部七个后代(不含起点自己),区别只在次序:

  • iter_descendants:广度优先——先点完两辆车,再点车里的人,最后点随身物。一层层扫。
  • iter_descendants_depth_first:深度优先——青篷车连人带物清完,才轮到铁皮货车。一支支捋。

多数时候你不在乎次序,随手用前者就行;要按“子树成块”处理时选后者。注意它们给的是平铺的实体流,不带“现在第几层”的信息——想要带缩进的树形输出,还是得像 Listing 9-2 那样自己递归。

向上:iter_ancestors 与 root_ancestor

向上遍历换 Query<&ChildOf>——沿着每个实体的父亲指针爬:

rust
/// 失物招领:从长戟一路向上找到树根
fn trace_up(
    everything: Query<(Entity, &Name)>,
    parents: Query<&ChildOf>,
    names: Query<&Name>,
) {
    let (spear, _) = everything
        .iter()
        .find(|(_, name)| name.as_str() == "长戟")
        .unwrap();

    print!("长戟在谁手里:长戟");
    for ancestor in parents.iter_ancestors(spear) {
        print!(" ← {}", names.get(ancestor).unwrap());
    }
    println!();

    let root = parents.root_ancestor(spear);
    println!("它属于哪支商队:{}", names.get(root).unwrap());
}

Listing 9-7(续):失物招领——从长戟一路上溯

text
长戟在谁手里:长戟 ← 小芙 ← 青篷车 ← 商队大旗
它属于哪支商队:商队大旗

iter_ancestors 逐级吐出父亲、祖父……直到树根;root_ancestor 是它的快捷收尾,直接给出树根(如果实体本来就没有父亲,树根就是它自己)。

一条警告与一张清单

上一节末尾的伏笔在此兑现:这些遍历方法不检测环。引擎只拦“自己当自己父亲”,拦不住 A、B 互为祖先的绕圈——真出了环,iter_descendants 会在圈里永远转下去,程序原地卡死。环几乎总是 reparent 惹的祸:把某个实体挂到它自己的后代名下,树就拧成了圈。写关卡编辑器这类要任意拖拽层级的工具时,挂接前先用 iter_ancestors 查一遍“新父亲是不是我的后代”。

最后把没上场的几个遍历方法列齐——都是 Query 的方法,用哪种组件做查询数据,决定了你能往哪个方向走:

方法查询数据给你什么
iter_descendants(e)&Children全部后代,广度优先
iter_descendants_depth_first(e)&Children全部后代,深度优先
iter_ancestors(e)&ChildOf全部祖先,由近及远
root_ancestor(e)&ChildOf树根
iter_leaves(e)&Children子树里的全部叶子(没有孩子的末端实体)
iter_siblings(e)(Option<&ChildOf>, Option<&Children>)同一个父亲名下的兄弟,不含自己

这章到现在,父子树的“增删走”三件事齐了。但你可能已经隐约觉得不对劲:长戟挂在小芙名下,用的也是 ChildOf——“拿在手里”和“坐在车上”真的是同一种关系吗?下一节就处理这个别扭。