返回文章列表

Rust 深入篇:桌面宠物助手的进阶玩法与架构设计

599 字·5 分钟阅读
RustBevy游戏开发

前言

在入门篇中,我们用 Rust + Bevy 构建了一个具备基础状态的桌面宠物。

今天,让我们深入探讨如何打造一个真正"好玩"的交互体验!

本文将深入剖析以下核心特性:

  • 状态机驱动的动画系统 - 让宠物行为更自然
  • 连击检测与隐藏彩蛋 - 增加发现的乐趣
  • 反应速度小游戏 - 互动游戏化体验
  • 成就解锁系统 - 激励持续互动
  • 事件驱动架构 - 解耦的系统设计

状态机驱动的动画系统

为什么需要状态机?

入门篇中的动画切换是"被动"的 —— 仅根据饥饿、快乐、能量值来决定。

但实际游戏中,用户可以主动触发动画(如跳舞、睡觉、喂食),这就需要一个 有限状态机(FSM) 来管理。

状态机设计

// src/animation/types.rs
 
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum UserAction {
    Dance, // 跳舞 - 行1 (帧 4-7)
    Sleep, // 睡觉 - 行2 (帧 8-11)
    Talk,  // 聊天 - 行3 (帧 12-15)
    Feed,  // 喂食 - 行4 (帧 16-19)
    Pet,   // 抚摸 - 行5 (帧 20-23)
}
 
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum ActionState {
    #[default]
    Idle,                   // 空闲状态
    Acting(UserAction),     // 正在执行某个动作
}
 
#[derive(Resource)]
pub struct ActionStateMachine {
    pub state: ActionState,
    timer: Timer,           // 动作持续时间
}

核心思想:

  • Idle 是默认状态,宠物处于待机动画
  • Acting(action) 表示正在执行某个动作,由定时器控制持续时间
  • 动作结束后自动返回 Idle

状态转移实现

impl ActionStateMachine {
    /// 触发一个新动作
    pub fn trigger(&mut self, action: UserAction) {
        self.state = ActionState::Acting(action);
        self.timer.reset();  // 重置计时器
    }
 
    /// 检查是否处于空闲状态
    pub fn is_idle(&self) -> bool {
        matches!(self.state, ActionState::Idle)
    }
}

动画帧映射

精灵图采用 4列×6行 的网格布局,每行对应一个动作:

pub fn update_action_animation(
    mut query: Query<&mut AnimationIndices>,
    state_machine: Res<ActionStateMachine>,
) {
    for mut indices in &mut query {
        let (first, last) = match state_machine.state {
            ActionState::Idle => (0, 3),
            ActionState::Acting(UserAction::Dance) => (4, 7),
            ActionState::Acting(UserAction::Sleep) => (8, 11),
            ActionState::Acting(UserAction::Talk) => (12, 15),
            ActionState::Acting(UserAction::Feed) => (16, 19),
            ActionState::Acting(UserAction::Pet) => (20, 23),
        };
        indices.first = first;
        indices.last = last;
    }
}

动作效果应用

动作执行期间,实时更新宠物状态:

pub fn tick_action_state_machine(
    time: Res<Time>,
    mut state_machine: ResMut<ActionStateMachine>,
    mut pet_query: Query<&mut Pet>,
) {
    let Some(action) = state_machine.current_action() else {
        return;
    };
 
    state_machine.timer.tick(time.delta());
 
    for mut pet in &mut pet_query {
        match action {
            UserAction::Dance => {
                pet.happiness = (pet.happiness + 15.0 * time.delta_secs()).min(100.0);
                pet.energy = (pet.energy - 5.0 * time.delta_secs()).max(0.0);
            }
            UserAction::Sleep => {
                pet.energy = (pet.energy + 25.0 * time.delta_secs()).min(100.0);
            }
            UserAction::Feed => {
                pet.hunger = (pet.hunger + 30.0 * time.delta_secs()).min(100.0);
            }
            // ... 其他动作
        }
    }
 
    // 动作结束,返回空闲
    if state_machine.timer.just_finished() {
        state_machine.state = ActionState::Idle;
    }
}

连击检测与隐藏彩蛋

设计思路

"彩蛋"是增加游戏趣味性的经典手段。我们设计了一个三连击彩蛋:

  • 用户在 0.65 秒内连续点击宠物 3 次
  • 触发隐藏的"跳舞"动画
  • 显示特殊提示信息

连击追踪器

// src/fun/state.rs
 
#[derive(Resource, Default)]
pub struct ComboTracker {
    streak: u8,              // 当前连击数
    last_click_secs: Option<f64>,  // 上次点击时间
}
 
impl ComboTracker {
    const COMBO_WINDOW_SECS: f64 = 0.65;  // 连击时间窗口
 
    pub fn register_click(&mut self, now_secs: f64) -> bool {
        // 检查是否在时间窗口内
        let within_window = self
            .last_click_secs
            .is_some_and(|last| now_secs - last <= Self::COMBO_WINDOW_SECS);
 
        if within_window {
            self.streak = self.streak.saturating_add(1);
        } else {
            self.streak = 1;  // 重置连击
        }
 
        self.last_click_secs = Some(now_secs);
 
        // 达成三连击
        if self.streak >= 3 {
            self.streak = 0;
            self.last_click_secs = None;
            return true;  // 触发彩蛋!
        }
        false
    }
}

触发彩蛋

// src/ui/menu.rs
 
pub fn handle_pet_click(
    // ...
    mut combo_tracker: ResMut<ComboTracker>,
    mut fun_events: MessageWriter<FunEvent>,
) {
    // ... 点击检测逻辑
 
    // 检测连击
    if combo_tracker.register_click(time.elapsed_secs_f64()) {
        action_state_machine.trigger(UserAction::Dance);  // 触发跳舞
        *menu_state = MenuState::Hidden;
        fun_events.write(FunEvent::ComboTriggered);  // 发送事件
        return;
    }
 
    // 正常点击切换菜单
    // ...
}

反应速度小游戏

游戏设计

一个简单的反应力测试游戏:

  1. 按 R 键 开始游戏
  2. 等待随机时间(0.8~2.0秒)
  3. 出现 "GO!" 提示后,尽快按 空格键
  4. 显示反应时间,记录最佳成绩

状态流转

Idle → Waiting → Go → Cooldown → Idle
         ↓         ↓
      (过早按键)  (超时)
         ↓         ↓
      Cooldown → Idle

状态定义

pub(crate) enum ReactionState {
    Idle,                                    // 等待开始
    Waiting { timer: Timer },               // 等待 GO
    Go { elapsed_ms: f32, timeout: Timer }, // 等待玩家反应
    Cooldown { timer: Timer },              // 冷却期
}

核心逻辑

// 开始游戏
fn try_start_reaction_game(
    keys: &ButtonInput<KeyCode>,
    time: &Time,
    reaction_game: &mut ReactionGame,
    action_state_machine: &ActionStateMachine,
    fun_events: &mut MessageWriter<FunEvent>,
) {
    if !keys.just_pressed(KeyCode::KeyR)
        || reaction_game.is_active()
        || !action_state_machine.is_idle()
    {
        return;
    }
 
    // 随机延迟(使用正弦函数增加随机性)
    let delay = 0.8 + (time.elapsed_secs() * 3.7).sin().abs() * 1.2;
    reaction_game.state = ReactionState::Waiting {
        timer: Timer::from_seconds(delay, TimerMode::Once),
    };
    fun_events.write(FunEvent::Notify("Reaction game: wait for GO...".into()));
}
 
// 处理 GO 状态
fn evaluate_go_state(
    elapsed_ms: &mut f32,
    timeout: &mut Timer,
    keys: &ButtonInput<KeyCode>,
    time: &Time,
    action_state_machine: &mut ActionStateMachine,
    fun_events: &mut MessageWriter<FunEvent>,
) -> Option<ReactionState> {
    // 玩家按下空格
    if pressed_space(keys) {
        let reaction_ms = finalize_reaction_ms(*elapsed_ms);
        action_state_machine.trigger(UserAction::Dance);  // 胜利舞蹈!
        emit_reaction_success(fun_events, reaction_ms);
        return Some(cooldown_state(1.5));
    }
 
    // 计时
    *elapsed_ms += time.delta_secs() * 1000.0;
 
    // 超时
    if timeout.tick(time.delta()).just_finished() {
        emit_reaction_failure(fun_events, "Missed! Press R to retry");
        return Some(cooldown_state(1.2));
    }
 
    None
}

成就解锁系统

成就定义

#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) enum Achievement {
    FirstFeed,     // 首次喂食
    PetLover,      // 抚摸 10 次
    ComboStarter,  // 触发连击彩蛋
    ReflexAce,     // 反应时间 ≤ 350ms
}

进度追踪

#[derive(Resource, Default)]
pub struct FunProgress {
    feed_count: u32,
    pet_count: u32,
    combo_count: u32,
    reaction_runs: u32,
    best_reaction_ms: Option<u32>,
    unlocked: HashSet<Achievement>,  // 已解锁的成就
}

解锁检测

fn update_achievements(progress: &mut FunProgress, hud: &mut FunHudMessage) {
    try_unlock(progress, hud, Achievement::FirstFeed, progress.feed_count >= 1);
    try_unlock(progress, hud, Achievement::PetLover, progress.pet_count >= 10);
    try_unlock(progress, hud, Achievement::ComboStarter, progress.combo_count >= 1);
    try_unlock(progress, hud, Achievement::ReflexAce, 
        progress.best_reaction_ms.is_some_and(|ms| ms <= 350));
}
 
fn try_unlock(
    progress: &mut FunProgress,
    hud: &mut FunHudMessage,
    achievement: Achievement,
    condition: bool,
) {
    // 成就未解锁且满足条件
    if condition && progress.unlocked.insert(achievement) {
        show_hud(hud, achievement.title(), 2.6);
    }
}

事件驱动架构

为什么使用事件?

当系统之间需要通信时,直接耦合会导致代码难以维护。事件驱动架构让系统松耦合:

用户点击 → UI系统 → 发送事件 → 游戏系统响应
                        ↓
                   成就系统检测

事件定义

// src/fun/events.rs
 
#[derive(Message, Debug, Clone)]
pub enum FunEvent {
    ActionTriggered(UserAction),    // 动作被触发
    ComboTriggered,                  // 连击彩蛋触发
    ReactionFinished {               // 反应游戏结束
        success: bool,
        reaction_ms: Option<u32>,
    },
    Notify(String),                  // 通知消息
}

事件注册与处理

// main.rs
fn main() {
    App::new()
        .add_message::<FunEvent>()  // 注册事件类型
        .add_systems(Update, process_fun_events)
        .run();
}
 
// 处理所有事件
pub fn process_fun_events(
    mut events: MessageReader<FunEvent>,
    mut progress: ResMut<FunProgress>,
    mut hud: ResMut<FunHudMessage>,
) {
    for event in events.read() {
        handle_fun_event(event, &mut progress, &mut hud);
        update_achievements(&mut progress, &mut hud);
    }
}

UI 组件设计模式

组件标记

使用 Marker Component 模式,让系统能精准查询特定 UI 元素:

// src/ui/components.rs
 
#[derive(Component)]
pub struct ActionMenuContainer;  // 菜单容器
 
#[derive(Component)]
pub struct DanceButton;          // 跳舞按钮
 
#[derive(Component)]
pub struct StatusValueHeart;     // 心情数值
 
#[derive(Component)]
pub struct FunToastText;         // 提示文字

UI 搭建

// src/ui/setup.rs
 
fn spawn_action_menu(commands: &mut Commands, asset_server: &AssetServer) {
    commands
        .spawn(Node { /* 布局配置 */ })
        .insert(BackgroundColor(Color::srgba(0.1, 0.12, 0.2, 0.95)))
        .insert(ActionMenuContainer)   // 标记组件
        .insert(Visibility::Hidden)    // 默认隐藏
        .with_children(|menu| {
            // 添加按钮...
        });
}

查询与更新

// 查询特定按钮的交互状态
pub fn handle_menu_actions(
    dance_query: Query<&Interaction, With<DanceButton>>,
    sleep_query: Query<&Interaction, With<SleepButton>>,
    // ...
) {
    if dance_query.iter().any(|i| *i == Interaction::Pressed) {
        // 处理跳舞按钮点击
    }
}
 
// 更新状态显示
pub fn update_status_display(
    pet_query: Query<&Pet>,
    mut heart_text: Query<&mut Text, With<StatusValueHeart>>,
) {
    for pet in &pet_query {
        for mut text in &mut heart_text {
            **text = format!("{:.0}", pet.happiness);
        }
    }
}

项目架构总结

src/
├── main.rs              # 应用入口,系统集成
├── pet.rs               # 宠物状态组件
├── window.rs            # 窗口配置
├── animation/           # 动画模块
│   ├── mod.rs
│   ├── types.rs         # 状态机、动作类型
│   └── systems.rs       # 动画更新、状态转移
├── fun/                 # 游戏玩法模块
│   ├── mod.rs
│   ├── events.rs        # 事件定义
│   ├── state.rs         # 连击、反应游戏、成就
│   └── systems.rs       # 游戏逻辑处理
└── ui/                  # 界面模块
    ├── mod.rs
    ├── components.rs    # UI 组件标记
    ├── resources.rs     # 资源定义
    ├── setup.rs         # UI 初始化
    ├── menu.rs          # 菜单交互
    └── status.rs        # 状态显示

设计亮点:

模式 应用场景 优势
ECS 架构 整体设计 数据与逻辑分离,易于扩展
状态机 动画控制 行为可控,状态明确
事件驱动 系统通信 松耦合,易维护
Marker Component UI 元素 精准查询,代码清晰
Resource 全局状态 单例数据,系统共享

性能优化建议

1. 动画帧缓存

精灵图加载后会被 GPU 缓存,无需额外优化。

2. 减少系统查询

使用 With<T> 过滤器减少不必要的组件访问:

// 只查询有 DanceButton 组件的实体
Query<&Interaction, With<DanceButton>>

3. 条件系统

使用 run_if 条件执行系统:

.add_systems(
    Update,
    update_reaction_game.run_if(resource_changed::<ReactionGame>),
)

结语

通过深入篇,我们学习了:

  • 状态机在游戏开发中的应用
  • 彩蛋与成就系统设计
  • 小游戏的状态流转
  • 事件驱动架构的优势
  • Bevy UI 组件模式

这些模式不仅适用于桌面宠物,也是游戏开发的通用技巧。希望本文能帮助你构建更有趣的交互应用!

🐰 新春快乐!愿这只小机器人陪伴你的编程之旅!