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.
There are plenty of other enhancements that can be made!
CODELINK: You can see the full code in this example here.