🎯 本章目标

学完这一章,你将能够:

  • 使用必需组件简化实体创建
  • 理解和使用资源管理全局数据
  • 掌握现代Bevy的最佳实践

🔧 必需组件:让创建实体更简单

还记得上一章我们创建学生时要写很多组件吗?现在有更简洁的方式:

rust

// 新的方式:使用必需组件
#[derive(Component)]
#[require(Name, Grade)] // Student组件自动需要Name和Grade
struct Student {
    student_id: u32, // 学生ID
}
 
// 现在创建学生变得超级简单
fn setup_students(mut commands: Commands) {
    // 只需要提供核心的Student组件,其他会自动添加
    commands.spawn(Student { student_id: 001 });
    
    // 如果想自定义某些组件,也可以手动提供
    commands.spawn((
        Student { student_id: 002 },
        Name("特殊学生李华".to_string()),
    ));
}

为什么这样更好?

  • 不会忘记添加必要的组件
  • 代码更简洁,专注于核心概念
  • 自动有合理的默认值

🌍 资源:管理全局数据

不是所有数据都属于特定实体。比如:

  • 学校名称(全校只有一个)
  • 当前学期(全校统一)
  • 总学生数量(全局统计)

这些用资源来管理:

rust

// 定义一个资源
#[derive(Resource)]
struct SchoolInfo {
    name: String,
    semester: String,
    total_students: u32,
}
 
// 在App中注册资源
fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        // 插入资源的初始值
        .insert_resource(SchoolInfo {
            name: "阳光小学".to_string(),
            semester: "2024春季".to_string(),
            total_students: 0,
        })
        .add_systems(Startup, setup_students)
        .add_systems(Update, update_student_count)
        .run();
}
 
// 系统中使用资源
fn update_student_count(
    student_query: Query<&Student>,
    mut school_info: ResMut<SchoolInfo>, // 可变访问资源
) {
    let current_count = student_query.iter().count() as u32;
    
    // 只在数量变化时更新(避免无意义的计算)
    if school_info.total_students != current_count {
        school_info.total_students = current_count;
        println!("{}当前有{}名学生", 
                school_info.name, 
                school_info.total_students);
    }
}
 
// 只读访问资源
fn print_school_info(school_info: Res<SchoolInfo>) {
    println!("学校:{}, 学期:{}", 
            school_info.name, 
            school_info.semester);
}

📊 实际应用示例:学生管理系统

让我们创建一个更完整的示例:

rust

use bevy::prelude::*;
 
fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .insert_resource(SchoolStats::default())
        .add_systems(Startup, setup_school)
        .add_systems(Update, (
            monitor_student_changes,
            print_stats.run_if(resource_changed::<SchoolStats>),
        ))
        .run();
}
 
#[derive(Component)]
#[require(Name(default_name), Grade(|| Grade(1)))] // 提供默认值
struct Student {
    id: u32,
    enrollment_date: String,
}
 
#[derive(Component)]
struct Name(String);
 
#[derive(Component)]  
struct Grade(u8);
 
#[derive(Resource, Default)]
struct SchoolStats {
    total_students: u32,
    grade_distribution: [u32; 6], // 1-6年级的人数分布
}
 
// 默认名称生成函数
fn default_name() -> Name {
    Name("新学生".to_string())
}
 
fn setup_school(mut commands: Commands) {
    // 创建不同年级的学生
    let students_data = [
        ("张小明", 3, 101),
        ("李华", 2, 102),  
        ("王小红", 1, 103),
        ("赵大壮", 4, 104),
        ("孙小花", 5, 105),
    ];
    
    for (name, grade, id) in students_data {
        commands.spawn((
            Student { 
                id, 
                enrollment_date: "2024-09-01".to_string() 
            },
            Name(name.to_string()),
            Grade(grade),
        ));
    }
}
 
fn monitor_student_changes(
    student_query: Query<&Grade, With<Student>>,
    mut stats: ResMut<SchoolStats>,
) {
    // 重新计算统计数据
    let mut new_grade_distribution = [0; 6];
    let mut total = 0;
    
    for grade in &student_query {
        if grade.0 >= 1 && grade.0 <= 6 {
            new_grade_distribution[(grade.0 - 1) as usize] += 1;
            total += 1;
        }
    }
    
    // 只在数据变化时更新
    if stats.total_students != total || 
       stats.grade_distribution != new_grade_distribution {
        stats.total_students = total;
        stats.grade_distribution = new_grade_distribution;
    }
}
 
fn print_stats(stats: Res<SchoolStats>) {
    println!("\n=== 阳光小学统计 ===");
    println!("总学生数: {}", stats.total_students);
    println!("年级分布:");
    for (i, count) in stats.grade_distribution.iter().enumerate() {
        if *count > 0 {
            println!("  {}年级: {}人", i + 1, count);
        }
    }
    println!("==================\n");
}

🔍 资源 vs 组件:何时使用哪个?

使用场景选择原因
全局唯一的数据资源比如游戏设置、时间、分数
属于特定实体的数据组件比如位置、生命值、名称
配置信息资源比如窗口设置、音量
实体的状态/属性组件比如是否可见、移动速度

💡 最佳实践提示

  1. 使用必需组件减少出错机会
  2. 资源用于全局数据,组件用于实体数据
  3. 利用变更检测避免不必要的计算
  4. 合理命名让代码自解释

🤔 检查理解

  1. 什么时候使用必需组件?
  2. 资源和组件的区别是什么?
  3. Res<T>ResMut<T> 的区别?

下一步

下一章我们将学习事件系统,了解如何让游戏的不同部分相互通信!