声音和事件

在这个 section 中,我们将工作于添加事件,这些事件将在后续阶段用来添加声音效果。在短语中,我们想在以下情况下播放声音:

  1. 当玩家击打墙或障碍时 - 为了让他们知道不能通过
  2. 当玩家把箱放在正确的位置 - 以表明 "你做得对"
  3. 当玩家把箱放在错误的位置 - 以表示move的错误

实际上播放声音并不是太难,ggez提供了这个功能,但我们目前面临的问题是需要确定何时播放声音。

让我们从box on correct spot来看。我们可能会使用游戏状态系统,并且会循环遍历boxes和spots来检查是否处于这种情况,然后播放声音。但是,这并不是一种好主意,因为我们将每次循环都尝试多次,造成不必要的重复和播放太快。

我们可以尝试在此过程中保持一些状态,但这并不感兴趣。我们的主要问题是,我们无法通过仅检查状态来做到这一点,而必须使用一种有反应性的模型,当发生某件事情时就能让系统作出反应。

我们会使用事件模型。这意味着当一个框架发生变化(如玩家击打墙或移动箱子)时,将引发一个事件。然后,我们可以在另一端接收这个事件,并根据其类型执行相应的操作。这个系统可以复用。

事件实现

让我们从讨论如何实现事件开始。我们不会使用组件或实体(尽管可以),而是使用一种与输入队列非常相似的资源。需要将事件加入队列的代码部分需要访问这个资源,然后我们将有一个系统来处理这些事件并采取相应的操作。

实现什么事件

让我们更详细地讨论一下需要哪些事件:

  1. 玩家击打障碍 - 这可以是事件本身,通过输入系统当玩家试图移动但无法移动时会引发
  2. 箱放在正确或错误的位置 - 我们可以将其表示为一个单独的事件,其中包含是否 correct_spot 的属性(我稍后再解释这个属性)

变化类型

我们需要用enum 来定义各种事件类型。我们曾使用过enum(例如Rendering类型和box颜色),但是这次我们要把它的潜力全推到使用,特别是我们可以在其中添加属性。

查看事件定义,它可能是这样的。

#![allow(unused)] fn main() { // events.rs use hecs::Entity; #[derive(Debug)] pub struct EntityMoved { pub entity: Entity, } #[allow(dead_code)] #[derive(Debug)] pub struct BoxPlacedOnSpot { pub is_correct_spot: bool, } #[derive(Debug)] pub enum Event { // Fired when the player hits an obstacle like a wall PlayerHitObstacle, // Fired when an entity is moved EntityMoved(EntityMoved), // Fired when the box is placed on a spot BoxPlacedOnSpot(BoxPlacedOnSpot), } }

事件队列资源

现在,我们需要一个resource来接收事件。这将是一个多生产者单消费者模型。我们会有多个系统添加事件,而一个system(events system)会只消费该事件。

#![allow(unused)] fn main() { // components.rs #[derive(Default)] pub struct EventQueue { pub events: Vec<Event>, } }

发送事件

现在,我们需要将两个事件在input_system中添加:EntityMoved和PlayerHitObstacle。

#![allow(unused)] fn main() { // input.rs pub fn run_input(world: &World, context: &mut Context) { let mut to_move: Vec<(Entity, KeyCode)> = Vec::new(); let mut events = Vec::new(); /// Code omitted /// ...... /// ...... // find a movable // if it exists, we try to move it and continue // if it doesn't exist, we continue and try to find an immovable instead match mov.get(&pos) { Some(entity) => to_move.push((*entity, key)), None => { // find an immovable // if it exists, we need to stop and not move anything // if it doesn't exist, we stop because we found a gap match immov.get(&pos) { Some(_id) => { to_move.clear(); events.push(Event::PlayerHitObstacle {}); break; } None => break, } } } /// Code omitted /// ...... /// ...... /// // Now actually move what needs to be moved for (entity, key) in to_move { let mut position = world.get::<&mut Position>(entity).unwrap(); match key { KeyCode::Up => position.y -= 1, KeyCode::Down => position.y += 1, KeyCode::Left => position.x -= 1, KeyCode::Right => position.x += 1, _ => (), } // Fire an event for the entity that just moved events.push(Event::EntityMoved(EntityMoved { entity })); } }

为了便于阅读,我省略了原始文件中的部分代码,但实际上我们只是在正确的位置添加了两行代码来创建事件并将它们添加到 events 向量中。

最后,我们需要将事件添加回世界中,这一步在系统的末尾完成。

#![allow(unused)] fn main() { // input.rs pub fn run_input(world: &World, context: &mut Context) { let mut to_move: Vec<(Entity, KeyCode)> = Vec::new(); let mut events = Vec::new(); /// Code omitted /// ...... /// ...... // Finally add events back into the world { let mut query = world.query::<&mut EventQueue>(); let event_queue = query.iter().next().unwrap().1; event_queue.events.append(&mut events); } } }

消费事件 - events系统

现在它是时候添加一个events system来处理事件。

我们将会对每个事件做出以下决策:

  • Event::PlayerHitObstacle -> 这是播放音效的地方,但我们会在添加音频部分时再回到这里。

  • Event::EntityMoved(EntityMoved { id }) -> 这是添加逻辑的地方,用于检查刚刚移动的实体是否是一个箱子,以及它是否在正确的位置上。

  • Event::BoxPlacedOnSpot(BoxPlacedOnSpot { is_correct_spot }) -> 这是播放音效的地方,但我们会在添加音频部分时再回到这里。

#![allow(unused)] fn main() { // systems/events.rs use crate::components::*; use crate::events::*; use hecs::World; use std::collections::HashMap; pub fn run_process_events(world: &mut World) { let events = { let mut query = world.query::<&mut crate::components::EventQueue>(); let events = query .iter() .next() .unwrap() .1 .events .drain(..) .collect::<Vec<_>>(); events }; let mut new_events = Vec::new(); let mut query = world.query::<(&Position, &BoxSpot)>(); let box_spots_by_position: HashMap<(u8, u8), &BoxSpot> = query .iter() .map(|(_, t)| ((t.0.x, t.0.y), t.1)) .collect::<HashMap<_, _>>(); for event in events { println!("New event: {:?}", event); match event { Event::PlayerHitObstacle => { // play sound here } Event::EntityMoved(EntityMoved { entity }) => { // An entity was just moved, check if it was a box and fire // more events if it's been moved on a spot. if let Ok(the_box) = world.get::<&Box>(entity) { if let Ok(box_position) = world.get::<&Position>(entity) { // Check if there is a spot on this position, and if there // is if it's the correct or incorrect type if let Some(box_spot) = box_spots_by_position.get(&(box_position.x, box_position.y)) { new_events.push(Event::BoxPlacedOnSpot(BoxPlacedOnSpot { is_correct_spot: (box_spot.colour == the_box.colour), })); } } } } Event::BoxPlacedOnSpot(BoxPlacedOnSpot { is_correct_spot: _ }) => { // play sound here } } } // Finally add events back into the world { let mut query = world.query::<&mut EventQueue>(); let event_queue = query.iter().next().unwrap().1; event_queue.events.append(&mut new_events); } } }

事件处理系统的结尾很重要,因为处理一个事件可能会导致另一个事件被创建。因此,我们必须将事件添加回世界。

代码链接:您可以在这个示例中看到完整代码 这里.