🎯 本章目标
学完这一章,你将能够:
- 理解观察者与事件的区别
- 使用OnAdd和OnRemove观察者响应组件变化
- 掌握观察者的即时性特点
- 避免观察者使用中的常见陷阱
🚨 为什么需要观察者?
还记得我们的学校系统吗?想象这样的场景:
场景1:学生请病假
- 传统方式:每帧检查所有学生的出勤状态,看谁请了病假
- 观察者方式:当学生被标记为”病假”的瞬间,立即通知相关系统
场景2:新老师入职
- 传统方式:人事系统定期检查是否有新老师
- 观察者方式:新老师实体创建时,立即分配办公室、更新花名册
观察者就像学校里的智能监控系统,不是定时巡逻,而是有事情发生就立即响应。
🔍 观察者 vs 事件:关键区别
// 事件方式:需要主动"拉取"
fn handle_student_events(mut events: EventReader<StudentSickEvent>) {
for event in events.read() {
// 处理事件(可能延迟1-2帧)
}
}
// 观察者方式:被动"推送",立即响应
fn on_student_sick_added(
trigger: Trigger<OnAdd<SickLeave>>, // 当SickLeave组件被添加时立即触发
mut commands: Commands,
query: Query<&Name>,
) {
let student_entity = trigger.target();
if let Ok(name) = query.get(student_entity) {
println!("🤒 学生{}请病假了,立即处理!", name.0);
// 立即执行相关逻辑
}
}
🏗️ 设置观察者系统
use bevy::prelude::*;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.init_state::<SchoolState>()
.insert_resource(SchoolRegistry::default())
// 注册观察者(不是事件!)
.add_observer(on_student_added)
.add_observer(on_teacher_added)
.add_observer(on_sick_leave_added)
.add_observer(on_sick_leave_removed)
.add_observer(on_student_removed)
.add_observer(on_special_student_added)
.add_systems(Startup, (setup_school, print_instructions))
.add_systems(Update, (
handle_keyboard_input,
simulate_random_events,
display_registry.run_if(resource_changed::<SchoolRegistry>),
))
.run();
}
// 学校注册表资源
#[derive(Resource, Default)]
struct SchoolRegistry {
total_students: u32,
total_teachers: u32,
sick_students: Vec<String>,
office_assignments: Vec<String>,
}
#[derive(States, Debug, Clone, PartialEq, Eq, Hash, Default)]
enum SchoolState {
#[default]
Normal,
Emergency,
}
👨🎓 学生相关的观察者
// 基础组件
#[derive(Component)]
struct Student {
id: u32,
class: String,
}
#[derive(Component)]
struct Name(String);
#[derive(Component)]
struct SickLeave {
reason: String,
days: u8,
}
#[derive(Component)]
struct SpecialNeeds {
requirement: String,
}
// 观察者:当学生实体被创建时
fn on_student_added(
trigger: Trigger<OnAdd<Student>>,
mut registry: ResMut<SchoolRegistry>,
query: Query<&Name>,
) {
let student_entity = trigger.target();
if let Ok(name) = query.get(student_entity) {
registry.total_students += 1;
println!("📝 新学生{}已登记注册(学生总数:{})",
name.0, registry.total_students);
// 可以在这里添加更多初始化逻辑
// 比如分配学号、创建学籍档案等
}
}
// 观察者:当学生请病假时
fn on_sick_leave_added(
trigger: Trigger<OnAdd<SickLeave>>,
mut registry: ResMut<SchoolRegistry>,
student_query: Query<&Name, With<Student>>,
sick_query: Query<&SickLeave>,
) {
let student_entity = trigger.target();
if let (Ok(name), Ok(sick_leave)) = (
student_query.get(student_entity),
sick_query.get(student_entity)
) {
let student_name = name.0.clone();
registry.sick_students.push(student_name.clone());
println!("🤒 学生{}请病假{}天(原因:{})",
student_name, sick_leave.days, sick_leave.reason);
println!(" 立即通知:班主任、校医、家长");
// 如果是传染病,提升安全级别
if sick_leave.reason.contains("发烧") || sick_leave.reason.contains("咳嗽") {
println!("⚠️ 可能传染病,建议提升防护级别!");
}
}
}
// 观察者:当学生病假结束时
fn on_sick_leave_removed(
trigger: Trigger<OnRemove<SickLeave>>,
mut registry: ResMut<SchoolRegistry>,
query: Query<&Name, With<Student>>,
) {
let student_entity = trigger.target();
if let Ok(name) = query.get(student_entity) {
let student_name = &name.0;
// 从病假列表中移除
if let Some(pos) = registry.sick_students.iter().position(|x| x == student_name) {
registry.sick_students.remove(pos);
println!("😊 学生{}病假结束,欢迎回校!", student_name);
println!(" 需要:健康检查、补课安排、心理关怀");
}
}
}
// 观察者:当学生被删除时(转学、毕业等)
fn on_student_removed(
trigger: Trigger<OnRemove<Student>>,
mut registry: ResMut<SchoolRegistry>,
) {
registry.total_students = registry.total_students.saturating_sub(1);
println!("👋 一名学生离校(转学/毕业),总数更新为:{}", registry.total_students);
// 这里可以添加离校手续:
// - 清理学籍档案
// - 退还押金
// - 发放证明文件
}
// 观察者:特殊需求学生
fn on_special_student_added(
trigger: Trigger<OnAdd<SpecialNeeds>>,
query: Query<(&Name, &SpecialNeeds), With<Student>>,
) {
let student_entity = trigger.target();
if let Ok((name, special_needs)) = query.get(student_entity) {
println!("🌟 特殊需求学生{}需要:{}", name.0, special_needs.requirement);
println!(" 立即安排:专门座位、辅助设备、个性化教学计划");
}
}
👨🏫 教师相关的观察者
#[derive(Component)]
struct Teacher {
subject: String,
experience_years: u8,
}
// 观察者:新教师入职
fn on_teacher_added(
trigger: Trigger<OnAdd<Teacher>>,
mut registry: ResMut<SchoolRegistry>,
mut commands: Commands,
query: Query<(&Name, &Teacher)>,
) {
let teacher_entity = trigger.target();
if let Ok((name, teacher)) = query.get(teacher_entity) {
registry.total_teachers += 1;
let office_info = format!("{}老师 - {}办公室", name.0, teacher.subject);
registry.office_assignments.push(office_info);
println!("🎓 新教师{}入职({}),教师总数:{}",
name.0, teacher.subject, registry.total_teachers);
// 根据经验自动分配责任
if teacher.experience_years >= 5 {
commands.entity(teacher_entity).insert(SeniorTeacher);
println!(" 资深教师,自动分配导师职责");
}
// 立即安排入职流程
println!(" 立即安排:办公室分配、教学设备、课程表制定");
}
}
#[derive(Component)]
struct SeniorTeacher;
🎮 交互和模拟系统
// 键盘输入处理
fn handle_keyboard_input(
mut commands: Commands,
keyboard_input: Res<ButtonInput<KeyCode>>,
) {
// 添加新学生
if keyboard_input.just_pressed(KeyCode::KeyS) {
let student_id = 1000 + rand::random::<u8>() as u32;
let student_name = format!("学生{}", student_id);
commands.spawn((
Student {
id: student_id,
class: "三年级".to_string()
},
Name(student_name),
));
}
// 添加新教师
if keyboard_input.just_pressed(KeyCode::KeyT) {
let subjects = ["数学", "语文", "英语", "科学"];
let subject = subjects[rand::random::<usize>() % subjects.len()];
let teacher_name = format!("{}老师{}", subject, rand::random::<u8>());
commands.spawn((
Teacher {
subject: subject.to_string(),
experience_years: rand::random::<u8>() % 10 + 1,
},
Name(teacher_name),
));
}
// 让第一个健康学生请病假
if keyboard_input.just_pressed(KeyCode::KeyI) {
// 这里需要实现查找健康学生并添加病假组件的逻辑
println!("🤒 模拟学生请病假...");
}
// 让第一个病假学生康复
if keyboard_input.just_pressed(KeyCode::KeyH) {
// 这里需要实现移除病假组件的逻辑
println!("😊 模拟学生康复...");
}
}
// 自动模拟随机事件
fn simulate_random_events(
mut commands: Commands,
time: Res<Time>,
mut timer: Local<Timer>,
student_query: Query<Entity, (With<Student>, Without<SickLeave>)>,
sick_query: Query<Entity, With<SickLeave>>,
) {
timer.tick(time.delta());
if timer.finished() {
*timer = Timer::from_seconds(8.0, TimerMode::Once);
let random_event = rand::random::<f32>();
if random_event < 0.3 {
// 30%概率:学生请病假
if let Some(student_entity) = student_query.iter().next() {
let reasons = ["发烧", "咳嗽", "肚子疼", "头痛"];
let reason = reasons[rand::random::<usize>() % reasons.len()];
commands.entity(student_entity).insert(SickLeave {
reason: reason.to_string(),
days: rand::random::<u8>() % 3 + 1,
});
}
} else if random_event < 0.5 {
// 20%概率:学生康复
if let Some(sick_entity) = sick_query.iter().next() {
commands.entity(sick_entity).remove::<SickLeave>();
}
} else if random_event < 0.6 {
// 10%概率:添加特殊需求学生
if let Some(student_entity) = student_query.iter().next() {
let requirements = ["无障碍通道", "助听设备", "特殊座椅", "个别辅导"];
let requirement = requirements[rand::random::<usize>() % requirements.len()];
commands.entity(student_entity).insert(SpecialNeeds {
requirement: requirement.to_string(),
});
}
}
}
}
📊 状态显示系统
fn display_registry(registry: Res<SchoolRegistry>) {
println!("\n📋 === 学校实时状态 ===");
println!("👨🎓 学生总数:{}", registry.total_students);
println!("👨🏫 教师总数:{}", registry.total_teachers);
if !registry.sick_students.is_empty() {
println!("🤒 病假学生:");
for student in ®istry.sick_students {
println!(" - {}", student);
}
}
if !registry.office_assignments.is_empty() {
println!("🏢 办公室分配:");
for assignment in ®istry.office_assignments {
println!(" - {}", assignment);
}
}
println!("====================\n");
}
fn setup_school(mut commands: Commands) {
// 创建一些初始学生
for i in 1..=3 {
commands.spawn((
Student {
id: i,
class: "三年级".to_string()
},
Name(format!("张同学{}", i)),
));
}
}
fn print_instructions() {
println!("\n🎮 === 观察者系统演示 ===");
println!("按键说明:");
println!(" S - 添加新学生(触发OnAdd<Student>观察者)");
println!(" T - 添加新教师(触发OnAdd<Teacher>观察者)");
println!(" I - 模拟学生请病假(触发OnAdd<SickLeave>观察者)");
println!(" H - 模拟学生康复(触发OnRemove<SickLeave>观察者)");
println!("自动事件:每8秒随机发生病假/康复/特殊需求事件");
println!("观察即时响应的效果!\n");
}
⚠️ 观察者的陷阱:无限递归
这是观察者系统最危险的陷阱:
// ❌ 危险!会导致无限递归和栈溢出
fn dangerous_observer(
trigger: Trigger<OnAdd<SomeComponent>>,
mut commands: Commands,
) {
let entity = trigger.target();
// 这会再次触发OnAdd<SomeComponent>,形成无限循环!
commands.entity(entity).insert(SomeComponent);
}
// ✅ 正确的方式:检查条件或使用不同的组件
fn safe_observer(
trigger: Trigger<OnAdd<Student>>,
mut commands: Commands,
query: Query<&Student, Without<StudentId>>,
) {
let entity = trigger.target();
// 只有当学生没有ID时才添加
if query.get(entity).is_ok() {
commands.entity(entity).insert(StudentId(generate_id()));
}
}
#[derive(Component)]
struct StudentId(u32);
fn generate_id() -> u32 {
rand::random()
}
🔄 OnAdd vs OnInsert:重要区别
// OnAdd: 只在组件第一次被添加时触发
fn on_first_add(trigger: Trigger<OnAdd<Health>>) {
println!("健康组件第一次被添加");
}
// OnInsert: 每次插入组件时都触发(包括覆盖)
fn on_every_insert(trigger: Trigger<OnInsert<Health>>) {
println!("健康组件被插入(可能是覆盖)");
}
#[derive(Component)]
struct Health(u32);
// 示例:
// commands.spawn(Health(100)); // 触发 OnAdd 和 OnInsert
// commands.entity(e).insert(Health(90)); // 只触发 OnInsert
🚀 观察者的高级用法
// 使用自定义触发器
#[derive(Event)]
struct EmergencyDrill;
fn trigger_emergency_drill(
mut commands: Commands,
keyboard_input: Res<ButtonInput<KeyCode>>,
) {
if keyboard_input.just_pressed(KeyCode::KeyE) {
// 手动触发观察者
commands.trigger(EmergencyDrill);
}
}
fn handle_emergency_drill(
trigger: Trigger<EmergencyDrill>,
mut next_state: ResMut<NextState<SchoolState>>,
) {
println!("🚨 紧急演练开始!");
next_state.set(SchoolState::Emergency);
}
// 在app中添加观察者
// .add_observer(handle_emergency_drill)
🎯 何时使用观察者?
场景 | 使用观察者 | 使用事件 | 使用变更检测 |
---|---|---|---|
组件添加/移除的响应 | ✅ 最佳选择 | ❌ 过于复杂 | ❌ 可能延迟 |
维护数据一致性 | ✅ 即时保证 | ⚠️ 可能延迟 | ⚠️ 可能延迟 |
高频事件处理 | ⚠️ 性能考虑 | ✅ 批处理优势 | ✅ 批处理优势 |
解耦的通信 | ⚠️ 直接耦合 | ✅ 最佳选择 | ❌ 不适用 |
调试友好性 | ⚠️ 隐式调用 | ✅ 显式发送 | ✅ 显式查询 |
🤔 检查理解
- 观察者与事件的主要区别是什么?
OnAdd
和OnRemove
什么时候被触发?- 为什么观察者可能导致无限递归?
- 什么时候应该使用观察者而不是事件?
💪 练习挑战
- 创建一个
on_teacher_removed
观察者,处理教师离职 - 实现一个复杂的观察者链:学生→班级→年级的级联更新
- 使用自定义触发器实现”火警演练”功能
- 避免无限递归,实现一个”自动升级”系统
⚡ 性能提示
- 观察者在组件变化时立即执行,适合低频但重要的事件
- 避免在观察者中执行耗时操作
- 考虑使用
commands.trigger_targets()
批量触发
下一步
下一章我们将学习系统调度,了解如何精确控制系统的执行顺序,让复杂的游戏逻辑井然有序!