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
pub enum GameplayState {
    Playing,
    Won,
}

#[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.

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 {
        GameplayState::Playing
    }
}
}

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 the input system, so let's adapt that for this purpose.


#![allow(unused)]
fn main() {
// input_system.rs
{{#include ../../../code/rust-sokoban-c02-05/src/systems/input_system.rs}}
        ...
}

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.

Gameplay System

Next, let's integrate this resource with a new gameplay state system. 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 components.

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 crate::components::*;
use hecs::World;

use std::collections::HashMap;

pub fn run_gameplay_state(world: &World) {
    // get all boxes indexed by position
    let mut query = world.query::<(&Position, &Box)>();
    let boxes_by_position: HashMap<(u8, u8), &Box> = query
        .iter()
        .map(|(e, 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
    let boxes_out_of_position: usize = world
        .query::<(&Position, &BoxSpot)>()
        .iter()
        .map(|(_, (position, _))| {
            if boxes_by_position.contains_key(&(position.x, position.y)) {
                0
            } else {
                1
            }
        })
        .collect::<Vec<usize>>()
        .into_iter()
        .sum();

    // If we made it this far, then all box spots have boxes on them, and the
    // game has been won
    if boxes_out_of_position == 0 {
        let mut query = world.query::<&mut Gameplay>();
        let mut gameplay = query.iter().next().unwrap().1;
        gameplay.state = GameplayState::Won;
    }
}
}

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

// main.rs
/* ANCHOR: all */
// Rust sokoban
// main.rs

use ggez::{
    conf, event,
    graphics::{self, DrawParam, Image},
    input::keyboard,
    input::keyboard::{KeyCode, KeyInput},
    Context, GameResult,
};
use glam::Vec2;
use hecs::{Entity, World};

use std::collections::HashMap;
use std::path;

mod components;
mod constants;
mod entities;
mod map;
mod systems;

// ANCHOR: game
// This struct will hold all our game state
// For now there is nothing to be held, but we'll add
// things shortly.
struct Game {
    world: World,
}
// ANCHOR_END: game

// ANCHOR: handler
impl event::EventHandler<ggez::GameError> for Game {
    fn update(&mut self, context: &mut Context) -> GameResult {
        // Run input system
        {
            systems::input::run_input(&self.world, context);
        }

        // Run gameplay state
        {
            systems::gameplay::run_gameplay_state(&mut self.world);
        }

        Ok(())
    }

    fn draw(&mut self, context: &mut Context) -> GameResult {
        // Render game entities
        {
            systems::rendering::run_rendering(&self.world, context);
        }

        Ok(())
    }
}
// ANCHOR_END: handler

// ANCHOR: main
pub fn main() -> GameResult {
    let mut world = World::new();
    map::initialize_level(&mut world);
    entities::create_gameplay(&mut world);

    // Create a game context and event loop
    let context_builder = ggez::ContextBuilder::new("rust_sokoban", "sokoban")
        .window_setup(conf::WindowSetup::default().title("Rust Sokoban!"))
        .window_mode(conf::WindowMode::default().dimensions(800.0, 600.0))
        .add_resource_path(path::PathBuf::from("./resources"));

    let (context, event_loop) = context_builder.build()?;

    // Create the game state
    let game = Game { world };
    // Run the main event loop
    event::run(context, event_loop, game)
}
// ANCHOR_END: main

/* ANCHOR_END: all */

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 rendering system 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 rendering system, so it can print GameplayState to the screen...


#![allow(unused)]
fn main() {
// rendering_systems.rs
{{#include ../../../code/rust-sokoban-c02-05/src/systems/rendering_system.rs:draw_text}}
}

...and then we'll add the Gameplay resource to RenderingSystem so we can call draw_text, and use it all to render the state and number of moves.


#![allow(unused)]
fn main() {
// rendering_system.rs
{{#include ../../../code/rust-sokoban-c02-05/src/systems/rendering_system.rs:draw_gameplay_state}}
}

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.