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

  • King
  • 发布于 6小时前
  • 阅读 31

前言在入门篇中,我们用Rust+Bevy构建了一个具备基础状态的桌面宠物。今天,让我们深入探讨如何打造一个真正"好玩"的交互体验!本文将深入剖析以下核心特性:状态机驱动的动画系统-让宠物行为更自然连击检测与隐藏彩蛋-增加发现的乐趣反应速度小游戏-互动游戏化体验

前言

在入门篇中,我们用 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 组件模式

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

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

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
King
King
0x56af...a0dd
擅长Rust/Solidity/FunC/Move开发