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

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

  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
use hecs::Entity;

#[derive(Debug)]
pub struct EntityMoved {
    pub entity: Entity,
}

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

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

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


#![allow(unused)]
fn main() {
// resources.rs
{{#include ../../../code/rust-sokoban-c03-03/src/resources.rs:54:57}}
}

И — как и всегда — зарегистрируем этот ресурс.


#![allow(unused)]
fn main() {
// resources.rs
{{#include ../../../code/rust-sokoban-c03-03/src/resources.rs:14:18}}
{{#include ../../../code/rust-sokoban-c03-03/src/resources.rs:20}}
}

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

Теперь, когда у нас есть способ постановки событий в очередь, давайте добавим два события в систему ввода: EntityMoved и PlayerHitObstacle.


#![allow(unused)]
fn main() {
// input_system.rs
{{#include ../../../code/rust-sokoban-c03-03/src/systems/input_system.rs:1:42}}
                    // ...
                    // ...
{{#include ../../../code/rust-sokoban-c03-03/src/systems/input_system.rs:83:124}}
}

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

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

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

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

  • Event::PlayerHitObstacle -> это то место, где будет воспроизводиться звук. Мы вернёмся сюда когда будем добавлять аудио.
  • Event::EntityMoved(EntityMoved { id }) -> здесь мы напишем логику проверки того, является ли передвигаемая сущность коробкой и помещена ли она на своё место.
  • Event::BoxPlacedOnSpot(BoxPlacedOnSpot { is_correct_spot }) -> здесь тоже будет воспроизводиться звук, и мы также вернёмся сюда позже.

#![allow(unused)]
fn main() {
// event_system.rs
{{#include ../../../code/rust-sokoban-c03-03/src/systems/event_system.rs:1:34}}
{{#include ../../../code/rust-sokoban-c03-03/src/systems/event_system.rs:36:63}}
{{#include ../../../code/rust-sokoban-c03-03/src/systems/event_system.rs:71:78}}
}

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

Теперь, когда у нас есть события, добавим звуковые ресурсы. Я взяла 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
{{#include ../../../code/rust-sokoban-c03-03/src/audio.rs:6:9}}
}

Зарегистрируем его.


#![allow(unused)]
fn main() {
// resources.rs
{{#include ../../../code/rust-sokoban-c03-03/src/resources.rs:14:20}}
}

И добавим код инициализации.


#![allow(unused)]
fn main() {
// audio.rs
{{#include ../../../code/rust-sokoban-c03-03/src/audio.rs:21:32}}
}

Проигрывание аудио

Наконец, добавим возможность проигрывания аудио из хранилища.


#![allow(unused)]
fn main() {
// audio.rs
{{#include ../../../code/rust-sokoban-c03-03/src/audio.rs:11:19}}
}

И проиграем их с помощью системы событий.


#![allow(unused)]
fn main() {
    // event_system.rs
{{#include ../../../code/rust-sokoban-c03-03/src/systems/event_system.rs:24:37}}
                        // ...
{{#include ../../../code/rust-sokoban-c03-03/src/systems/event_system.rs:61:73}}
}

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

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