Gameplay

The player character is able to move and push boxes on the field. Many (but not all!) games have some kind of objective for the player to achieve. The objective for Sokoban-style games is typically to push boxes onto a goal spot. There's nothing stopping the player from doing this now, but the game also isn't checking for success. The player might achieve the objective without realizing it! Let's update the game to check for the success state.

Let's think about what we'll need to add to this game to check for the success condition and to notify the user when they've beaten the level:

  • A resource for tracking the game state
    • Is the game in progress or completed?
    • How many move has the player made?
  • A system for checking if the user has completed their objective
  • A system for updating the number of moves made
  • UI for reporting game state

Gameplay Resource

We're choosing to use a resource to track game state because the game state is not associated with a specific entity. Let's start by defining a Gameplay resource.


#![allow(unused)]
fn main() {
// resources.rs
#[derive(Default)]
pub struct Gameplay {
    pub state: GameplayState,
    pub moves_count: u32
}
}

Gameplay has two fields: state and moves_count. These are used to track the current state of the game (is the game still in play, or has the player won?) and the number of moves made. state is described by an enum, defined like so:


#![allow(unused)]
fn main() {
// resources.rs
pub enum GameplayState {
    Playing,
    Won
}
}

The eagle-eyed reader will note that we used a macro to derive the Default trait for Gameplay, but not for the GameplayState enum. If we want to use Gameplay as a resource, it must implement Default.

So, what will we do? Since Rust macros can't derive Default for enums automatically, we must implement Default for Gameplay ourselves.


#![allow(unused)]
fn main() {
// resources.rs
impl Default for GameplayState {
    fn default() -> Self {
        Self::Playing
    }
}
}

Having defined the resource, let's register it with the world:


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

Now, when the game is started, the Gameplay resource will look like this:


#![allow(unused)]
fn main() {
Gameplay {
    state: GameplayState::Playing,
    moves_count: 0
}
}

Step Counter System

We can increment Gameplay's moves_count field to track the number of turns taken. We already have a system dealing with user input in InputSystem, so let's adapt that for this purpose.

Since we need to mutate the Gameplay resource, we need to register it with InputSystem by adding Write<'a, Gameplay> to the SystemData type definition.


#![allow(unused)]
fn main() {
// input_system.rs
use crate::components::*;
use crate::constants::*;
use crate::resources::{InputQueue, Gameplay};
use ggez::event::KeyCode;
use specs::world::Index;
use specs::{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, 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 input_queue, mut gameplay, entities, mut positions, players, movables, immovables) = data;
        ...
}

Since we've already done the work to check if a player character will move in response to a keypress, we can use that to determine when to increment the step counter.


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

        // 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,
                    _ => (),
                }
            }
        }
    }
}
}

Gameplay System

Next, let's integrate this resource with a new GamePlayStateSystem. This system will continuously check to see if all the boxes have the same position as all the box spots. Once all the boxes are on all the box spots, the game has been won!

Aside from Gameplay, this system only needs read-only access to the Position, Box, and BoxSpot storages.

The system uses Join to create a vector from the Box and Position storages. This vector is mapped into a hashmap containing the location of each box on the board.

Next, the system uses the Join method again to create an iterable from entities that have both BoxSpot and Position components. The system walks through this iterable. If all box spots have a corresponding box at the same position, the game is over and the player has won. Otherwise, the game is still in play.


#![allow(unused)]
fn main() {
// gameplay_state_system.rs
use specs::{Join, ReadStorage, System, Write};
use std::collections::HashMap;

use crate::{
    components::{Box, BoxSpot, Position},
    resources::{Gameplay, GameplayState},
};

pub struct GameplayStateSystem {}

impl<'a> System<'a> for GameplayStateSystem {
    // Data
    type SystemData = (
        Write<'a, Gameplay>,
        ReadStorage<'a, Position>,
        ReadStorage<'a, Box>,
        ReadStorage<'a, BoxSpot>,
    );

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

        // get all boxes indexed by position
        let boxes_by_position: HashMap<(u8, u8), &Box> = (&positions, &boxes)
            .join()
            .map(|t| ((t.0.x, t.0.y), t.1))
            .collect::<HashMap<_, _>>();

        // loop through all box spots and check if there is a corresponding
        // box at that position
        for (_box_spot, position) in (&box_spots, &positions).join() {
            if boxes_by_position.contains_key(&(position.x, position.y)) {
                // continue
            } else {
                gameplay_state.state = GameplayState::Playing;
                return;
            }
        }

        // If we made it this far, then all box spots have boxes on them, and the
        // game has been won
        gameplay_state.state = GameplayState::Won;
    }
}
}

Finally, let's run the gameplay system in our main update loop.


#![allow(unused)]
fn main() {
// main.rs
impl event::EventHandler for Game {
    fn update(&mut self, _context: &mut Context) -> GameResult {
        // Run input system
        {
            let mut is = InputSystem {};
            is.run_now(&self.world);
        }

        // Run gameplay state system
        {
            let mut gss = GameplayStateSystem {};
            gss.run_now(&self.world);
        }

        Ok(())
    }
    // ...
}
}

Gameplay UI

The last step is to provide feedback to the user letting them know what the state of the game is. This requires a resource to track the state and a system to update the state. We can adapt the GameplayState resource and RenderingSystem for this.

First, we'll implement Display for GameplayState so we can render the state of the game as text. We'll use a match expression to allow GameplayState to render "Playing" or "Won".


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

impl Display for GameplayState {
    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
        fmt.write_str(match self {
            GameplayState::Playing => "Playing",
            GameplayState::Won => "Won"
        })?;
        Ok(())
    }
}
}

Next, we'll add a draw_text method to RenderingSystem, so it can print GameplayState to the screen...


#![allow(unused)]
fn main() {
// rendering_systems.rs
impl RenderingSystem<'_> {
    pub fn draw_text(&mut self, text_string: &str, x: f32, y: f32) {
        let text = graphics::Text::new(text_string);
        let destination = na::Point2::new(x, y);
        let color = Some(Color::new(0.0, 0.0, 0.0, 1.0));
        let dimensions = na::Point2::new(0.0, 20.0);

        graphics::queue_text(self.context, &text, dimensions, color);
        graphics::draw_queued_text(
            self.context,
            graphics::DrawParam::new().dest(destination),
            None,
            graphics::FilterMode::Linear,
        )
        .expect("expected drawing queued text");
    }
}
}

...and then we'll add the Gameplay resource to RenderingSystem so we can call draw_text. RenderingSystem needs to be able to read the Gameplay resource.


#![allow(unused)]
fn main() {
// rendering_system.rs
impl<'a> System<'a> for RenderingSystem<'a> {
    // Data
    type SystemData = (Read<'a, Gameplay>, ReadStorage<'a, Position>, ReadStorage<'a, Renderable>);

    fn run(&mut self, data: Self::SystemData) {
        let (gameplay, positions, renderables) = data;

        // Clearing the screen (this gives us the backround colour)
        graphics::clear(self.context, graphics::Color::new(0.95, 0.95, 0.95, 1.0));

        // Get all the renderables with their positions and sort by the position z
        // This will allow us to have entities layered visually.
        let mut rendering_data = (&positions, &renderables).join().collect::<Vec<_>>();
        rendering_data.sort_by_key(|&k| k.0.z);

        // Iterate through all pairs of positions & renderables, load the image
        // and draw it at the specified position.
        for (position, renderable) in rendering_data.iter() {
            // Load the image
            let image = Image::new(self.context, renderable.path.clone()).expect("expected image");
            let x = position.x as f32 * TILE_WIDTH;
            let y = position.y as f32 * TILE_WIDTH;

            // draw
            let draw_params = DrawParam::new().dest(na::Point2::new(x, y));
            graphics::draw(self.context, &image, draw_params).expect("expected render");
        }

        // Render any text
        self.draw_text(&gameplay.state.to_string(), 525.0, 80.0);
        self.draw_text(&gameplay.moves_count.to_string(), 525.0, 100.0);

        // Finally, present the context, this will actually display everything
        // on the screen.
        graphics::present(self.context).expect("expected to present");
    }
}
}

At this point, the game will provide basic feedback to the user:

  • Counts the number of steps
  • Tells the player when they have won

Here's how it looks.

Sokoban play

There are plenty of other enhancements that can be made!

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