前言在入门篇中,我们用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) 表示正在执行某个动作,由定时器控制持续时间Idleimpl 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;
}
}
"彩蛋"是增加游戏趣味性的经典手段。我们设计了一个三连击彩蛋:
// 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;
}
// 正常点击切换菜单
// ...
}
一个简单的反应力测试游戏:
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);
}
}
使用 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; // 提示文字
// 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 | 全局状态 | 单例数据,系统共享 |
精灵图加载后会被 GPU 缓存,无需额外优化。
使用 With<T> 过滤器减少不必要的组件访问:
// 只查询有 DanceButton 组件的实体
Query<&Interaction, With<DanceButton>>
使用 run_if 条件执行系统:
.add_systems(
Update,
update_reaction_game.run_if(resource_changed::<ReactionGame>),
)
通过深入篇,我们学习了:
这些模式不仅适用于桌面宠物,也是游戏开发的通用技巧。希望本文能帮助你构建更有趣的交互应用!
🐰 新春快乐!愿这只小机器人陪伴你的编程之旅!
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!