Sounds and events

In this section we will work on adding events which will be later used for adding sound effects. In short, we want to play sounds in these circumstances:

  1. when the player hits a wall or an obstacle - to let them know they cannot get through
  2. when the player places a box on the correct spot - as an indication of "you've done it correctly"
  3. when the player places a box on the incorrect spot - as an indication that the move was wrong

Actually playing audio will not be too difficult as ggez provides this ability, but the biggest issue we have right now is that we need to determine when to play the sounds.

Let's take the box on correct spot example. We could probably use our game state system and loop through the boxes and the spots and determine when we are in this state and play the sound. But that is not going to work because we will be interating many times per second, and we will always be in this state as long as the box doesn't move, so we will attempt to play many times per second, which is not what we want. We could try to keep some state to know what we are currently playing, but that doesn't feel right. The problem is we cannot do this by iteratively checking state, we instead need a reactive model where we can be told something has just happenned, and we need to take an action. What I've described here is an event model, we need to fire an event when a box gets placed on a spot, and then when we receive this event on the other end we need to play the sound. The really good thing about this is that we will be able to re-use this event system for many other purposes.

Events infrastructure: How

Let's start by discussing how we will implement events. We will not use components or entities (although we could), instead we will use a resource very similar to the input queue. The parts of the code that need to enqueue events will need to get access to this resource, and we will then have a system which processes these events and take the appropriate actions.

What events

Let's discuss in more detail what events we will need:

  1. player hit obstacle - this can be an event in itself which we fire from the input system when we try to move but can't
  2. box placed on correct/incorrect spot - we can model this as a single event with a property inside it that tells us if the box/spot combination is correct - thinking a bit deeper about how we can achieve this, we can have an entity moved event, and when we receive that event we can check the entity id of that entity that just moved to see if it's a box and if it's on the right/wrong/any spot (this is an example of creating an event chain - an event from an event)

Events types

Now let's go into the implementation of events. We'll use an enum to define various event types. Now, we've used enums before (for the rendering type and the box colours) but this time we will take full advantage of the power of Rust enums. One of the most interesting things about them is that we actually attach properties to each enum variant.

Let's look at the event definitions, it should be something like this.


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

Event queue resource

Now let's add a resource for the event queue. We will have various systems writing to this queue and one system (the event system) consuming this queue. It's basically a multiple producer single consumer model.


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

Sending events

Now that we have a way to enqueue events, let's add the two events we need in the input_system: EntityMoved and 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 }));
    }
}

I've omitted some of the code in the original file for readability, but we are really just adding two lines in the right places to create the events and add them to the events vector.

Finally we need to add the events back into the world which we do at the end of the system.


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

Consuming events - event system

Now it's time to add a way to consume the events, which will be the events system. This system will contain the logic for what should happen when a specific event is received.

Let discuss how we will handle each event:

  • Event::PlayerHitObstacle -> this is where the sound playing will go, but we'll come back to this when we add the audio bits
  • Event::EntityMoved(EntityMoved { id }) -> this is where we will add the logic for checking if the entity that just moved is a box and whether it's on a spot or not
  • Event::BoxPlacedOnSpot(BoxPlacedOnSpot { is_correct_spot }) -> this is where the sound playing will go, but we'll come back to this when we add the audio bits

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

The end of this system is important, processing an event could lead to another event created. So we must add events back into the world again.

CODELINK: You can see the full code in this example here.