mcpField
B
bevy-ecs

bevy-ecs

github.com/gamedev-skills开发5.4k5.7k 次安装1 天前更新
代码仓库

关于 bevy-ecs

bevy-ecs 是面向 Rust 游戏开发者的 Bevy ECS 技能,讲解 App、插件、组件、资源、系统查询与调度。示例针对 Bevy 0.20,并强调版本匹配、帧率无关更新和查询冲突处理。

主要功能

  1. 01搭建 App 与插件
  2. 02定义组件和资源
  3. 03编写系统与实体查询
  4. 04配置系统调度和运行条件
  5. 05处理查询冲突与延迟命令

适用场景

  1. 01为 Bevy 游戏组织 ECS 逻辑
  2. 02实现按帧更新的实体移动
  3. 03安排有先后依赖的系统
  4. 04排查组件访问冲突或版本 API 错误

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/gamedev-skills/awesome-gamedev-agent-skills --skill bevy-ecs

安装后在 Claude Code 中输入/bevy-ecs确认已加载。

附带文件references/queries-and-scheduling.md

详细介绍

Bevy ECS

围绕实体组件系统组织 Rust 中的 Bevy 游戏:包括 App 和插件、组件与资源、带查询的系统、调度,以及与帧率无关的更新。新示例面向 Bevy 0.20。如果项目已固定到其他版本,请保持该版本,并使用对应的迁移指南。

适用场景

  • 用于接入 Bevy App、定义 Component/Resource 类型、编写查询实体的系统、为系统排序/筛选,或修复借用冲突导致的 panic 和依赖帧率的移动问题。
  • 用于 Cargo.toml 依赖 bevy,且代码调用 App::new()、add_systems、Query 或 Commands 的场景。

**不适用场景:**这是 ECS 核心内容。深入的渲染、自定义着色器/管线、UI 布局和音频属于其他领域。对于与引擎无关的 AI 或程序化算法,可搭配使用 game-ai / procedural-gen。

核心工作流

  1. **检测并固定版本。**先查看 Cargo.toml 和 Cargo.lock。新项目使用 bevy = "0.20";绝不要悄然将现有项目迁移到其他 Bevy 次要版本。以对应的文档和迁移指南为准。
  2. 构建 App。App::new().add_plugins(DefaultPlugins) 会提供窗口、输入、渲染、时间等功能。将系统注册到调度中:Startup(运行一次)和 Update(每帧运行)。
  3. **将数据建模为组件,将全局数据建模为资源。**使用 #[derive(Component)] 定义每个实体的数据;使用 #[derive(Resource)] 定义唯一数据(分数、设置、Time 时钟)。从 0.19 起,Resource 扩展自 Component,且每个资源都存储在自己的实体上,因此不要同时派生两者。
  4. **将系统编写为普通函数。**参数声明数据访问方式:使用 Query<...> 查询实体,使用 Res<T>/ResMut<T> 访问资源,使用 Commands 延迟生成/销毁实体。只要访问不存在冲突,系统就会并行运行。
  5. 使用 time.delta_secs() 驱动移动,确保移动速度与帧率无关。
  6. 只对必须排序的系统排序,使用 .chain() 或显式约束;使用 run_if 控制系统运行条件。将相关的初始化工作归入 Plugin。使用 cargo run 构建并查看 panic 信息——Bevy 会在启动时报告查询冲突。

模式

1. Cargo.toml + 最小化 App
# Cargo.toml — pin the version; the API differs across minor releases.
[dependencies]
bevy = "0.20"
// main.rs
use bevy::prelude::*;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)            // window, input, render, time, ...
        .add_systems(Startup, setup)            // runs once at startup
        .add_systems(Update, move_players)      // runs every frame
        .run();
}
2. 组件、资源与实体生成
#[derive(Component)]
struct Player;

#[derive(Component)]
struct Velocity(Vec2);

#[derive(Resource)]
struct Score(u32);

fn setup(mut commands: Commands) {
    commands.insert_resource(Score(0));

    // Camera2d is a component with required components (bundles removed in 0.16);
    // spawning it pulls in Transform, Camera, etc. automatically.
    commands.spawn(Camera2d);

    // Spawn an entity as a tuple of components.
    commands.spawn((
        Player,
        Velocity(Vec2::new(150.0, 0.0)),
        Transform::from_xyz(0.0, 0.0, 0.0),
    ));
}
3. 使用查询和 Time 资源的系统
// Iterate every entity that has BOTH Velocity and Transform; mutate Transform.
fn move_players(time: Res<Time>, mut query: Query<(&Velocity, &mut Transform)>) {
    for (velocity, mut transform) in &mut query {
        // delta_secs() is f32 seconds (renamed from delta_seconds() in 0.16).
        transform.translation += velocity.0.extend(0.0) * time.delta_secs();
    }
}
4. 查询筛选器(With / Without / Changed)
// Only entities tagged Player (the Player component itself isn't read).
fn aim_player(mut q: Query<&mut Transform, With<Player>>) { /* ... */ }

// Disjoint two mutable Transform queries so they don't conflict at runtime.
fn separate(
    mut players: Query<&mut Transform, With<Player>>,
    mut enemies: Query<&mut Transform, Without<Player>>,
) { /* ... */ }

// React only when Health changed since last run (change detection).
fn on_health_change(q: Query<&Health, Changed<Health>>) {
    for health in &q { /* update the HUD, etc. */ }
}
5. 资源:读取和写入
fn add_points(mut score: ResMut<Score>) {
    score.0 += 10;                 // ResMut = write access
}

fn show_score(score: Res<Score>) {
    info!("score: {}", score.0);   // Res = read access
}
6. 排序、运行条件和插件
fn main() {
    App::new()
        .add_plugins((DefaultPlugins, GameplayPlugin))
        // .chain() forces order: damage resolves before death is checked.
        .add_systems(Update, (apply_damage, check_deaths).chain())
        // run_if gates a system on a condition each frame.
        .add_systems(Update, spawn_wave.run_if(wave_timer_finished))
        .run();
}

struct GameplayPlugin;
impl Plugin for GameplayPlugin {
    fn build(&self, app: &mut App) {
        app.insert_resource(Score(0))
           .add_systems(Startup, setup)
           .add_systems(Update, (move_players, add_points));
    }
}

常见问题

  • 找不到 delta_seconds() → 在 0.16 中,它已重命名为 time.delta_secs()(以及 elapsed_secs())。使用旧名称会导致编译失败。
  • 移动速度随帧率变化 → 将每帧的变化量乘以 time.delta_secs()。切勿假设帧时间固定。
  • Panic error[B0001]:"... accesses component(s) Transform in a way that conflicts with a previous system parameter" → 同一系统中的两个 Query 都写入同一个组件,或者一个读取组件、另一个写入组件,且它们涉及的实体有重叠。使用 With/Without 让它们处理互不重叠的实体,或使用 ParamSet。
  • 找不到 Camera2dBundle/SpriteBundle → 这些 Bundle 在 0.15 中已弃用,并在 0.16 中移除。 直接生成组件(Camera2d、Sprite、Transform);必需组件会自动补齐其余部分。
  • “trait Component is not implemented” → 忘记添加 #[derive(Component)](如果是资源,则添加 #[derive(Resource)])。
  • 同一帧中生成的实体无法被后续查询看到 → Commands 会延迟执行,并在下一个同步点应用。请在后续系统中读取该实体,而不是在生成它的系统中读取。
  • 假设系统顺序已确定,但实际上并未强制执行 → 默认情况下,系统会并行运行。如果 B 必须在 A 之后运行,请添加 (A, B).chain() 或明确的顺序约束。 若要查找仅因偶然而生效的顺序依赖,可在测试中使用 ScheduleBuildSettings::shuffle_seed 打乱调度(0.20,需启用 debug 功能;参见参考资料)。
  • 同时派生 Resource 和 Component 后出现“conflicting implementations of trait Component” → 从 0.19 起,Resource 扩展了 Component;只派生 Resource 即可。
  • “Despawn everything”导致 Panic,或 Res<T> 旁边的 Query<EntityMut> 因 error[B0002] 导致 Panic → 从 0.19 起,每个资源都存在于带有 IsResource 标记的实体上,因此 Query<Entity>/EntityRef/EntityMut 也会匹配这些实体。请查询你自己的标记组件,或添加 Without<IsResource>(use bevy::ecs::resource::IsResource)。
  • On<Add, Player> 因“struct takes 1 generic argument but 2 generic arguments were supplied”而失败 → 0.20 将组件移入了生命周期事件中:应写为 On<Add<Player>>(Insert<T>、Discard<T>、Remove<T>、Despawn<T> 也一样)。
  • “切换”到当前状态时,OnExit/OnEnter 再次运行 → NextState::set 总会运行状态转换调度。使用 set_if_different 跳过状态未变化的转换(该方法在 0.20 中从 set_if_neq 重命名而来;在 ResMut<NextState<_>> 上调用 set_if_neq 会匹配到无关的变更检测方法,并因“can't compare NextState”而失败)。
  • Name::new(text.as_str()) 报“text does not live long enough” → 从 0.20 起,Name 接受 &'static str 字面量或拥有所有权的 String,而不接受借用的 &str;请传入 Name::new(text.clone()) 或 Name::new(format!("Enemy {id}"))。
  • 升级到 0.20 后,重叠的精灵绘制顺序发生变化 → 精灵现在通过 Mesh2d 后端渲染,而相同 z 值下的绘制顺序从未得到保证。请为可能重叠的精灵设置不同的 Transform z 值。
  • 复制粘贴较旧版本的 Bevy 代码片段 → API 会随次版本变化。缓冲事件系统已变为消息系统:EventReader/EventWriter/add_event 均已不存在,请改用 #[derive(Message)]、MessageReader/MessageWriter 和 app.add_message::<T>()。请对照你固定使用的版本对应的文档和迁移指南进行核实;不要混用版本。

参考资料

  • 关于调度和 SystemSet 顺序(包括 0.20 弱排序和调度打乱)、States/OnEnter/OnExit 和状态范围内的实体销毁、变更检测、Commands 生命周期与同步点、ParamSet 和资源实体冲突、iter_many 结果,以及 0.20 语法中的消息/观察者,请阅读 references/queries-and-scheduling.md。

相关技能

  • game-ai — 可移植的 FSM/行为树/转向概念,可在 ECS 中实现。
  • procedural-gen — 可由系统驱动的噪声/RNG/生成算法。
  • pygame-core / love2d-core — 更轻量的引擎,适用于较小型项目。

评论

支持 Markdown