Звуки и события

В этой главе мы добавим звуковые эффекты. Если кратко — мы хотим проигрывать звуки при следующих условиях:

  1. Когда игрок ударяется о стену или препятствие — чтобы дать понять, что невозможно пройти.
  2. Когда игрок помещает коробку на правильное место — чтобы сказать "ты всё правильно сделал".
  3. Если игрок поставил коробку не на своё место — чтобы оповестить, что ход был неправильным.

На самом деле проигрывание аудио не является сильно сложной штукой — ggez позволяет работать с ним. На данном этапе большей проблемой будет правильное определение того, когда проигрывать звуки.

Давайте разберём на примере, когда коробка на правильном месте. Мы можем использовать нашу игровую систему состояний, чтобы пройтись по местам и коробкам — и проиграть звук, когда коробка находится там, где нужно. Но это не сработает, так как мы будем проходить по местам и коробкам много раз в секунду — и останемся в этом состоянии до тех пор, пока коробка не сдвинется. Из-за этого мы будем пытаться проиграть аудио несколько раз в секунду, а это явно не то, чего мы хотим. Мы можем попытаться сохранить какое-то состояние, чтобы знать, что мы проигрываем сейчас, но это неверный подход. Проблема в том, что мы просто не сможем сделать это обычной последовательной проверкой. Вместо этого нам нужна реактивная модель, которая позволит нам вовремя узнавать, что что-то только что произошло и нам необходимо отреагировать. То, что я здесь описала, — это модель событий: нам нужно запустить событие, когда коробка будет на нужном месте, и затем, когда мы получим это событие, проиграть аудио со своей стороны. Главное преимущество такого подхода в том, что мы можем переиспользовать систему событий для разных целей.

Инфраструктура событий: Как

Давайте поговорим о том, как мы можем реализовать события. Мы не будем использовать компоненты или сущности (хотя и могли бы). Вместо этого мы используем ресурс, очень похожий на входную очередь. Часть кода, необходимая для добавления событий в очередь, должна иметь доступ к этому ресурсу — и тогда мы получим систему, обрабатывающую события и выполняющую соответствующие действия.

Что является событием

Давайте более детально разберём, какие события нам необходимы:

  1. Игрок встретил препятствие. Это может быть событие самого игрока, которое создаётся из системы ввода, когда мы пытаемся переместиться, но не можем этого сделать.
  2. Коробка установлена на правильное или неправильное место. Мы можем представить это как одно событие с внутренним свойством, которое сообщит, корректна ли комбинация коробки и места. Если мы задумаемся глубже о том, как это сделать, у нас появится идея о событии перемещения сущности. Когда мы получим это событие, мы сможем проверить идентификатор сущности, которая только что сдвинулась, чтобы понять, коробка ли это и на каком месте она стоит: правильном, неправильном или любом другом. Это пример создания цепочки событий — событие из события.

Типы событий

Давайте перейдём к реализации событий. Мы будем использовать перечисление для определения различных типов событий. Мы уже использовали перечисления ранее (для указания типа отрисовки и цвета коробки), но сейчас мы используем всю силу перечислений 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,
}
}

Ресурс очереди событий

Теперь добавим ресурс для очереди событий. У нас будут разные системы записи в эту очередь и одна система (система событий) для её потребления. По сути, это модель с несколькими производителями и одним потребителем.


#![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());
}
}

Отправка событий

Теперь, когда у нас есть способ постановки событий в очередь, давайте добавим два события в систему ввода: 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 }));
        }
    }
}
}

Для читаемости я опустила часть кода, но на самом деле мы просто добавили две строки в нужное место.

Потребление событий

Пришло время добавить способ потребления событий, который и станет системой событий. Эта система будет содержать логику того, что должно произойти при получении события.

Поговорим о том, как мы будем их обрабатывать:

  • 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);
    }
}
}

Аудиоресурсы

Теперь, когда у нас есть события, добавим звуковые ресурсы. Я взяла 3 аудио из этого набора, но вы можете выбрать свои.

Звук корректной постановки коробки

Звук некорректной постановки

Звук стены

Добавим эти аудио в новую поддиректорию директории resources.

.
├── 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())
                }
            }
        }
}

Теперь давайте запустим игру и насладимся звуковыми эффектами!

КОД: Увидеть весь код из данной главы можно здесь.