第六章:系统调度 - 让混乱变得井然有序

🎯 本章目标

学完这一章,你将能够:

  • 理解为什么需要控制系统执行顺序
  • 使用.before()、.after()和.chain()控制系统顺序
  • 创建和使用SystemSet组织相关系统
  • 设计清晰的数据流架构

🏫 学校运营的秩序

想象学校的一天必须按照特定顺序进行:

错误的顺序:
❌ 先上课 → 再开门 → 最后老师到校
❌ 先发成绩单 → 再考试 → 最后批改试卷

正确的顺序:
✅ 开门 → 老师到校 → 学生入校 → 上课
✅ 考试 → 批改试卷 → 统计成绩 → 发成绩单

在Bevy中,系统默认是并行运行的,这意味着执行顺序不可预测。有时候这没问题,但当系统之间有依赖关系时,我们就需要系统调度来确保正确的执行顺序。

🚀 基础项目设置

use bevy::prelude::*;
use std::collections::HashMap;
 
fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .init_resource::<SchoolSchedule>()
        .init_resource::<ExamResults>()
        .init_resource::<SchoolStats>()
        
        // 不同阶段的系统组织
        .add_systems(Startup, setup_school)
        
        // 主要的系统调度演示
        .add_systems(Update, (
            // 第一组:基础输入和时间管理(优先级最高)
            (
                handle_keyboard_input,
                update_school_time,
            ).chain(), // 确保按顺序执行
            
            // 第二组:学生和教师管理
            (
                process_student_attendance,
                assign_classrooms,
                prepare_teaching_materials,
            ).chain().after(update_school_time), // 在时间更新后执行
            
            // 第三组:教学活动
            (
                conduct_classes,
                handle_exam_taking,
                grade_exams,
            ).chain().after(prepare_teaching_materials),
            
            // 第四组:统计和显示(最后执行)
            (
                calculate_statistics,
                update_displays,
                print_daily_report,
            ).chain().after(grade_exams),
        ))
        
        .run();
}

📚 基础组件和资源

// 基础组件
#[derive(Component)]
struct Student {
    id: u32,
    name: String,
    grade: u8,
}
 
#[derive(Component)]
struct Teacher {
    id: u32,
    name: String,
    subject: String,
}
 
#[derive(Component)]
struct Present; // 出勤标记
 
#[derive(Component)]
struct Classroom {
    number: u8,
    capacity: u32,
}
 
#[derive(Component)]
struct Exam {
    subject: String,
    max_score: u32,
}
 
#[derive(Component)]
struct ExamScore {
    score: u32,
    graded: bool,
}
 
// 资源
#[derive(Resource)]
struct SchoolSchedule {
    current_period: u8,
    periods_today: u8,
    current_activity: String,
}
 
impl Default for SchoolSchedule {
    fn default() -> Self {
        Self {
            current_period: 1,
            periods_today: 6,
            current_activity: "晨会".to_string(),
        }
    }
}
 
#[derive(Resource, Default)]
struct ExamResults {
    results: HashMap<u32, Vec<u32>>, // 学生ID -> 成绩列表
    average_scores: HashMap<String, f32>, // 科目 -> 平均分
}
 
#[derive(Resource, Default)]
struct SchoolStats {
    present_students: u32,
    total_exams_graded: u32,
    classrooms_assigned: u32,
}

⏰ 第一阶段:输入和时间管理

// 系统1:处理键盘输入
fn handle_keyboard_input(
    keyboard_input: Res<ButtonInput<KeyCode>>,
    mut schedule: ResMut<SchoolSchedule>,
) {
    if keyboard_input.just_pressed(KeyCode::Space) {
        schedule.current_period = (schedule.current_period % schedule.periods_today) + 1;
        
        schedule.current_activity = match schedule.current_period {
            1 => "第一节课".to_string(),
            2 => "第二节课".to_string(),
            3 => "课间操".to_string(),
            4 => "第三节课".to_string(),
            5 => "午休".to_string(),
            6 => "考试时间".to_string(),
            _ => "放学".to_string(),
        };
        
        println!("🕐 切换到:{} (第{}节)", schedule.current_activity, schedule.current_period);
    }
    
    if keyboard_input.just_pressed(KeyCode::KeyR) {
        println!("🔄 重置为第一节课");
        schedule.current_period = 1;
        schedule.current_activity = "第一节课".to_string();
    }
}
 
// 系统2:更新学校时间(必须在输入处理后)
fn update_school_time(
    schedule: Res<SchoolSchedule>,
    time: Res<Time>,
    mut last_update: Local<f32>,
) {
    // 这个系统依赖于handle_keyboard_input的输入结果
    *last_update += time.delta_seconds();
    
    if *last_update >= 2.0 {
        println!("⏰ 当前时段:{}", schedule.current_activity);
        *last_update = 0.0;
    }
}

👥 第二阶段:学生和教师管理

// 系统3:处理学生出勤(必须在时间确定后)
fn process_student_attendance(
    mut commands: Commands,
    schedule: Res<SchoolSchedule>,
    student_query: Query<Entity, (With<Student>, Without<Present>)>,
    mut stats: ResMut<SchoolStats>,
) {
    // 这个系统依赖update_school_time确定当前时段
    if schedule.current_period == 1 {
        let mut present_count = 0;
        
        // 模拟学生到校
        for entity in student_query.iter().take(3) {
            commands.entity(entity).insert(Present);
            present_count += 1;
        }
        
        if present_count > 0 {
            stats.present_students = present_count;
            println!("📝 {}名学生已到校签到", present_count);
        }
    }
}
 
// 系统4:分配教室(必须在出勤处理后)
fn assign_classrooms(
    schedule: Res<SchoolSchedule>,
    mut commands: Commands,
    student_query: Query<Entity, With<Present>>,
    classroom_query: Query<&Classroom>,
    mut stats: ResMut<SchoolStats>,
) {
    // 这个系统需要知道有多少学生出勤
    if schedule.current_period <= 2 {
        let present_students = student_query.iter().count();
        let available_classrooms = classroom_query.iter().count();
        
        if present_students > 0 && available_classrooms > 0 {
            stats.classrooms_assigned = available_classrooms as u32;
            println!("🏫 为{}名学生分配了{}间教室", present_students, available_classrooms);
        }
    }
}
 
// 系统5:准备教学材料(必须在教室分配后)
fn prepare_teaching_materials(
    schedule: Res<SchoolSchedule>,
    teacher_query: Query<&Teacher>,
    stats: Res<SchoolStats>,
) {
    // 这个系统依赖教室分配的结果
    if schedule.current_period <= 4 && stats.classrooms_assigned > 0 {
        for teacher in teacher_query.iter() {
            println!("📖 {}老师正在准备{}课程材料", teacher.name, teacher.subject);
        }
    }
}

📖 第三阶段:教学活动

// 系统6:进行课堂教学
fn conduct_classes(
    schedule: Res<SchoolSchedule>,
    teacher_query: Query<&Teacher>,
    student_query: Query<&Student, With<Present>>,
) {
    if matches!(schedule.current_period, 1 | 2 | 4) {
        let student_count = student_query.iter().count();
        let teacher_count = teacher_query.iter().count();
        
        if student_count > 0 && teacher_count > 0 {
            println!("👨‍🏫 正在进行课堂教学:{}名老师教{}名学生", 
                    teacher_count, student_count);
        }
    }
}
 
// 系统7:考试进行
fn handle_exam_taking(
    mut commands: Commands,
    schedule: Res<SchoolSchedule>,
    student_query: Query<Entity, (With<Student>, With<Present>, Without<ExamScore>)>,
) {
    if schedule.current_period == 6 {
        // 为参加考试的学生生成考试成绩
        for entity in student_query.iter() {
            let score = 60 + (rand::random::<u32>() % 40); // 60-100分
            commands.entity(entity).insert((
                Exam { subject: "数学".to_string(), max_score: 100 },
                ExamScore { score, graded: false },
            ));
        }
        
        let exam_count = student_query.iter().count();
        if exam_count > 0 {
            println!("📝 {}名学生正在参加数学考试", exam_count);
        }
    }
}
 
// 系统8:批改考试(必须在考试完成后)
fn grade_exams(
    mut exam_query: Query<(&mut ExamScore, &Exam), Changed<ExamScore>>,
    mut results: ResMut<ExamResults>,
    mut stats: ResMut<SchoolStats>,
    student_query: Query<&Student>,
) {
    let mut newly_graded = 0;
    
    for (mut exam_score, exam) in exam_query.iter_mut() {
        if !exam_score.graded {
            exam_score.graded = true;
            newly_graded += 1;
            
            // 记录成绩到结果中
            // 这里简化处理,实际应该关联具体学生
            results.results.entry(1).or_insert_with(Vec::new).push(exam_score.score);
        }
    }
    
    if newly_graded > 0 {
        stats.total_exams_graded += newly_graded;
        println!("✅ 批改了{}份考试,总计已批改{}份", 
                newly_graded, stats.total_exams_graded);
    }
}

📊 第四阶段:统计和显示

// 系统9:计算统计数据(必须在批改完成后)
fn calculate_statistics(
    mut results: ResMut<ExamResults>,
    exam_query: Query<&ExamScore, With<Exam>>,
) {
    if !exam_query.is_empty() {
        let scores: Vec<u32> = exam_query.iter().map(|e| e.score).collect();
        
        if !scores.is_empty() {
            let average = scores.iter().sum::<u32>() as f32 / scores.len() as f32;
            results.average_scores.insert("数学".to_string(), average);
        }
    }
}
 
// 系统10:更新显示(必须在统计计算后)
fn update_displays(
    schedule: Res<SchoolSchedule>,
    stats: Res<SchoolStats>,
    results: Res<ExamResults>,
) {
    // 这个系统依赖统计数据的计算结果
    if let Some(avg_score) = results.average_scores.get("数学") {
        if *avg_score > 0.0 {
            println!("📊 数学考试平均分:{:.1}分", avg_score);
        }
    }
}
 
// 系统11:打印日报(最后执行)
fn print_daily_report(
    schedule: Res<SchoolSchedule>,
    stats: Res<SchoolStats>,
    results: Res<ExamResults>,
    keyboard_input: Res<ButtonInput<KeyCode>>,
) {
    // 按P键打印完整报告
    if keyboard_input.just_pressed(KeyCode::KeyP) {
        println!("\n📋 === 学校日报 ===");
        println!("当前时段:{}", schedule.current_activity);
        println!("出勤学生:{}人", stats.present_students);
        println!("分配教室:{}间", stats.classrooms_assigned);
        println!("已批改考试:{}份", stats.total_exams_graded);
        
        if let Some(avg) = results.average_scores.get("数学") {
            println!("数学平均分:{:.1}分", avg);
        }
        println!("================\n");
    }
}

🎯 使用SystemSet组织系统

当项目变大时,用SystemSet来组织系统更清晰:

// 定义系统集
#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
enum SchoolSystemSet {
    Input,      // 输入处理
    Setup,      // 设置和准备
    Teaching,   // 教学活动
    Evaluation, // 评估统计
    Display,    // 显示和报告
}
 
fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .init_resource::<SchoolSchedule>()
        .init_resource::<ExamResults>()
        .init_resource::<SchoolStats>()
        
        // 配置系统集的执行顺序
        .configure_sets(Update, (
            SchoolSystemSet::Input,
            SchoolSystemSet::Setup,
            SchoolSystemSet::Teaching,
            SchoolSystemSet::Evaluation,
            SchoolSystemSet::Display,
        ).chain())
        
        // 将系统分配到相应的集合
        .add_systems(Update, (
            // 输入处理集合
            (
                handle_keyboard_input,
                update_school_time,
            ).in_set(SchoolSystemSet::Input),
            
            // 设置准备集合
            (
                process_student_attendance,
                assign_classrooms,
                prepare_teaching_materials,
            ).in_set(SchoolSystemSet::Setup),
            
            // 教学活动集合
            (
                conduct_classes,
                handle_exam_taking,
                grade_exams,
            ).in_set(SchoolSystemSet::Teaching),
            
            // 评估统计集合
            (
                calculate_statistics,
            ).in_set(SchoolSystemSet::Evaluation),
            
            // 显示报告集合
            (
                update_displays,
                print_daily_report,
            ).in_set(SchoolSystemSet::Display),
        ))
        
        .add_systems(Startup, setup_school)
        .run();
}
 
fn setup_school(mut commands: Commands) {
    println!("🏫 学校系统初始化");
    println!("🎮 控制说明:");
    println!("   空格键 - 切换到下一个时段");
    println!("   R键 - 重置到第一节课");
    println!("   P键 - 打印完整日报");
    println!("观察系统按正确顺序执行!\n");
    
    // 创建学生
    for i in 1..=5 {
        commands.spawn(Student {
            id: i,
            name: format!("学生{}", i),
            grade: 3,
        });
    }
    
    // 创建老师
    commands.spawn(Teacher {
        id: 1,
        name: "张老师".to_string(),
        subject: "数学".to_string(),
    });
    
    commands.spawn(Teacher {
        id: 2,
        name: "李老师".to_string(),
        subject: "语文".to_string(),
    });
    
    // 创建教室
    for i in 1..=3 {
        commands.spawn(Classroom {
            number: i,
            capacity: 30,
        });
    }
}

🔧 高级调度技巧

1. 条件执行与调度结合

// 只在特定条件下按顺序执行系统
.add_systems(Update, (
    system_a,
    system_b.after(system_a),
    system_c.after(system_b),
).run_if(in_state(GameState::Playing)))

2. 灵活的依赖关系

// 复杂的依赖关系
.add_systems(Update, (
    input_system,
    (
        physics_system,
        ai_system,
    ).after(input_system), // 这两个可以并行
    render_system.after(physics_system).after(ai_system), // 等两个都完成
))

3. 系统集的嵌套

#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
enum GameplaySet {
    Input,
    Logic,
    Physics,
}
 
#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
enum PhysicsSet {
    Movement,
    Collision,
    Cleanup,
}
 
.configure_sets(Update, (
    GameplaySet::Input,
    GameplaySet::Logic,
    GameplaySet::Physics,
).chain())
 
.configure_sets(Update, (
    PhysicsSet::Movement,
    PhysicsSet::Collision, 
    PhysicsSet::Cleanup,
).chain().in_set(GameplaySet::Physics))

⚠️ 常见的调度陷阱

1. 循环依赖

// ❌ 这会导致错误!
.add_systems(Update, (
    system_a.after(system_b),
    system_b.after(system_a), // 循环依赖!
))

2. 过度细化

// ❌ 过度复杂,难以维护
.add_systems(Update, (
    sys1.before(sys2).after(sys3).before(sys4).after(sys5)
))
 
// ✅ 使用SystemSet更清晰
.configure_sets(Update, (SetA, SetB, SetC).chain())
.add_systems(Update, sys1.in_set(SetA))

3. 忽略并行性

// ❌ 不必要的串行化
.add_systems(Update, (
    independent_system_a,
    independent_system_b.after(independent_system_a), // 不必要
))
 
// ✅ 让独立系统并行运行
.add_systems(Update, (
    independent_system_a,
    independent_system_b, // 可以并行
    dependent_system.after(independent_system_a),
))

📊 调度性能优化

  1. 最小化强制顺序:只对真正有依赖的系统使用.after()/.before()
  2. 合理分组:将相关系统放在同一个SystemSet中
  3. 避免不必要的.chain():.chain()会完全串行化系统

🤔 检查理解

  1. 什么情况下需要控制系统执行顺序?
  2. .chain().after() 的区别是什么?
  3. SystemSet有什么优势?
  4. 如何避免创建循环依赖?

💪 练习挑战

  1. 为学校系统添加一个新的”SecuritySet”(安全检查集合)
  2. 实现一个”午餐供应”系统链:准备→分发→清理
  3. 创建一个条件执行的系统:只在考试时段运行的监考系统
  4. 使用嵌套SystemSet组织一个复杂的”体育课”系统

🎯 系统调度设计原则

  1. 数据流驱动:按照数据的流向安排系统顺序
  2. 最小约束:只对必要的依赖关系添加约束
  3. 清晰分层:用SystemSet创建清晰的架构层次
  4. 文档化:为复杂的调度添加注释说明

下一步

恭喜!你已经掌握了Bevy ECS的核心概念。下一章我们将总结所有学到的内容,并讨论如何选择合适的工具来解决实际问题!