不同颜色的盒子

现在我们的游戏有些单调,玩法也比较简单,只是把盒子推到目标点上就可以了.接下来我们可以把盒子和点分成不同的颜色,比如分成红色和蓝色,你可以按照自己的意愿使用不同的颜色,玩家只有把盒子推到一样颜色的目标点上才算赢得了游戏.

素材

首先我们需要添加些新的素材,你可以右键下载这些图片,也可以自己创建一些图片.

Blue box Red box Blue box spot Red box 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

变更组件

接下来我们新增一个用来表示颜色的枚举类型(我们这里只给出了两种颜色,如果你想使用更多的颜色可以在这里添加):


#![allow(unused)]
fn main() {
// components.rs
pub enum BoxColour {
    Red,
    Blue,
}
}

现在我们就可以在盒子和目标点上应用颜色了:


#![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,
}
}

创建实体

在创建盒子和目标点时还需要把颜色做为参数,并根据具体颜色加载相应的图片素材.

我们可以把素材的加载路径设置为 "/images/box_{}.png" ,其中 {} 根据颜色取相应的值.这里我们就需要把枚举类型的颜色值转换为字符串.比如把 BoxColour::Red 转换为"red". 如果能调用 colour.to_string() 转换就好了. 幸运的是Rust提供了 Display 特征,我们只需要为 BoxColour 枚举类型实现这个特征就可以把它转换为相应的字符串了:


#![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(())
    }
}

}

接下来就可以在创建实体时使用颜色了:


#![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();
}
}

地图

现在我们还需要修改下地图映射代码,以支持有颜色的盒子和目标点:

  • "BB" 表示蓝色的盒子
  • "RB" 表示红色的盒子
  • "BS" 表示蓝色的目标点
  • "RS" 表示红色的目标点

#![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),
            }
        }
    }
}
}

接下来还需要在main.rs文件中修改我们的静态地图:


#![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());
}
}

试玩

终于完成了,现在可以运行代码试玩下了.你会发现代码虽然可以正常运行,但是当我们把红色的盒子推到蓝色的目标点上时也会提示获胜了!这跟我们先要的玩法不一样啊,那咋办呢?

我们可以在判断是否获胜的代码里加上判断盒子和目标点的颜色是否一致的代码:


#![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;
    }
}
}

现在编译代码,编译器会报错,因为Rust不知道怎么对俩个枚举值进行 == 操作.怎么告诉Rust怎么处理呢?可以为颜色枚举类型实现 PartialEq 特征.


#![allow(unused)]
fn main() {
// components.rs
#[derive(PartialEq)]
pub enum BoxColour {
    Red,
    Blue,
}
}

这里我们使用了扩展 derive 注解. 先前我们也用过这个注解只是没做过多介绍.扩展Deribe注解可以被用在结构体和枚举类型上,帮他们快速实现一些特征.比如我们这里使用的#[derive(PartialEq)]就是为枚举类型BoxColour 快速实现了PartialEq特征.

快速实现的 PartialEq 是什么样的呢?看起来就像下面这样:


#![allow(unused)]
fn main() {
pub trait PartialEq {
  fn eq(&self, other: &Self) -> bool;
  fn ne(&self, other: &Self) -> bool { !self.eq(other) };
}
}

虽然只是简单的判断了两个对象是否相等,如果相等就返回true,如果不相等就返回false,但对我们当前的应用场景已经够用了.

也就是说由于我们通过给枚举类型BoxColor添加#[derive(PartialEq)]注解实现了PartialEq特征,当我们在使用==比较俩个颜色时,Rust就知道怎么处理了.它会判断这俩个颜色值是不是一样的.如果一样就返回true,如果不一样就返回false.

MORE: 了解更多关于 PartialEq 知识请点这里 了解更多关于Derive知识请点 这里.

现在再编译运行代码,就可以基于颜色判断是否获胜了.

Sokoban play

CODELINK: 可以点 这里获取当前完整代码.