Art by takepon
940 字
5 分钟
Bevy Engine 中的 ECS(延迟更改、查询操作)
我这里使用的 Bevy Engine 版本是0.17.3,于2025年11月18日发布:
[dependencies]bevy = { version = "0.17.3", features = ["dynamic_linking"] }关于 ECS (Entity Component System)
这是我的博客中提到 ECS 的第一篇文章。我想我需要先对 ECS 进行一个快速概述。
关于 ECS 的概述可以在这里找到:https://github.com/SanderMertens/ecs-faq
我这里说一下我的理解:
- 实体(或者对象)由组件组合而成。比如Minecraft中的僵尸:
Zombie = Entity + Mob + Hostile + Alive + Health + ...(实体 + 怪物 + 敌对 + 活着的东西 + 生命值)。 - 组件负责存储数据。在僵尸的例子中,Health组件可以存储僵尸的血量:
struct Health { hp: f32 } - 系统用于赋予世界上的东西逻辑。比如我们可以使用一个系统让僵尸移动。
需要注意的是:
- 组件只存储数据,不包含逻辑。
- 系统不能存储(游戏的)数据。
Bevy 中的系统
Bevy 中的系统只是一些符合某些样式的 Rust 函数。比如让我们的僵尸在阳光下着火:
我这里假设僵尸拥有以下组件:Zombie, Mob。
fn zombie_set_on_fire(mut cmd: Command, query: Query<(&Zombie, &Mob)>) { for (zombie, mob) in &query { // 做些什么 }}系统的参数列表里面可以放很多东西。这篇文章我们只关心Command和Query。
查询(Query)
Query表明一个「查询结果」。
它的类型列表可以放:
- 借用类型元组。比如刚刚的
(Zombie, Mob)。此时它表示:我想要查询所有同时拥有Zombie和Mob组件的实体。 - 主组件,带有次要组件的「关键字」的列表。比如
Query<&Mob, With<Zombie>>表示查询所有同时拥有Zombie和Mob组件的实体。
常见「关键字」:
With<C>: 并且拥有C。Without<C>: 没有C。Or<A, B>: 拥有A或者B。
二者有什么区别呢?
类型元组允许你获取两者的值:
fn some_system(mut cmd: Command, query: Query<(&Position, &Velocity)>) { for (position, velocity) in &query { // 此时position的类型是Position,velocity的类型是Velocity // 做点什么 }}后者不允许你获取值,它只保证满足「关键字」条件。
fn some_system(mut cmd: Command, query: Query<&Position, With<Velocity>>) { for position in &query { // 此时position的类型是Position,你没法获取Velocity组件的值。 // 做点什么 }}此外,前者可能阻止一些系统并行运行。而后者不会。
总之:
- 关心值:使用类型元组。
- 只关心「有没有」:使用
With<C>。
立即更新值
给Query<...>和在查询要改变的东西标上mut即可。比如按照速度更新位置:
#[derive(Component)]struct Position(f32, f32);
#[derive(Component)]struct Velocity(f32, f32);
fn update_position(mut query: Query<(&mut Position, &Velocity)>) { for (mut position, velocity) in &mut query { position.0 += velocity.0; position.1 += velocity.1; }}延迟更新
使用立即更新值可能会造成一些混乱,因为系统的执行顺序可能并不是那么明显。或者你可能不太关心系统的执行顺序。
使用延迟更新可以把组件更新推迟到帧末。在帧末,Bevy 会用缓冲区的值覆盖世界里的数据。
延迟更新的实现方法是使用 Commands :
#[derive(Component)]struct Data(i32);
fn setup(mut cmd: Commands) { cmd.spawn(Data(3));}
fn change(mut cmd: Commands, query: Query<(Entity, &Data)>) { for (e, d) in &query { let original_val = d.0; println!("d changed from {} to {}", original_val, original_val + 1); cmd.entity(e).insert(Data(original_val + 1)); }}
fn read(_cmd: Commands, query: Query<&Data>) { for d in query { println!("Read value: {}", d.0); }}
fn main() { let mut app = App::new(); app.add_systems(Startup, setup) .add_systems(Update, read) .add_systems(Update, change); app.update(); println!("==============="); app.update();}输出:
d changed from 3 to 4Read value: 3===============d changed from 4 to 5Read value: 4一些解释:
- 这里
app.update();的作用是让Bevy运行一遍所有系统。因为我没有引入DefaultPlugins。使用app.run();会让所有系统只运行一遍,效果不是很明显。 cmd.spawn(...)方法是延迟生效的。
关于Commands结构的其他用法请参见:https://docs.rs/bevy/0.17.3/bevy/ecs/prelude/struct.Commands.html
部分信息可能已经过时