Coloured boxes
It's time for a little more flair in our game! The gameplay so far is quite simple, put the box on the spot. Let's make it more exciting by adding different coloured boxes. We'll go with red and blue for now but feel free to adapt this to your preference, and create more colours! To win now you'll have to put the box on the same colour spot to win.
Assets
First let's add the new assets, right click and download these as images, or create your own!
The directory structure should look like this (notice we've removed the old default box and default spot).
├── resources
│ └── images
│ ├── box_blue.png
│ ├── box_red.png
│ ├── box_spot_blue.png
│ ├── box_spot_red.png
│ ├── floor.png
│ ├── player.png
│ └── wall.png
├── src
│ ├── systems
│ │ ├── gameplay_state_system.rs
│ │ ├── input_system.rs
│ │ ├── mod.rs
│ │ └── rendering_system.rs
│ ├── components.rs
│ ├── constants.rs
│ ├── entities.rs
│ ├── main.rs
│ ├── map.rs
│ └── resources.rs
├── Cargo.lock
├── Cargo.toml
Component changes
Now let's add an enum for the colour (if you chose to implement more than two colours you'll have to add them here).
#![allow(unused)] fn main() { // components.rs pub enum BoxColour { Red, Blue, } }
Now let's use this enum both for the box and the spot.
#![allow(unused)] fn main() { // components.rs #[derive(Component)] #[storage(VecStorage)] pub struct Box { pub colour: BoxColour, } #[derive(Component)] #[storage(VecStorage)] pub struct BoxSpot { pub colour: BoxColour, } }
Entity creation
Let's also add the colour as a parameter when we created boxes and spots and make sure we pass the correct asset path based on the colour enum.
In order to create the correct string for the asset path we basically want "/images/box_{}.png"
where {}
is the colour of the box we are trying to create. The challenge we have now is that we are using an enum for the colour, so the Rust compiler will not know how to convert BoxColour::Red
into "red"
. It would be really cool to be able to do colour.to_string()
and get the right value. Fortunately, Rust has a nice way for us to do this, we need to implement the Display
trait on the BoxColour
enum. Here is how that looks like, we simply specify how to map each variant of the enum into a string.
#![allow(unused)] fn main() { // components.rs impl Display for BoxColour { fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { fmt.write_str(match self { BoxColour::Red => "red", BoxColour::Blue => "blue", })?; Ok(()) } } }
Now let's include the colour in our entity creation code and use the fancy colour.to_string()
we just made possible in the previous snippet.
#![allow(unused)] fn main() { // entities.rs pub fn create_box(world: &mut World, position: Position, colour: BoxColour) { world .create_entity() .with(Position { z: 10, ..position }) .with(Renderable { path: format!("/images/box_{}.png", colour), }) .with(Box { colour }) .with(Movable) .build(); } pub fn create_box_spot(world: &mut World, position: Position, colour: BoxColour) { world .create_entity() .with(Position { z: 9, ..position }) .with(Renderable { path: format!("/images/box_spot_{}.png", colour), }) .with(BoxSpot { colour }) .build(); } }
Map
Now let's change our map code to allow new options for coloured boxes and spots:
- "BB" for blue box
- "RB" for red box
- "BS" for blue spot
- "RS" for red spot
#![allow(unused)] fn main() { // map.rs use crate::components::{BoxColour, Position}; use crate::entities::*; use specs::World; pub fn load_map(world: &mut World, map_string: String) { // read all lines let rows: Vec<&str> = map_string.trim().split('\n').map(|x| x.trim()).collect(); for (y, row) in rows.iter().enumerate() { let columns: Vec<&str> = row.split(' ').collect(); for (x, column) in columns.iter().enumerate() { // Create the position at which to create something on the map let position = Position { x: x as u8, y: y as u8, z: 0, // we will get the z from the factory functions }; // Figure out what object we should create match *column { "." => create_floor(world, position), "W" => { create_floor(world, position); create_wall(world, position); } "P" => { create_floor(world, position); create_player(world, position); } "BB" => { create_floor(world, position); create_box(world, position, BoxColour::Blue); } "RB" => { create_floor(world, position); create_box(world, position, BoxColour::Red); } "BS" => { create_floor(world, position); create_box_spot(world, position, BoxColour::Blue); } "RS" => { create_floor(world, position); create_box_spot(world, position, BoxColour::Red); } "N" => (), c => panic!("unrecognized map item {}", c), } } } } }
And let's update our static map in the main.
#![allow(unused)] fn main() { // main.rs // Initialize the level pub fn initialize_level(world: &mut World) { const MAP: &str = " N N W W W W W W W W W . . . . W W . . . BB . . W W . . RB . . . W W . P . . . . W W . . . . RS . W W . . BS . . . W W . . . . . . W W W W W W W W W "; load_map(world, MAP.to_string()); } }
Gameplay
Now we've done the hard work, so we can go ahead and test this code out. You'll notice everything works, but there is a big gameplay bug. You can win by putting the red box on the blue spot and viceversa. Let's fix that.
We've learnt before that data goes in components and behaviour goes in systems - as per ECS methodology. What we are discussing now is behaviour, so it must be in a system. Remember how we added a system for checking whether you've won or not? Well that is the exact place we are after.
Let's modify the run function and check the colour of the spot and the box match.
#![allow(unused)] fn main() { // gameplay_state_system.rs 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. since we now have different types of boxes // we need to make sure the right type of box is on the right // type of spot. for (box_spot, position) in (&box_spots, &positions).join() { if let Some(the_box) = boxes_by_position.get(&(position.x, position.y)) { if the_box.colour == box_spot.colour { // continue } else { // return, haven't won yet return; } } 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; } } }
Now if you compile the code at this point it should complain about the fact that we are trying to compare two enums with ==
. Rust doesn't know by default how to handle this, so we must tell it. The best way we can do that is add an implementation for the PartialEq
trait.
#![allow(unused)] fn main() { // components.rs #[derive(PartialEq)] pub enum BoxColour { Red, Blue, } }
Now is a good time to discuss these unusual derive
annotations. We've used them before, but never got too deep into what they do. Derive attributes can be applied to structs or enums and they allow us to add default trait implementations to our types. For example here we are telling Rust to add the PartialEq
default trait implementations to our BoxColour
enum.
Here is how the PartialEq
default implementation looks like, it just checks if something equals itself. If it does, the comparison succeeds and if it doesn't it fails. Don't worry too much about this if it doesn't make sense.
#![allow(unused)] fn main() { pub trait PartialEq { fn eq(&self, other: &Self) -> bool; fn ne(&self, other: &Self) -> bool { !self.eq(other) }; } }
So by adding the #[derive(PartialEq)]
on top of the enum we are telling Rust that BoxColour
now implements the partial eq trait we saw before, which means if we try to do box_colour_1 == box_colour_2
it will use this implementation which will just check if the colour_1 object is the same as the colour_2 object. This is not the most sophisticated partial equality implementation, but it should do just fine for our usecase.
MORE: Read more about PartialEq here and more about derivable traits here.
Now we can compile the code the reap the rewards of our efforts by seeing the game run and telling us we've won only when we put the right box in the right spot!
CODELINK: You can see the full code in this example here.