系统调度(Schedule & App)
概述
学习目标:
- 理解 Schedule 的概念和作用
- 掌握如何控制系统执行顺序
- 了解自定义 Schedule 的创建方法
- 理解 App 的生命周期和运行条件
前置知识要求:
- 核心编程框架(ECS)
- 系统(Systems)
- 资源(Resources)
- Rust 基础语法
核心概念
什么是 Schedule?
Schedule 控制系统执行策略和每个 tick 内系统的广泛顺序。每个系统都属于一个 Schedule,Schedule 控制系统的执行顺序。
为什么使用 Schedule?
- 执行顺序:Schedule 控制系统的执行顺序
- 执行策略:Schedule 控制系统的执行策略(并行或顺序)
- 生命周期:Schedule 控制系统的生命周期(启动、更新、结束等)
Schedule 的设计思想
Schedule 采用数据导向的设计思想,将系统执行顺序和逻辑分离。这种设计使得:
- 系统可以灵活组织
- 系统执行顺序可以明确控制
- 系统可以并行执行,提高性能
基础用法
默认 Schedule
Bevy 提供了多个默认 Schedule,如 Startup、Update、Last 等。
源代码文件:bevy/examples/ecs/ecs_guide.rs
代码示例:
1 2 3 4 5 6 7 8 9 10 11 12
| fn main() { App::new() .add_systems(Startup, startup_system) .add_systems(Update, print_message_system) .add_systems(Last, print_at_end_round) .run(); }
|
关键要点:
Startup:启动系统,在应用启动时运行一次
Update:更新系统,每次更新运行一次
Last:最后系统,在每次运行的末尾运行
- 系统按 Schedule 顺序执行
说明:
Bevy 提供了多个默认 Schedule,用于控制系统的执行顺序。Startup 系统在应用启动时运行一次,用于初始化。Update 系统每次更新运行一次,用于游戏逻辑。Last 系统在每次运行的末尾运行,用于清理。
系统执行顺序
可以通过 .before() 和 .after() 方法控制系统的执行顺序。
源代码文件:bevy/examples/ecs/ecs_guide.rs
代码示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
.add_systems(Last, print_at_end_round)
|
关键要点:
- 默认情况下,系统并行运行
- 使用
.before() 和 .after() 方法控制顺序
- 系统不会调度,直到依赖的系统完成
- 可变访问会阻止并行执行
说明:
默认情况下,系统并行运行,除非它们需要对数据的可变访问。使用 .before() 和 .after() 方法可以明确控制系统的执行顺序。系统不会调度,直到它们依赖的所有系统都已完成。
系统集(SystemSet)
系统集用于组织相关系统,并控制它们的执行顺序。
源代码文件:bevy/examples/ecs/ecs_guide.rs
代码示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
|
#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)] enum MySystems { BeforeRound, Round, AfterRound, }
fn main() { App::new() .configure_sets( Update, ( MySystems::BeforeRound, MySystems::Round, MySystems::AfterRound, ) .chain(), ) .add_systems( Update, ( ( (new_round_system, new_player_system).chain(), exclusive_player_system, ) .in_set(MySystems::BeforeRound), score_system.in_set(MySystems::Round), ( score_check_system, game_over_system.after(score_check_system), ) .in_set(MySystems::AfterRound), ), ) .run(); }
|
关键要点:
- 使用
SystemSet 组织相关系统
- 使用
configure_sets() 配置系统集的顺序
- 使用
.chain() 方法链式系统集
- 使用
.in_set() 方法将系统添加到系统集
说明:
系统集用于组织相关系统,并控制它们的执行顺序。使用 configure_sets() 可以配置系统集的顺序,使用 .chain() 方法可以链式系统集。
进阶用法
自定义 Schedule
可以创建自定义 Schedule,用于特定的执行策略。
源代码文件:bevy/examples/ecs/custom_schedule.rs
代码示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| use bevy::{ app::MainScheduleOrder, ecs::schedule::{ExecutorKind, ScheduleLabel}, prelude::*, };
#[derive(ScheduleLabel, Debug, Hash, PartialEq, Eq, Clone)] struct SingleThreadedUpdate;
#[derive(ScheduleLabel, Debug, Hash, PartialEq, Eq, Clone)] struct CustomStartup;
fn main() { let mut app = App::new();
let mut custom_update_schedule = Schedule::new(SingleThreadedUpdate); custom_update_schedule.set_executor_kind(ExecutorKind::SingleThreaded);
app.add_schedule(custom_update_schedule);
let mut main_schedule_order = app.world_mut().resource_mut::<MainScheduleOrder>(); main_schedule_order.insert_after(Update, SingleThreadedUpdate);
app.add_schedule(Schedule::new(CustomStartup));
let mut main_schedule_order = app.world_mut().resource_mut::<MainScheduleOrder>(); main_schedule_order.insert_startup_after(PreStartup, CustomStartup);
app.add_systems(SingleThreadedUpdate, single_threaded_update_system) .add_systems(CustomStartup, custom_startup_system) .add_systems(PreStartup, pre_startup_system) .add_systems(Startup, startup_system) .add_systems(First, first_system) .add_systems(Update, update_system) .add_systems(Last, last_system) .run(); }
|
关键要点:
- 使用
Schedule::new() 创建自定义 Schedule
- 使用
ScheduleLabel 派生宏定义 Schedule 标签
- 使用
set_executor_kind() 设置执行器类型
- 使用
MainScheduleOrder 配置 Schedule 顺序
注意事项:
- 自定义 Schedule 需要添加到应用
- 需要配置
MainScheduleOrder 来运行自定义 Schedule
- 可以使用
ExecutorKind::SingleThreaded 创建单线程 Schedule
最佳实践:
- 对于需要特定执行策略的系统,使用自定义 Schedule
- 对于需要单线程执行的系统,使用单线程 Schedule
- 注意自定义 Schedule 的执行顺序
固定时间步(Fixed Timestep)
固定时间步允许系统以固定时间间隔运行,而不是每帧运行。
源代码文件:bevy/examples/ecs/fixed_timestep.rs
代码示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| use bevy::prelude::*;
fn main() { App::new() .add_plugins(DefaultPlugins) .add_systems(Update, frame_update) .add_systems(FixedUpdate, fixed_update) .insert_resource(Time::<Fixed>::from_seconds(0.5)) .run(); }
fn frame_update(mut last_time: Local<f32>, time: Res<Time>) { info!( "time since last frame_update: {}", time.elapsed_secs() - *last_time ); *last_time = time.elapsed_secs(); }
fn fixed_update(mut last_time: Local<f32>, time: Res<Time>, fixed_time: Res<Time<Fixed>>) { info!( "time since last fixed_update: {}\n", time.elapsed_secs() - *last_time );
info!("fixed timestep: {}\n", time.delta_secs()); info!( "time accrued toward next fixed_update: {}\n", fixed_time.overstep().as_secs_f32() ); *last_time = time.elapsed_secs(); }
|
关键要点:
- 使用
FixedUpdate Schedule 运行固定时间步系统
- 使用
Time::<Fixed>::from_seconds() 配置固定时间步
- 固定时间步系统以固定时间间隔运行
- 固定时间步适合物理模拟等需要稳定时间步的场景
注意事项:
- 固定时间步系统以固定时间间隔运行
- 固定时间步适合物理模拟等需要稳定时间步的场景
- 注意固定时间步与帧率的区别
最佳实践:
- 对于物理模拟等需要稳定时间步的系统,使用固定时间步
- 对于与帧率相关的系统,使用
Update Schedule
- 注意固定时间步与帧率的区别
运行条件(Run Conditions)
运行条件允许控制系统是否应该运行。
源代码文件:bevy/examples/ecs/run_conditions.rs
代码示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
| use bevy::prelude::*;
fn main() { App::new() .add_plugins(DefaultPlugins) .init_resource::<InputCounter>() .add_systems( Update, ( increment_input_counter .run_if(resource_exists::<InputCounter>) .run_if(resource_exists::<Unused>.or( has_user_input, )), print_input_counter .run_if(resource_exists::<InputCounter>.and( |counter: Res<InputCounter>| counter.is_changed() && !counter.is_added(), )), print_time_message .run_if(time_passed(2.0)) .run_if(not(time_passed(2.5))), ), ) .run(); }
fn has_user_input( keyboard_input: Res<ButtonInput<KeyCode>>, mouse_button_input: Res<ButtonInput<MouseButton>>, touch_input: Res<Touches>, ) -> bool { keyboard_input.just_pressed(KeyCode::Space) || keyboard_input.just_pressed(KeyCode::Enter) || mouse_button_input.just_pressed(MouseButton::Left) || mouse_button_input.just_pressed(MouseButton::Right) || touch_input.any_just_pressed() }
fn time_passed(t: f32) -> impl FnMut(Local<f32>, Res<Time>) -> bool { move |mut timer: Local<f32>, time: Res<Time>| { *timer += time.delta_secs(); *timer >= t } }
|
关键要点:
- 使用
.run_if() 方法添加运行条件
- 使用
.and() 和 .or() 组合运行条件
- 使用
not() 反转运行条件
- 运行条件可以是函数或闭包
注意事项:
- 运行条件必须返回
bool
- 运行条件参数必须是只读的(除了本地参数)
- 只有所有运行条件都返回
true 时,系统才会运行
最佳实践:
- 对于条件系统执行,使用运行条件
- 使用
.and() 和 .or() 组合运行条件
- 注意运行条件的性能影响
系统步进(System Stepping)
系统步进允许逐步执行系统,用于调试。
源代码文件:bevy/examples/ecs/system_stepping.rs
代码示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| use bevy::{ecs::schedule::Stepping, log::LogPlugin, prelude::*};
fn main() { let mut app = App::new();
app.add_plugins(LogPlugin::default()) .add_systems( Update, ( update_system_one, update_system_two.after(update_system_one), update_system_three.after(update_system_two), update_system_four, ), ) .add_systems(PreUpdate, pre_update_system);
app.insert_resource(Stepping::new()); let mut stepping = app.world_mut().resource_mut::<Stepping>(); stepping.add_schedule(Update).enable(); stepping.step_frame(); app.update(); stepping.continue_frame(); app.update(); }
|
关键要点:
- 使用
Stepping 资源控制系统步进
- 使用
step_frame() 步进一帧
- 使用
continue_frame() 继续执行剩余系统
- 使用
always_run() 和 never_run() 控制特定系统的执行
注意事项:
- 系统步进需要启用
bevy_debug_stepping 特性
- 系统步进主要用于调试
- 注意系统步进对性能的影响
最佳实践:
- 对于调试,使用系统步进
- 对于生产环境,禁用系统步进
- 注意系统步进对性能的影响
非确定性系统顺序
默认情况下,Bevy 系统并行运行,除非明确指定顺序,否则它们的相对顺序是非确定性的。
源代码文件:bevy/examples/ecs/nondeterministic_system_order.rs
代码示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| use bevy::{ ecs::schedule::{LogLevel, ScheduleBuildSettings}, prelude::*, };
fn main() { App::new() .edit_schedule(Update, |schedule| { schedule.set_build_settings(ScheduleBuildSettings { ambiguity_detection: LogLevel::Warn, ..default() }); }) .init_resource::<A>() .init_resource::<B>() .add_systems( Update, ( reads_a, writes_a, adds_one_to_b, doubles_b.after(adds_one_to_b), reads_b.after(doubles_b), reads_a_and_b.ambiguous_with(adds_one_to_b), ), ) .add_plugins(DefaultPlugins) .run(); }
|
关键要点:
- 默认情况下,系统并行运行,顺序是非确定性的
- 使用
.after() 和 .before() 明确指定顺序
- 使用
.ambiguous_with() 静默歧义
- 使用
ScheduleBuildSettings 配置歧义检测
注意事项:
- 非确定性顺序可能导致微妙的错误
- 使用
.after() 和 .before() 明确指定顺序
- 注意歧义检测的性能影响
最佳实践:
- 对于有数据访问冲突的系统,明确指定顺序
- 使用
.ambiguous_with() 静默明显的假阳性
- 注意歧义检测的性能影响
实际应用
在游戏开发中的应用场景
系统调度在游戏开发中有广泛的应用:
- 游戏逻辑:使用
Update Schedule 运行游戏逻辑
- 物理模拟:使用
FixedUpdate Schedule 运行物理模拟
- 初始化:使用
Startup Schedule 运行初始化代码
- 清理:使用
Last Schedule 运行清理代码
常见问题
问题 1:如何控制系统执行顺序?
解决方案:
- 使用
.before() 和 .after() 方法控制顺序
- 使用系统集组织系统
- 使用
.chain() 方法链式系统
问题 2:如何创建自定义 Schedule?
解决方案:
- 使用
Schedule::new() 创建自定义 Schedule
- 使用
ScheduleLabel 派生宏定义 Schedule 标签
- 使用
MainScheduleOrder 配置 Schedule 顺序
问题 3:如何控制系统是否运行?
解决方案:
- 使用
.run_if() 方法添加运行条件
- 使用
.and() 和 .or() 组合运行条件
- 使用
not() 反转运行条件
性能考虑
- 并行执行:系统可以并行执行,但可变访问会阻止并行
- 系统顺序:明确指定系统顺序可以提高性能
- 运行条件:注意运行条件的性能影响
相关资源
相关源代码文件:
bevy/examples/ecs/ecs_guide.rs - ECS 完整指南示例(系统调度)
bevy/examples/ecs/custom_schedule.rs - 自定义 Schedule 示例
bevy/examples/ecs/fixed_timestep.rs - 固定时间步示例
bevy/examples/ecs/run_conditions.rs - 运行条件示例
bevy/examples/ecs/system_stepping.rs - 系统步进示例
bevy/examples/ecs/nondeterministic_system_order.rs - 非确定性系统顺序示例
官方文档链接:
进一步学习建议:
- 学习系统(Systems),了解如何定义系统
- 学习资源(Resources),了解如何访问资源
- 学习 ECS 进阶,了解系统调度的高级功能
索引:返回上级目录