声音和事件

本节我们将给游戏添加声效.简单说就是在某些场景下播放声音:

  1. 当角色碰到墙或障碍物时播放声音,让玩家知道不能穿越过去.
  2. 当角色把盒子推到了正确的地方时播放声音,让玩家知道"这么干就对了"
  3. 当角色把盒子推到了不正确的地方时播放声音,让玩家知道"这地儿不对"

根据ggez提供的功能实现播放声音并不难,我们需要解决的最大问题是确定在何时播放声音.

以把盒子推到了正确的地方为例.我们很可能不断的检测游戏状态系统中保存的盒子和目标点的信息,如果匹配上了就播放声效.但这样有个问题,我们的检测会每秒种执行多次,也就是会播放声效多次.但我们只想播放一次.当然也可以通过维护一些状态信息做到只播放一次,但我们并不想这么做.我们不想通过循环检测状态信息,而是使用响应式模型解决这个问题.也就是当某个动作发生时,可以发送一个时间,然后在其它地方就可以监听这个时间并做出响应.比如当玩家把盒子推到正确的地方时触发一个事件,在其它地方监听到这个事件了就播放相应的音效.而且这个事件系统还能用于解决其它问题.

实现事件系统

接下来看下怎么实现事件系统.我们不使用组件也不使用实体(虽然也可以用),像实现输入队列一样使用资源.我们需要编写把事件放入到队列中和从资源中获取事件的代码,另外我还需要编写根据事件类型执行相应操作的代码.

事件

接下来我们看下需要什么样的事件:

  1. 角色碰到障碍物事件- 这个事件是现成的可以在想移动却移动不成时在输入系统中触发这个事件
  2. 把盒子推到正确/不正确的地方事件 - 我们可以用一个事件表示这俩种情况,只需要用一个属性区分就好.深入点说就是我们可以做个实体移动事件,当我们接受到实体移动事件时获取移动实体的ID,并判断这个移动的实体是否是盒子,如果是就判断它是不是移动到了正确的地方 (这也是创建事件链的示例-根据一个事件生成另一个事件)

事件类型

接下来就开始考虑怎么实现事件了.我们使用enum定义多种事件类型.先前我们已经使用过枚举类型了,像渲染类型,盒子颜色.但这次我们将用到Rust枚举更高级的功能.枚举一个最有趣的功能是每种枚举都可以携带相应属性.

上代码:


#![allow(unused)]
fn main() {
// events.rs
#[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),
}
}

注意看第二个 EntityMoved 和第二个 BoxPlacedOnSpot. 这些就是我们定义用来携带属性的结构体.代码是这样的:


#![allow(unused)]
fn main() {
// events.rs
pub type EntityId = u32;

#[derive(Debug)]
pub struct EntityMoved {
    pub id: EntityId,
}

#[derive(Debug)]
pub struct BoxPlacedOnSpot {
    pub is_correct_spot: bool,
}
}

Event queue resource

现在我们可以编写事件队列资源了.可以有很多系统往这个队列里发送数据,但只有一个系统(事件系统)从队列里消费数据.这是一个典型的多生产者单消费者模式.


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

跟原来一样,还是要注册下资源的:


#![allow(unused)]
fn main() {
// resources.rs
pub fn register_resources(world: &mut World) {
    world.insert(InputQueue::default());
    world.insert(Gameplay::default());
    world.insert(Time::default());
    world.insert(EventQueue::default());
}
}

事件发送

现在我们已经有了往队列里放事件的方法.接下来我们就创建俩个在input_system中用到的事件:EntityMoved和 PlayerHitObstacle.


#![allow(unused)]
fn main() {
// input_system.rs
use crate::components::*;
use crate::constants::*;
use crate::events::{EntityMoved, Event};
use crate::resources::{EventQueue, Gameplay, InputQueue};
use ggez::event::KeyCode;
use specs::{world::Index, Entities, Join, ReadStorage, System, Write, WriteStorage};

use std::collections::HashMap;

pub struct InputSystem {}

// System implementation
impl<'a> System<'a> for InputSystem {
    // Data
    type SystemData = (
        Write<'a, EventQueue>,
        Write<'a, InputQueue>,
        Write<'a, Gameplay>,
        Entities<'a>,
        WriteStorage<'a, Position>,
        ReadStorage<'a, Player>,
        ReadStorage<'a, Movable>,
        ReadStorage<'a, Immovable>,
    );

    fn run(&mut self, data: Self::SystemData) {
        let (
            mut events,
            mut input_queue,
            mut gameplay,
            entities,
            mut positions,
            players,
            movables,
            immovables,
        ) = data;

        let mut to_move = Vec::new();

        for (position, _player) in (&positions, &players).join() {
            // Get the first key pressed
            if let Some(key) = input_queue.keys_pressed.pop() {
                    // ...
                    // ...
                            // 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.events.push(Event::PlayerHitObstacle {})
                                }
                                None => break,
                            }
                        }
                    }
                }
            }
        }

        // We've just moved, so let's increase the number of moves
        if to_move.len() > 0 {
            gameplay.moves_count += 1;
        }

        // Now actually move what needs to be moved
        for (key, id) in to_move {
            let position = positions.get_mut(entities.entity(id));
            if let Some(position) = position {
                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.events.push(Event::EntityMoved(EntityMoved { id }));
        }
    }
}
}

为了读起来方便,这里省略了一些代码.其实我们就是在相应的地方添加了2行代码.

事件消费 - 事件系统

是时候添加处理消费事件的功能了,也就是事件系统.这个功能实现根据接受到的事件执行相应操作的逻辑.

接下来我们看下怎么处理每种类型的事件:

  • Event::PlayerHitObstacle -> 播放相应音效
  • Event::EntityMoved(EntityMoved { id }) -> 检查移动的实体是否是盒子,盒子是放到了正确的目标点还是放到了错误的目标点.
  • Event::BoxPlacedOnSpot(BoxPlacedOnSpot { is_correct_spot }) -> 播放相应音效.

#![allow(unused)]
fn main() {
// event_system.rs
use crate::{
    audio::AudioStore,
    components::*,
    events::{BoxPlacedOnSpot, EntityMoved, Event},
    resources::EventQueue,
};
use specs::{Entities, Join, ReadStorage, System, Write};
use std::collections::HashMap;

pub struct EventSystem<'a> {
    pub context: &'a mut ggez::Context,
}

// System implementation
impl<'a> System<'a> for EventSystem<'a> {
    // Data
    type SystemData = (
        Write<'a, EventQueue>,
        Write<'a, AudioStore>,
        Entities<'a>,
        ReadStorage<'a, Box>,
        ReadStorage<'a, BoxSpot>,
        ReadStorage<'a, Position>,
    );

    fn run(&mut self, data: Self::SystemData) {
        let (mut event_queue, mut audio_store, entities, boxes, box_spots, positions) = data;

        let mut new_events = Vec::new();

        for event in event_queue.events.drain(..) {
            println!("New event: {:?}", event);

            match event {
                    // play sound here
                    audio_store.play_sound(self.context, &"wall".to_string());
                }
                Event::EntityMoved(EntityMoved { id }) => {
                    // 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 Some(the_box) = boxes.get(entities.entity(id)) {
                        let box_spots_with_positions: HashMap<(u8, u8), &BoxSpot> =
                            (&box_spots, &positions)
                                .join()
                                .map(|t| ((t.1.x, t.1.y), t.0))
                                .collect::<HashMap<_, _>>();

                        if let Some(box_position) = positions.get(entities.entity(id)) {
                            // 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_with_positions.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
                }
            }
        }

        event_queue.events.append(&mut new_events);
    }
}

}

Audio assets

现在已经添加好了事件,接下来我们开始添加声音素材.我们从这个 素材包里选了3个声音素材,你也可以使用自己的素材.

盒子放到正确地方时播放 这个

盒子放到不正确地方时播放这个

角色碰到障碍物时播放这个

把这些声音素材添加到resources文件夹下的sounds文件夹下:

.
├── resources
│   ├── images
│   │   ├── box_blue_1.png
│   │   ├── box_blue_2.png
│   │   ├── box_red_1.png
│   │   ├── box_red_2.png
│   │   ├── box_spot_blue.png
│   │   ├── box_spot_red.png
│   │   ├── floor.png
│   │   ├── player_1.png
│   │   ├── player_2.png
│   │   ├── player_3.png
│   │   └── wall.png
│   └── sounds
│       ├── correct.wav
│       ├── incorrect.wav
│       └── wall.wav
├── Cargo.lock
└── Cargo.toml

声音仓库

现在为了播放声音我们需要加载一些wav文件.为了避免每次播放声音都重新加载一次,我们需要创建一个声音仓库,在游戏开始时就把所有的声音文件加载好.

我们使用一个资源做为声音仓库:


#![allow(unused)]
fn main() {
// audio.rs
#[derive(Default)]
pub struct AudioStore {
    pub sounds: HashMap<String, audio::Source>,
}
}

像往常一样注册下资源:


#![allow(unused)]
fn main() {
// resources.rs
pub fn register_resources(world: &mut World) {
    world.insert(InputQueue::default());
    world.insert(Gameplay::default());
    world.insert(Time::default());
    world.insert(EventQueue::default());
    world.insert(AudioStore::default());
}
}

接下来添加初始化仓库的代码:


#![allow(unused)]
fn main() {
// audio.rs
pub fn initialize_sounds(world: &mut World, context: &mut Context) {
    let mut audio_store = world.write_resource::<AudioStore>();
    let sounds = ["correct", "incorrect", "wall"];

    for sound in sounds.iter() {
        let sound_name = sound.to_string();
        let sound_path = format!("/sounds/{}.wav", sound_name);
        let sound_source = audio::Source::new(context, sound_path).expect("expected sound loaded");

        audio_store.sounds.insert(sound_name, sound_source);
    }
}
}

播放音效

最后是在仓库中添加播放音效的功能:


#![allow(unused)]
fn main() {
// audio.rs
impl AudioStore {
    pub fn play_sound(&mut self, context: &mut Context, sound: &String) {
        let _ = self
            .sounds
            .get_mut(sound)
            .expect("expected sound")
            .play_detached(context);
    }
}
}

然后在声音系统中执行播放音效操作:


#![allow(unused)]
fn main() {
    // event_system.rs
    );

    fn run(&mut self, data: Self::SystemData) {
        let (mut event_queue, mut audio_store, entities, boxes, box_spots, positions) = data;

        let mut new_events = Vec::new();

        for event in event_queue.events.drain(..) {
            println!("New event: {:?}", event);

            match event {
                Event::PlayerHitObstacle => {
                    // play sound here
                    audio_store.play_sound(self.context, &"wall".to_string());
                        // ...
                }
                Event::BoxPlacedOnSpot(BoxPlacedOnSpot { is_correct_spot }) => {
                    // play sound here
                    let sound = if is_correct_spot {
                        "correct"
                    } else {
                        "incorrect"
                    };

                    audio_store.play_sound(self.context, &sound.to_string())
                }
            }
        }
}

现在让我们运行程序,感受下声音效果吧!

CODELINK:这里获取示例完整代码.