SuperAPI AI 工具集

bevy-game-loop-time-trial

编程与开发

查看项目

更新于 10-16 06:08

技能介绍

用途

本技能用于在 Bevy 0.20 中实现一个计时赛(time trial)循环:当一次跑动需要记录已用时间、分段(splits)或终点线时加载它。它把比赛数据组织为 ECS 数据,而不是把计时逻辑塞进移动系统。

主要能力

  • 比赛时钟:已用时间(elapsed)保存在一个 Resource 上。
  • 分段记录:当角色经过 SplitGate 时,把当前 elapsed 写入该 Resource 上的 splits 列表。
  • 终点判定:角色越过 FinishLine 时设置 finished 标志,并在成绩优于 best 时更新 best。
  • 固定步长移动:角色由 FixedUpdate 中的系统按固定步长推进,该系统不拥有比赛时钟。

ECS 结构

组件(Component):

  • Racer:角色标记。
  • FinishLine:终点体积标记。
  • SplitGate:分段门,含 at_x 与 taken 字段。

资源(Resource):

  • RaceClock:包含 elapsed、splits、finished、best,比赛时钟、分段列表和最佳成绩都放在这一个资源上。

系统(Systems):

  • tick_race:推进时钟。
  • record_split:记录分段。
  • cross_finish:检测终点线。 三者按 tick_race → record_split → cross_finish 的顺序用 .chain() 串联,因为它们都写入同一个资源。

消息(Messages):一次性事实(命中、拾取、任务推进)走 bevy-events-messaging;本文件不新增事件类型名。

使用方式

依赖声明:

[package]
name = "time-trial"
version = "0.1.0"
edition = "2021"

[dependencies]
bevy = "0.20"

核心代码结构如下(保留原文代码):

#[derive(Resource)]
struct RaceClock {
    elapsed: f32,
    splits: Vec<f32>,
    finished: bool,
    best: f32,
}

#[derive(Component)]
struct Racer;

#[derive(Component)]
struct FinishLine;

fn spawn_course(mut commands: Commands) {
    commands.insert_resource(RaceClock {
        elapsed: 0.0,
        splits: Vec::new(),
        finished: false,
        best: f32::MAX,
    });
    commands.spawn((Racer, Transform::from_xyz(0.0, 0.0, 0.0)));
    commands.spawn((FinishLine, Transform::from_xyz(10.0, 0.0, 0.0)));
    commands.spawn(SplitGate {
        at_x: 5.0,
        taken: false,
    });
}

fn tick_race(time: Res<Time>, mut clock: ResMut<RaceClock>) {
    if !clock.finished {
        clock.elapsed += time.delta_secs();
    }
}

#[derive(Component)]
struct SplitGate {
    at_x: f32,
    taken: bool,
}

fn record_split(
    mut clock: ResMut<RaceClock>,
    racers: Query<&Transform, With<Racer>>,
    mut gates: Query<&mut SplitGate>,
) {
    if clock.finished {
        return;
    }
    let Some(racer) = racers.iter().next() else {
        return;
    };
    for mut gate in &mut gates {
        if !gate.taken && racer.translation.x >= gate.at_x {
            gate.taken = true;
            let elapsed = clock.elapsed;
            clock.splits.push(elapsed);
        }
    }
}

fn cross_finish(
    mut clock: ResMut<RaceClock>,
    racers: Query<&Transform, With<Racer>>,
    lines: Query<&Transform, With<FinishLine>>,
) {
    if clock.finished {
        return;
    }
    let Some(racer) = racers.iter().next() else {
        return;
    };
    let Some(line) = lines.iter().next() else {
        return;
    };
    if racer.translation.x >= line.translation.x {
        clock.finished = true;
        if clock.elapsed < clock.best {
            clock.best = clock.elapsed;
        }
    }
}

pub struct TimeTrialPlugin;

impl Plugin for TimeTrialPlugin {
    fn build(&self, app: &mut App) {
        app.add_systems(Startup, spawn_course).add_systems(
            Update,
            (tick_race, record_split, cross_finish).chain(),
        );
    }
}

固定步长移动角色(不拥有比赛时钟):

#[derive(Component)]
struct Racer;

fn drive(time: Res<Time<Fixed>>, mut racers: Query<&mut Transform, With<Racer>>) {
    let step = time.delta_secs() * 8.0;
    for mut transform in &mut racers {
        transform.translation.x += step;
    }
}

pub struct DrivePlugin;

impl Plugin for DrivePlugin {
    fn build(&self, app: &mut App) {
        app.add_systems(FixedUpdate, drive);
    }
}

状态归属

  • 单个实体上的数据:Component,由一个系统通过 Query 修改,运行在 Update 或 FixedMain。
  • 整个应用共享的一个值:Resource,由一个系统修改,Startup 插入,之后由读取它的调度使用。
  • 生成:Commands,由生成系统负责;初始世界在 Startup,运行时生成在后续调度。
  • 不能竞态的顺序:.chain(),仅约束被链的系统,处于同一调度。
  • 插件边界:Plugin,build 注册系统与资源,在应用运行前完成。

借用规则:两个都可变查询同一组件的系统必须排序(.chain())或拆分,使只有一个写入;无关系统保持并行。

决策表

  • 需要逐实体数据:用 Component + Query,不要用一个结构体存所有实体的字段。
  • 需要应用级配置或时钟:用 Resource,不要把值复制到每个实体。
  • 需要稳定模拟步长:用 FixedMain 和 Fixed 时钟,不要把模拟绑到原始帧 Update。
  • 需要启动生成:在 Startup 中用 Commands,不要从渲染系统生成。
  • 需要非共享状态的跨系统通知:用 bevy-events-messaging 中的消息或观察者槽位,不要在本文件里新造事件类型。
  • 需要文档集之外的 API:阅读链接的 Bevy 页面,不要编造 crate 或 API。

适用场景

  • 竞速类玩法中的单圈计时、分段计时与终点判定。
  • 需要把比赛时钟、分段列表、最佳成绩作为共享状态统一管理的场景。
  • 需要固定步长驱动角色、同时让计时逻辑独立于移动系统的场景。

限制与边界

  • 比赛计时、分段和幽灵数据由使用者自行以组件形式构建;幽灵回放属于 bevy-save-load。
  • 本技能只命名有文档的 Bevy crate 与模块,不引入物理、导航、网络或 XR crate。
  • 分段或终点不使用信号总线:时钟资源是共享状态,一次性通知属于 bevy-events-messaging。
  • Bevy 代码保持惯用 ECS,只使用有文档的 Bevy API,不使用节点类型。
  • 文档 URL 不要固定 /0.20.0/,链接保持最新;0.20 目标只在正文中说明一次。
  • 不要从两个无序系统可变查询同一组件。

相关技能

前置:bevy-master(枢纽与调度)、bevy-ecs-composition(组件与系统)、bevy-state-machines(倒计时与完成)、bevy-data-resources(表格与定义)、bevy-events-messaging(一次性事实)。 互补:bevy-state-machines(模式标志)。 下游/消费者:暂无。 主技能:bevy-master(Bevy 路由)。

参考

官方文档:Assets(官方资源目录,未选择第三方 crate)、Examples(最新分支示例树)、ECS(Component、systems、Query、Startup、Update 与 .chain())、Plugins(Plugin、DefaultPlugins、MinimalPlugins 与 add_plugins)。bevy-master 上的库固定版本保持 0.20.0,官方链接保持无版本号的 learn 页面。