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, 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<ggez::GameError> 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 = Vec2::new(x, y); let color = Some(Color::new(0.0, 0.0, 0.0, 1.0)); let dimensions = Vec2::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(Vec2::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.
There are plenty of other enhancements that can be made!
CODELINK: You can see the full code in this example here.