找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

1544

积分

0

好友

200

主题
发表于 6 天前 | 查看: 24| 回复: 0

像素风格机器人桌面宠物,显示心形、闪电、电池三种状态值

在上一篇入门教程中,我们使用 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),                 // 通知消息
}

事件注册与处理

在 Bevy 的 App 中注册事件类型,并添加处理这些事件的系统。任何系统都可以发送事件,而专门的事件处理系统会集中响应并更新游戏状态。

// 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 组件设计模式

组件标记

在复杂的 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 搭建

创建 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| {
            // 添加按钮...
        });
}

查询与更新

在后续的系统里,我们可以使用 With<T> 过滤器进行高效且意图明确的查询。

// 查询特定按钮的交互状态
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>Without<T> 过滤器减少查询时遍历的组件数量,这是 Rust 游戏开发中常见的优化点。

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

3. 条件系统

使用 run_if 让系统只在条件满足时运行,避免不必要的计算。

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

结语

通过这篇进阶教程,我们不仅为桌面宠物添加了丰富的互动功能,更重要的是实践了一系列在 开源实战 和游戏开发中通用的核心设计模式:

  • 如何使用状态机管理复杂的行为逻辑。
  • 如何设计彩蛋和成就系统来提升用户粘性。
  • 如何实现一个状态清晰的小游戏。
  • 如何利用事件驱动架构解耦系统,提升代码可维护性。
  • 如何使用标记组件来高效管理 UI。

这些思想和模式完全可以迁移到更复杂的项目中去。希望这个用 Rust 和 Bevy 构建的桌面宠物项目,能为你自己的创意应用或游戏开发提供一些有趣的灵感。欢迎在 云栈社区 分享你的实现和想法!

🐰 愿这只小机器人陪伴你的编程之旅,带来更多创造乐趣!




上一篇:硬件工程师的日常:从PCB布局到热设计,为何总是“背锅侠”?
下一篇:底层逻辑构建方法论:掌握动态立体思维模型,提升认知与问题解决能力
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-2-23 07:33 , Processed in 0.505094 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表