彩色方块

是时候为我们的游戏增添一些色彩了!到目前为止,游戏玩法相当简单,就是把方块放到指定位置。让我们通过添加不同颜色的方块来让游戏更有趣!现在我们将使用红色和蓝色方块,但你可以根据自己的喜好进行调整,创建更多颜色!现在要赢得游戏,你必须把方块放在相同颜色的目标点上。

资源

首先让我们添加新的资源,右键下载这些图片,或者创建你自己的图片!

蓝色方块 红色方块 蓝色目标点 红色目标点

目录结构应该是这样的(注意我们已经移除了默认的方块和目标点):

├── 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.rs
│   │   ├── input.rs
│   │   ├── mod.rs
│   │   └── rendering.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
pub struct Box {
    pub colour: BoxColour,
}

pub struct BoxSpot {
    pub colour: BoxColour,
}
}

实体创建

让我们在创建方块和目标点时添加颜色参数,并确保根据颜色枚举传递正确的资源路径。

为了根据颜色创建正确的资源路径字符串,我们基本上想要 "/images/box_{}.png",其中 {} 是我们要创建的方块的颜色。现在我们面临的挑战是我们使用的是颜色枚举,所以 Rust 编译器不知道如何将 BoxColour::Red 转换为 "red"。如果能够使用 colour.to_string() 并获得正确的值就太好了。幸运的是,Rust 为我们提供了一个很好的方法,我们需要在 BoxColour 枚举上实现 Display 特征。下面是具体实现,我们只需指定如何将枚举的每个变体映射到字符串。


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

现在让我们在实体创建代码中包含颜色,并使用我们刚刚实现的 colour.to_string() 功能。


#![allow(unused)]
fn main() {
// entities.rs
pub fn create_box(world: &mut World, position: Position, colour: BoxColour) -> Entity {
    world.spawn((
        Position { z: 10, ..position },
        Renderable {
            path: format!("/images/box_{}.png", colour),
        },
        Box { colour },
        Movable {},
    ))
}

pub fn create_box_spot(world: &mut World, position: Position, colour: BoxColour) -> Entity {
    world.spawn((
        Position { z: 9, ..position },
        Renderable {
            path: format!("/images/box_spot_{}.png", colour),
        },
        BoxSpot { colour },
    ))
}
}

地图

现在让我们修改地图代码以允许新的彩色方块和目标点选项:

  • "BB" 表示蓝色方块
  • "RB" 表示红色方块
  • "BS" 表示蓝色目标点
  • "RS" 表示红色目标点

#![allow(unused)]
fn main() {
// map.rs
            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),
            }
}

让我们在初始化关卡时更新我们的静态地图。


#![allow(unused)]
fn main() {
// map.rs
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());
}
}

游戏玩法

现在我们已经完成了艰难的工作,可以继续测试这段代码了。你会注意到一切都能工作,但是存在一个重大的游戏玩法错误。你可以通过把红色方块放在蓝色目标点上或反之来赢得游戏。让我们来修复这个问题。

我们之前学过,根据 ECS 方法论,数据放在组件中,行为放在系统中。我们现在讨论的是行为,所以它必须在系统中。还记得我们如何添加检查是否获胜的系统吗?这正是我们需要修改的地方。

让我们修改运行函数,检查目标点和方块的颜色是否匹配。


#![allow(unused)]
fn main() {
// gameplay.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(|(_, 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, box_spot))| {
            if let Some(the_box) = boxes_by_position.get(&(position.x, position.y)) {
                if box_spot.colour == the_box.colour {
                    0
                } else {
                    1
                }
            } else {
                0
            }
        })
        .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 gameplay = query.iter().next().unwrap().1;
        gameplay.state = GameplayState::Won;
    }
}
}

如果你现在编译代码,它会抱怨我们试图用 == 比较两个枚举。Rust 默认不知道如何处理这个问题,所以我们必须告诉它。我们能做的最好的方法是为 PartialEq 特征添加一个实现。


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

现在是讨论这些不寻常的 derive 注解的好时机。我们以前使用过它们,但从未深入探讨它们的作用。派生属性可以应用于结构体或枚举,它们允许我们为我们的类型添加默认的特征实现。例如,这里我们告诉 Rust 为我们的 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) };
}
}

所以通过在枚举上方添加 #[derive(PartialEq)],我们告诉 Rust BoxColour 现在实现了我们之前看到的偏等特征,这意味着如果我们尝试进行 box_colour_1 == box_colour_2,它将使用这个实现,只检查 colour_1 对象是否与 colour_2 对象相同。这不是最复杂的偏等实现,但对我们的用例来说应该足够了。

更多:这里阅读更多关于 PartialEq 的信息,在这里阅读更多关于可派生特征的信息。

现在我们可以编译代码并通过看到游戏运行来收获我们努力的成果,只有当我们把正确的方块放在正确的位置时,游戏才会告诉我们赢了!

索科班游戏

CODELINK: 你可以在这里看到这个示例的完整代码。