扫雷游戏 bevy 实践(bevy 0.12)-1

经典的扫雷游戏 bevy 实践(bevy 0.12)

网上大多是0.6的

但愿大家能够摸索着 上手


参考资料:

Bevy Minesweeper: Introduction - DEV Community

(原始教程,0.6版本)

https://github.com/leonidv/bevy-minesweeper-tutorial

(教程版本更新至bevy 0.12.1)


先是原始教程部分:

Bevy Minesweeper: Introduction - DEV Community

bevy扫雷:简介

Bevy 扫雷(12 部分系列)

2 Bevy 扫雷:项目设置

3 Bevy Minesweeper:平铺地图生成

4 Bevy 扫雷:董事会

5 Bevy 扫雷:瓷砖和组件

6 Bevy 扫雷:输入管理

7 Bevy 扫雷:发现瓷砖

8 Bevy 扫雷:安全开始

9 Bevy 扫雷:通用状态

10 Bevy 扫雷:资产

11 Bevy 扫雷:标记方块

12 Bevy 扫雷:WASM 构建


检查存储库

1 贝维扫雷:简介

本课程的最终结果是一个跨平台扫雷游戏,可在此实时版本中进行测试。

本教程重点关注以下目标:

  • 详细介绍 Bevy 的基本功能和制作扫雷的 ECS
  • 使用inspector gui、logger等开发工具
  • 开发一个通用的 bevy 插件,充分利用状态系统和资源
  • 拥有 Web Assembly 支持

编程选择不是最有效的,但可以实现目标。例如,您可能会注意到发现系统有 3 到 4 帧的延迟:

  • 第一帧:点击事件读取和平铺触发事件发送
  • 第二帧:tile触发事件读取和Uncover组件插入
  • 第3帧:实际揭开

有更好的方法可以做到这一点,但通过这种方式,您可以学习接收和发送事件、放置组件以便系统查询它等。

对 Rust 语言有充分的了解是先决条件。

请注意,这是我的第一个 Bevy 项目,可能会有改进,因此请相信最新版本的代码。

ECS

那么什么是实体组件系统呢?

Unity文档对 ECS 有很好的图解解释:

它是一种面向数据的编码范例,使用以下元素:

  • 实体:通过简单的标识符(通常是经典整数)表示对象    ---识别标识
  • 组件:可以附加到实体的结构,包含数据但不包含逻辑    ---数据存储
  • 系统:使用组件 查询来应用逻辑的函数                            ---逻辑处理(或状态转移)

重点是将相同的逻辑应用于所有Health组件,而不是将逻辑应用于完整的对象。
它更加模块化,并且更容易在线程中进行管理。

最后一个元素是资源,它是跨系统共享的数据。

实体组件系统

实体组件系统 (ECS) 是 Unity 面向数据的技术堆栈的核心。顾名思义,ECS 具有三个主要部分:

  • 实体— 填充您的游戏或程序的实体或事物。
  • 成分— 与您的实体关联的数据,但由数据本身而不是实体组织。(组织上的这种差异是面向对象设计和面向数据设计之间的主要区别之一。)
  • 系统- 将组件数据从当前状态转换为下一个状态的逻辑 - 例如,系统可能会通过速度乘以自上一帧以来的时间间隔来更新所有移动实体的位置。

2 Bevy 扫雷:项目设置

本教程的目的之一是创建一个可以嵌入到任何应用程序中的通用插件。
为此,我们将初始化两个嵌套的cargo项目。

cargo 设置

主要的二进制应用程序:

  • cargo init --bin . --name minesweeper-tutorial

和板插件:

  • cargo init --lib board_plugin

你的目录应该是这样的:

├── Cargo.toml
├── board_plugin
│   ├── Cargo.toml
│   └── src
│       └── lib.rs
└── src
    └── main.rs

板插件配置

Cargo.toml

将以下元素添加到board_plugin/Cargo.toml文件中:

[features]
default = []
debug = ["colored", "bevy-inspector-egui"]

[dependencies]
# Engine
bevy = "0.6"

# Serialization
serde = "1.0"

# Random
rand = "0.8"

# Console Debug
colored = { version = "2.0", optional = true }
# Hierarchy inspector debug
bevy-inspector-egui = { version = "0.8", optional = true }

本教程末尾的检查器 GUI:

检查器图形用户界面

我们通过调试功能门激活调试箱debug

库文件

删除生成的代码并创建插件struct

// board_plugin/src/lib.rs
pub struct BoardPlugin;

应用程序配置

Cargo.toml

将以下元素添加到主src/Cargo.toml文件中:

[features]
default = []
debug = ["board_plugin/debug", "bevy-inspector-egui"]

[dependencies]
bevy = "0.6"
board_plugin = { path = "board_plugin" }

# Hierarchy inspector debug
bevy-inspector-egui = { version = "0.8", optional = true }

[workspace]
members = [
    "board_plugin"
]

我们使用我们的 lib 作为依赖项并将其添加到我们的workspace.

主程序.rs

将以下内容添加到src/main.rs文件中:

use bevy::prelude::*;

#[cfg(feature = "debug")]
use bevy_inspector_egui::WorldInspectorPlugin;

fn main() {
    let mut app = App::new();
    // Window setup
    app.insert_resource(WindowDescriptor {
        title: "Mine Sweeper!".to_string(),
        width: 700.,
        height: 800.,
        ..Default::default()
    })
    // Bevy default plugins
    .add_plugins(DefaultPlugins);
    #[cfg(feature = "debug")]
    // Debug hierarchy inspector
    app.add_plugin(WorldInspectorPlugin::new());
    // Startup system (cameras)
    app.add_startup_system(camera_setup);
    // Run the app
    app.run();
}

fn camera_setup(mut commands: Commands) {
    // 2D orthographic camera
    commands.spawn_bundle(OrthographicCameraBundle::new_2d());
}

让我们来分解一下:

  • bevy App是我们所有游戏逻辑的构建者,允许注册系统资源插件
  • bevy Plugin是应用程序构建逻辑的容器,是一种向应用程序添加系统 和资源的模块化方式。

    例如,将注册显示 GUI 检查器所需的WorldInspectorPlugin每个系统资源。

  • Bevy DefaultPlugins是一个基本插件的集合,提供基本的引擎功能,如输入处理、窗口、转换、渲染......

我们添加一项资源 WindowDescriptor 来自定义我们的窗口。

添加资源有何作用?它们只是数据!

资源确实只是数据,没有逻辑。注册 DefaultPlugins系统负责使用资源作为配置来绘制窗口WindowDescriptor
该资源是可选的,因为如果您不设置任何内容,系统只会使用默认值。

我们将使用我们BoardPlugin.

启动系统

我们还注册了一个启动系统camera_setup

经典系统每帧运行,并具有可选的运行标准,例如StagesFixedTimeSteps启动系统
仅在启动时运行一次。

我们这样注册系统:

app.add_system(my_function)
这是相机设置功能:
fn camera_setup(mut commands: Commands) {
    // 2D orthographic camera
    // 2D正射相机
    commands.spawn_bundle(OrthographicCameraBundle::new_2d());
}

Commands参数是每个需要世界编辑的系统的主要 ECS 工具,它允许生成和删除实体、向实体添加组件、插入和删除资源等。

那么每个系统都只有一个参数吗?

完全不是,并且Commands参数是可选的。系统可以有任意数量的参数,但只能是有效的ECS参数,例如:

  • 我们刚才看到的Commands(Commands)
  • 封装在Res<>或ResMut<>中的资源(可以是资产、窗口或任何插入的资源)
  • 组件查询Query<>)
  • 事件项目(EventReader<>EventWriter<>)
  • 等等。

Bevy 将自动为您处理一切,并为您的系统提供正确的参数。

系统“生成一个组件包”,这是什么意思?

我们在简介中解释说,在我们的游戏世界中,存在带有附加组件的实体

要生成实体并添加组件,我们可以这样做:

fn my_system(mut commands: Commands) {
  // This spawns an entity and returns a builder
  // 这会生成一个实体,并返回一个构建器
  let entity = commands.spawn();
  // We can add components to the entity
  // 我们可以向实体添加组件
  entity
          .insert(MyComponent {})
          .insert(MyOtherComponent {});
}
但对于复杂的对象,我们使用包含要添加的组件集合的捆绑包(Bundles)

我们就可以这样做:

fn my_system(mut commands: Commands) {
  let entity = commands.spawn();
  entity.insert_bundle(MyComponentBundle::new());
}
或直接:
fn my_system(mut commands: Commands) {
  // This spawns an entity with all components in the bundle
  // 这会生成一个带有组件包中所有组件的实体
  commands.spawn_bundle(MyComponentBundle::new());
}
在我们的系统中,我们生成一个具有所有关联组件的相机实体,以拥有一个 2D 正交相机。

运行

您现在可以运行该应用程序

  • cargo run: 给你一个空窗口
  • cargo run --features debug 调试UI:

检查器图形用户界面

显示调试检查器,我们可以看到 2D 相机实体以及通过捆绑包插入的组件。


3Bevy Minesweeper:平铺地图生成

让我们生成扫雷基础图块地图并设置我们的插件。

在board_plugin中创建一个包含coordinates.rs文件的components模块,和一个包含tile.rs和tilemap.rs文件的resources模块:

<span style="color:var(--syntax-text-color)"><span style="color:var(--syntax-text-color)"><code>├── Cargo.toml
└── src
    ├── components
    │   ├── coordinates.rs
    │   └── mod.rs
    ├── lib.rs
    └── resources
        ├── mod.rs
        ├── tile.rs
        └── tile_map.rs
</code></span></span>

组件

为了管理图块和坐标,我们将制作第一个组件Coordinates

// coordinates.rs
use std::fmt::{self, Display, Formatter};
use std::ops::{Add, Sub};
use bevy::prelude::Component;

#[cfg_attr(feature = "debug", derive(bevy_inspector_egui::Inspectable))]
#[derive(Debug, Default, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Component)]
pub struct Coordinates {
    pub x: u16,
    pub y: u16,
}

// We want to be able to make coordinates sums..
// 我们希望能够进行坐标的求和...
impl Add for Coordinates {
    type Output = Self;

    fn add(self, rhs: Self) -> Self::Output {
        Self {
            x: self.x + rhs.x,
            y: self.y + rhs.y,
        }
    }
}

// ..and subtractions
// ..和减法
impl Sub for Coordinates {
    type Output = Self;

    fn sub(self, rhs: Self) -> Self::Output {
        Self {
            x: self.x.saturating_sub(rhs.x),
            y: self.y.saturating_sub(rhs.y),
        }
    }
}

impl Display for Coordinates {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        write!(f, "({}, {})", self.x, self.y)
    }
}

Coordinate结构包含代表板的两个轴的无符号数值。
我们添加Display实现作为良好实践,并添加AddSub实现以允许数字运算。

请注意使用saturating_sub以避免减法结果为负时出现恐慌(panic)。

我们通过特征门debug添加派生Inspectable。这个特性(trait)将使我们的组件在检查器 GUI 中正确显示。

为什么要在这里做一个组件呢?

我们还不会将Coordinates用作组件,但我们会在以后的步骤中使用。这说明了一个重要方面:如果派生了Component,任何东西都可以成为组件
我们还添加了一系列派生属性,这些属性将来会很有用。

资源

图块

让我们声明我们的图块:

// tile.rs
#[cfg(feature = "debug")]
use colored::Colorize;

/// Enum describing a Minesweeper tile
/// 枚举描述一个扫雷游戏的图块
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum Tile {
    /// Is a bomb
    /// 是一个炸弹
    Bomb,
    /// Is a bomb neighbor
    /// 是一个炸弹的邻居
    BombNeighbor(u8),
    /// Empty tile
    /// 空图块
    Empty,
}

impl Tile {
    /// Is the tile a bomb?
    /// 图块是炸弹吗?
    pub const fn is_bomb(&self) -> bool {
        matches!(self, Self::Bomb)
    }

    #[cfg(feature = "debug")]
    pub fn console_output(&self) -> String {
        format!(
            "{}",
            match self {
                Tile::Bomb => "*".bright_red(),
                Tile::BombNeighbor(v) => match v {
                    1 => "1".cyan(),
                    2 => "2".green(),
                    3 => "3".yellow(),
                    _ => v.to_string().red(),
                },
                Tile::Empty => " ".normal(),
            }
        )
    }
}

我们使用枚举来避免复杂的结构体,并添加了console_output方法,这个方法使用了我们可选的colorize包。。

为什么这不是一个组件?

我们可以将Tile其用作组件,但正如我们将在未来的步骤中看到的,我们需要最大的灵活性,这意味着:

  • 炸弹图块将有一个特定的组件
  • 炸弹邻居图块也将有一个特定的组件

查询Query<>) 只能过滤组件存在或不存在(我们称之为查询工件),因此直接使用我们的Tile结构将
不允许我们的系统使用直接针对炸弹图块的查询,因为所有图块都会被查询。

图块地图

让我们制作图块地图生成器:

空地图

// tile_map.rs
use crate::resources::tile::Tile;
use std::ops::{Deref, DerefMut};

/// Base tile map
/// 基础瓦片地图
#[derive(Debug, Clone)]
pub struct TileMap {
    bomb_count: u16,
    height: u16,
    width: u16,
    map: Vec<Vec<Tile>>,
}

impl TileMap {
    /// Generates an empty map
    /// 生成一个空地图
    pub fn empty(width: u16, height: u16) -> Self {
        let map = (0..height)
            .into_iter()
            .map(|_| (0..width).into_iter().map(|_| Tile::Empty).collect())
            .collect();
        Self {
            bomb_count: 0,
            height,
            width,
            map,
        }
    }

    #[cfg(feature = "debug")]
    pub fn console_output(&self) -> String {
        let mut buffer = format!(
            "Map ({}, {}) with {} bombs:\n",
            // "地图 ({}, {}),包含 {} 个炸弹:\n",
            self.width, self.height, self.bomb_count
        );
        let line: String = (0..=(self.width + 1)).into_iter().map(|_| '-').collect();
        buffer = format!("{}{}\n", buffer, line);
        for line in self.iter().rev() {
            buffer = format!("{}|", buffer);
            for tile in line.iter() {
                buffer = format!("{}{}", buffer, tile.console_output());
            }
            buffer = format!("{}|\n", buffer);
        }
        format!("{}{}", buffer, line)
    }

    // Getter for `width`
    // `width`的获取器
    pub fn width(&self) -> u16 {
        self.width
    }

    // Getter for `height`
    // `height`的获取器
    pub fn height(&self) -> u16 {
        self.height
    }

    // Getter for `bomb_count`
    // `bomb_count`的获取器
    pub fn bomb_count(&self) -> u16 {
        self.bomb_count
    }
}

impl Deref for TileMap {
    type Target = Vec<Vec<Tile>>;

    fn deref(&self) -> &Self::Target {
        &self.map
    }
}

impl DerefMut for TileMap {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.map
    }
}
我们的图块地图具有我们需要的每个生成选项:
  • widthheight设置尺寸和图块数量
  • bomb_count设置地雷数量
  • map 是一个双层二维图块数组

为什么使用 Vec<> 而不是切片?

我尝试做一些事情,比如[[Tile; WIDTH]; HEIGHT]来利用rust 1.52 的generic consts(泛型常量)的功能,但我发现它变得非常混乱。
如果您找到一种干净的方法来做到这一点,我很乐意接受一个拉取请求!

现在我们有:

  • 一个empty方法 ,来构建Tile::Empty图块地图
  • 一个 console_output 方法在控制台打印图块地图
  • 一个 Deref 和 DerefMut 实现指向我们的二维向量
炸弹和邻居

让我们声明一个 2D 增量坐标数组:

// tile_map.rs
/// Delta coordinates for all 8 square neighbors
/// 所有 8 个方块邻居的增量坐标
const SQUARE_COORDINATES: [(i8, i8); 8] = [
    // Bottom left  // 左下
    (-1, -1),
    // Bottom  // 下
    (0, -1),
    // Bottom right      // 右下
    (1, -1),
    // Left  // 左
    (-1, 0),
    // Right   // 右
    (1, 0),
    // Top Left    // 左上
    (-1, 1),
    // Top    // 上
    (0, 1),
    // Top right  // 右上
    (1, 1),
];

这些元组定义了任何图块周围的正方形中 8 个图块的增量坐标:

*--------*-------*-------*
| -1, 1  | 0, 1  | 1, 1  |
|--------|-------|-------|
| -1, 0  | tile  | 1, 0  |
|--------|-------|-------|
| -1, -1 | 0, -1 | 1, -1 |
*--------*-------*-------*

我们可以通过添加一个方法来检索相邻图块来利用它:

// tile_map.rs
use crate::components::Coordinates;

 pub fn safe_square_at(&self, coordinates: Coordinates) -> impl Iterator<Item = Coordinates> {
        SQUARE_COORDINATES
            .iter()
            .copied()
            .map(move |tuple| coordinates + tuple)
    }

为了允许Coordinates+(i8, i8)我们需要在coordinates.rs中添加以下内容:
// coordinates.rs
impl Add<(i8, i8)> for Coordinates {
    type Output = Self;

    fn add(self, (x, y): (i8, i8)) -> Self::Output {
        let x = ((self.x as i16) + x as i16) as u16;
        let y = ((self.y as i16) + y as i16) as u16;
        Self { x, y }
    }
}

现在我们可以检索周围的图块,我们将使用它来计算坐标周围的炸弹数量,以填充我们的炸弹邻居图块:

// tile_map.rs

pub fn is_bomb_at(&self, coordinates: Coordinates) -> bool {
    if coordinates.x >= self.width || coordinates.y >= self.height {
        return false;
    };
    self.map[coordinates.y as usize][coordinates.x as usize].is_bomb()
}

pub fn bomb_count_at(&self, coordinates: Coordinates) -> u8 {
    if self.is_bomb_at(coordinates) {
        return 0;
    }
    let res = self
         .safe_square_at(coordinates)
         .filter(|coord| self.is_bomb_at(*coord))
         .count();
    res as u8
}

让我们放置炸弹和邻居!

// tile_map.rs
use rand::{thread_rng, Rng};

/// Places bombs and bomb neighbor tiles
/// 放置炸弹和炸弹邻居图块
pub fn set_bombs(&mut self, bomb_count: u16) {
    self.bomb_count = bomb_count;
    let mut remaining_bombs = bomb_count;
    let mut rng = thread_rng();
    // Place bombs
    // 放置炸弹
    while remaining_bombs > 0 {
        let (x, y) = (
            rng.gen_range(0..self.width) as usize,
            rng.gen_range(0..self.height) as usize,
        );
        if let Tile::Empty = self[y][x] {
            self[y][x] = Tile::Bomb;
            remaining_bombs -= 1;
        }
    }
    // Place bomb neighbors
    // 放置炸弹邻居
    for y in 0..self.height {
        for x in 0..self.width {
            let coords = Coordinates { x, y };
            if self.is_bomb_at(coords) {
                continue;
            }
            let num = self.bomb_count_at(coords);
            if num == 0 {
                continue;
            }
            let tile = &mut self[y as usize][x as usize];
            *tile = Tile::BombNeighbor(num);
        }
    }
}

太好了,让我们连接模块中的所有内容:

// board_plugin/resources/mod.rs

pub(crate) mod tile;
pub(crate) mod tile_map;
// board_plugin/components/mod.rs
pub use coordinates::Coordinates;

mod coordinates;

插件

我们有了图块地图,让我们在插件中测试它:

// lib.rs
pub mod components;
pub mod resources;

use bevy::log;
use bevy::prelude::*;
use resources::tile_map::TileMap;

pub struct BoardPlugin;

impl Plugin for BoardPlugin {
    fn build(&self, app: &mut App) {
        app.add_startup_system(Self::create_board);
        log::info!("Loaded Board Plugin");
    }
}

impl BoardPlugin {
    /// System to generate the complete board
    /// 生成完整棋盘的系统
    pub fn create_board() {
        let mut tile_map = TileMap::empty(20, 20);
        tile_map.set_bombs(40);
        #[cfg(feature = "debug")]
        log::info!("{}", tile_map.console_output());
    }
}

这里发生的事情应该看起来很熟悉,我们实现了Plugin. 然后我们注册一个简单的启动系统来生成新的图块地图并打印它。BoardPluginApp

我们需要将我们的插件注册到我们的main.rs

// main.rs
use board_plugin::BoardPlugin;

// ..
.add_plugin(BoardPlugin)

让我们运行该应用程序:cargo run --features debug

现在我们在控制台中打印了图块地图:

2022-02-21T09:24:05.748340Z  INFO board_plugin: Loaded Board Plugin
2022-02-21T09:24:05.917041Z  INFO board_plugin: Map (20, 20) with 40 bombs:
----------------------
|      111  1*1      |
|      1*211111 111  |
|111   223*1    1*1  |
|1*1   1*211   1221  |
|111   111     1*1   |
|121211    111 111   |
|*2*2*1    1*211     |
|121211  11212*21    |
|111     1*1 13*31   |
|1*1     111  2**21  |
|222         1234*2  |
|2*2         1*23*211|
|2*2         123*222*|
|111     1221 1*211*2|
|   111  1**1 111 111|
|   1*1  1221   1221 |
| 11322111   1111**1 |
| 1*3*11*1  12*11221 |
|123*21222  1*21 111 |
|1*211 1*1  111  1*1 |
----------------------

4 Bevy 扫雷:董事会

我们已经有了图块地图,但屏幕上仍然没有任何内容,让我们创建一些图块!

主板选项

为了实现我们制作完全模块化插件的目标,我们必须首先提供生成选项。我们现在将创建一个很好的配置资源,就像我们在第 1 部分
中看到的 bevy WindowDescriptor 一样。

在我们的插件中创建一个模块board_options

├── Cargo.toml
└── src
    ├── components
    │   ├── coordinates.rs
    │   └── mod.rs
    ├── lib.rs
    └── resources
        ├── board_options.rs
        ├── mod.rs
        ├── tile.rs
        └── tile_map.rs

并将其添加到mod.rs文件中:

// ..
pub use board_options::*;

mod board_options;

我们想要以下选项:

  • 所有图块地图的选项(宽度、高度和炸弹数量)
  • 平铺精灵之间的自定义填充间距
  • 自定义图块尺寸或窗口自适应尺寸
  • 自定义棋盘世界位置或窗口居中并可选偏移
  • 可选的安全无遮盖起始区

好多啊 !

是的,但是越多越好!我们开工吧:

// board_options.rs
use bevy::prelude::Vec3;
use serde::{Deserialize, Serialize};

/// Tile size options
/// 图块大小选项
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum TileSize {
    /// Fixed tile size
    /// 固定图块大小
    Fixed(f32),
    /// Window adaptative tile size
    /// 窗口自适应图块大小
    Adaptive { min: f32, max: f32 },
}

/// Board position customization options
/// 棋盘位置自定义选项
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum BoardPosition {
    /// Centered board
    /// 居中的棋盘
    Centered { offset: Vec3 },
    /// Custom position
    /// 自定义位置
    Custom(Vec3),
}

/// Board generation options. Must be used as a resource
/// 棋盘生成选项。必须作为资源使用
// We use serde to allow saving option presets and loading them at runtime
// 我们使用 serde 来支持保存选项预设并在运行时加载它们
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BoardOptions {
    /// Tile map size
    /// 图块地图大小
    pub map_size: (u16, u16),
    /// bomb count
    /// 炸弹数量
    pub bomb_count: u16,
    /// Board world position
    /// 棋盘世界位置
    pub position: BoardPosition,
    /// Tile world size
    /// 图块世界大小
    pub tile_size: TileSize,
    /// Padding between tiles
    /// 图块间填充
    pub tile_padding: f32,
    /// Does the board generate a safe place to start
    /// 棋盘是否生成一个安全的起始地方
    pub safe_start: bool,
}

这看起来很复杂,但如果我们实现良好的Default实现,我们的选项将非常易于使用

// board_options.rs

impl Default for TileSize {
    fn default() -> Self {
        Self::Adaptive {
            min: 10.0,
            max: 50.0,
        }
    }
}

impl Default for BoardPosition {
    fn default() -> Self {
        Self::Centered {
            offset: Default::default(),
        }
    }
}

impl Default for BoardOptions {
    fn default() -> Self {
        Self {
            map_size: (15, 15),
            bomb_count: 30,
            position: Default::default(),
            tile_size: Default::default(),
            tile_padding: 0.,
            safe_start: false,
        }
    }
}
让我们将新资源注册到应用程序:
// main.rs
+ use board_plugin::resources::BoardOptions;

fn main() {
    let mut app = App::new();
    // Window setup
    // 窗口设置
    app.insert_resource(WindowDescriptor {
        title: "Mine Sweeper!".to_string(),
        width: 700.,
        height: 800.,
        ..Default::default()
    })
    // Bevy default plugins
    // Bevy 默认插件
    .add_plugins(DefaultPlugins);
    #[cfg(feature = "debug")]
    // Debug hierarchy inspector
    // 调试层次结构检视器
    app.add_plugin(WorldInspectorPlugin::new());
+     // Board plugin options
      // 棋盘插件选项
+     app.insert_resource(BoardOptions {
+         map_size: (20, 20),
+         bomb_count: 40,
+         tile_padding: 3.0,
+         ..Default::default()
+     })
    .add_plugin(BoardPlugin)
    // Startup system (cameras)
    // 启动系统(摄像机)
    .add_startup_system(camera_setup);
    // Run the app
    // 运行应用程序
    app.run();
}

棋盘生成

现在我们有了生成选项资源,让我们在插件中使用它来构建我们的第一个棋盘。
让我们编辑create_board启动系统:

参数和瓦片地图

// lib.rs
use resources::BoardOptions;

// ..
pub fn create_board(
        mut commands: Commands,
        board_options: Option<Res<BoardOptions>>,
        window: Res<WindowDescriptor>,
    ) {}
请注意,我们向系统添加了参数:
  • Commands 因为我们将生成实体和组件
  • Option<Res<BoardOption>> 是我们新一代的选项资源,但可选!
  • Res<WindowDescriptor> 是我们在main.rs中设置的窗口配置资源

/!\在编写本教程时,我意识到由于WindowDescriptor资源是可选的,如果没有设置窗口配置,我们的系统将会出现恐慌。
更好的做法是像我们的 BoardOptions 那样将其用在 Option<> 中,或者直接访问 Windows 资源。

由于我们的生成选项是可选的,如果未设置,我们需要使用Default实现:

// ..
let options = match board_options {
     None => BoardOptions::default(), // If no options is set we use the default one
                                      // 如果没有设置选项,我们使用默认的
     Some(o) => o.clone(),
};

我们现在可以生成瓦片地图了:

// ..
// Tilemap generation
// 图块地图生成
let mut tile_map = TileMap::empty(options.map_size.0, options.map_size.1);
tile_map.set_bombs(options.bomb_count);
#[cfg(feature = "debug")]
// Tilemap debugging
// 图块地图调试
log::info!("{}", tile_map.console_output());

图块尺寸

我们为图块大小添加了选项,并且使图块大小根据窗口确定。如果选择了该选项,我们必须在窗口和图块地图尺寸之间计算瓦片大小。

在 BoardPlugin 中添加以下方法:

// lib.rs
/// Computes a tile size that matches the window according to the tile map size
/// 根据图块地图大小计算与窗口相匹配的图块大小
fn adaptative_tile_size(
    window: Res<WindowDescriptor>,
    (min, max): (f32, f32), // Tile size constraints
    (width, height): (u16, u16), // Tile map dimensions
) -> f32 {
    let max_width = window.width / width as f32;
    let max_heigth = window.height / height as f32;
    max_width.min(max_heigth).clamp(min, max)
}

让我们在create_board系统中使用它:

// lib.rs
use resources::TileSize;

// ..
// We define the size of our tiles in world space
// 我们定义了图块在世界空间中的大小
let tile_size = match options.tile_size {
    TileSize::Fixed(v) => v,
    TileSize::Adaptive { min, max } => Self::adaptative_tile_size(
        window,
        (min, max),
        (tile_map.width(), tile_map.height()),
    ),
}

棋盘创建

我们现在可以计算棋盘世界大小和世界位置world_position了

// lib.rs
use resources::BoardPosition;

// ..
// We deduce the size of the complete board
// 我们推算出完整棋盘的大小
let board_size = Vec2::new(
    tile_map.width() as f32 * tile_size,
    tile_map.height() as f32 * tile_size,
);
log::info!("board size: {}", board_size);
// We define the board anchor position (bottom left)
// 我们定义棋盘的锚点位置(左下角)
let board_position = match options.position {
    BoardPosition::Centered { offset } => {
        Vec3::new(-(board_size.x / 2.), -(board_size.y / 2.), 0.) + offset
    }
    BoardPosition::Custom(p) => p,
};

这是一个奇怪的计算

我在这里选择将棋盘锚定固定在左下角而不是中心,以便将所有图块子项放置
在正相对位置。

坐标:

坐标

实际的棋盘对象将位于可见棋盘的左下角

我们现在可以创建我们的棋盘:

// lib.rs
// ..
commands
            .spawn()
            .insert(Name::new("Board"))
            .insert(Transform::from_translation(board_position))
            .insert(GlobalTransform::default());

如果我们运行该应用程序,我们现在有一个包含三个组件的空棋盘:

  • Name(将由检查器 GUI 使用)
  • Transform,描述其局部平移缩放旋转
  • GlobalTransform 描述与 Transform 相同的值,但是全局的

请注意,我们必须创建两者Transform, GlobalTransform。但我们从未设置全局的。
如果缺少一个,整个层次结构将不会按预期运行。

让我们创建 Board 背景:将以下内容添加到代码spawn

// lib.rs
// ..
    .with_children(|parent| {
                // We spawn the board background sprite at the center of the board, since the sprite pivot is centered
                 // 我们在棋盘中心生成棋盘背景精灵,因为精灵的轴心是居中的
                parent
                    .spawn_bundle(SpriteBundle {
                        sprite: Sprite {
                            color: Color::WHITE,
                            custom_size: Some(board_size),
                            ..Default::default()
                        },
                        transform: Transform::from_xyz(board_size.x / 2., board_size.y / 2., 0.),
                        ..Default::default()
                    })
                    .insert(Name::new("Background"));
            });

那么这里发生了什么:
首先我们使用with_children,为我们提供了一个类似于Commands的构建器,生成我们的 Board 子对象 。
然后我们生成了一个新的带有 SpriteBundle 的 "Background" 实体(注意所有内置的 Component Bundles 已经包含 Transform 和 GlobalTransform 组件):

- sprite:我们创建了一个基本的矩形,大小为我们的棋盘,并且颜色为白色。
- transform:精灵的锚点是居中的,由于我们把棋盘放在了左下角,我们希望将这个背景放在棋盘中心。

背景坐标:

背景坐标

背景位于中心

让我们生成图块吧!

// lib.rs
use components::Coordinates;

// ..
    .with_children(|parent| {

        // ..
 
        // Tiles 图块
        for (y, line) in tile_map.iter().enumerate() {
                    for (x, tile) in line.iter().enumerate() {
                        parent
                            .spawn_bundle(SpriteBundle {
                                sprite: Sprite {
                                    color: Color::GRAY,
                                    custom_size: Some(Vec2::splat(
                                        tile_size - options.tile_padding as f32,
                                    )),
                                    ..Default::default()
                                },
                                transform: Transform::from_xyz(
                                    (x as f32 * tile_size) + (tile_size / 2.),
                                    (y as f32 * tile_size) + (tile_size / 2.),
                                    1.,
                                ),
                                ..Default::default()
                            })
                            .insert(Name::new(format!("Tile ({}, {})", x, y)))
                            // We add the `Coordinates` component to our tile entity
                            // 我们为图块实体添加了 `Coordinates` 组件
                            .insert(Coordinates {
                                x: x as u16,
                                y: y as u16,
                            });
                    }
                }
    }

我们遍历图块地图并使用SpriteBundle再次为每个图块生成一个新实体。我们还为每个图块添加一个Coordinates组件。
请注意,我们输入Transform1 的z值为1,图块比背景更靠近相机,因此打印在背景之上。

让我们使用debug功能运行我们的应用程序

cargo run --features debug

检视器 GUI

检查器图形用户界面

我们的棋盘已生成,我们可以观察 Transform 和 GlobalTransform 之间的差异。

  • 我们的棋盘实体TransformGlobalTransform是相同的,因为该实体没有父对象
  • 我们的图块Transform 位移是相对于它们的父级(棋盘实体)而言的,从而在组件中给出真实的位移GlobalTransform

5Bevy 扫雷:图块和组件

资产

让我们完成我们的棋盘,为此我们需要资产

  • 炸弹png纹理
  • 一种字体

(您可以使用教程存储库中的资源)

将您的资产放置在assets项目根目录的文件夹中

<span style="color:var(--syntax-text-color)"><span style="color:var(--syntax-text-color)"><code>├── Cargo.lock
├── Cargo.toml
├── assets
│   ├── fonts
│   │   └── my_font.ttf
│   └── sprites
│       ├── bomb.png
├── board_plugin
│   ├── Cargo.toml
│   └── src
│       ├── components
│       ├── lib.rs
│       └── resources
├── src
│   └── main.rs
</code></span></span>

让我们将这些资产加载到我们的create_board启动系统中。
为此,我们需要向系统添加一个参数:

 pub fn create_board(
        mut commands: Commands,
        board_options: Option<Res<BoardOptions>>,
        window: Res<WindowDescriptor>,
        asset_server: Res<AssetServer>, // The AssetServer resource
                                        // AssetServer 资源
    ) {

AssetServer资源允许从assets文件夹加载文件。

现在,我们可以在函数的开头加载资源并检索句柄:

// lib.rs
// ..
    let font = asset_server.load("fonts/pixeled.ttf");
    let bomb_image = asset_server.load("sprites/bomb.png");
// ..

组件声明

我们的图块地图知道哪个图块是炸弹、炸弹邻居或空的,但 ECS 不知道。
让我们在 board_plugin 的组件中声明我们将附加到瓦片实体的组件,与我们的 Coordinates 组件一起:

  • board_plugin/src/components/bomb.rs
  • board_plugin/src/components/bomb_neighbor.rs
  • board_plugin/src/components/uncover.rs
// board_plugin/src/components/mod.rs
pub use bomb::Bomb;
pub use bomb_neighbor::BombNeighbor;
pub use uncover::Uncover;

mod bomb;
mod bomb_neighbor;
mod uncover;

炸弹

该组件会将图块识别为炸弹

// bomb.rs
use bevy::prelude::Component;

/// Bomb component
/// 炸弹组件
#[cfg_attr(feature = "debug", derive(bevy_inspector_egui::Inspectable))]
#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Component)]
pub struct Bomb;

炸弹邻居

该组件将识别一个方块为炸弹邻居

use bevy::prelude::Component;

/// Bomb neighbor component
/// 炸弹邻居组件
#[cfg_attr(feature = "debug", derive(bevy_inspector_egui::Inspectable))]
#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Component)]
pub struct BombNeighbor {
    /// Number of neighbor bombs
    /// 邻近炸弹的数量
    pub count: u8,
}

揭开

该组件将识别要揭开的图块,我们将在第 6 部分中使用它

use bevy::prelude::Component;

/// Uncover component, indicates a covered tile that should be uncovered
/// Uncover 组件,表明一个应该被揭露的被覆盖的瓦片
#[cfg_attr(feature = "debug", derive(bevy_inspector_egui::Inspectable))]
#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Component)]
pub struct Uncover;

让我们在插件中注册调试检查器的组件:

// lib.rs
#[cfg(feature = "debug")]
use bevy_inspector_egui::RegisterInspectable;
use components::*;

impl Plugin for BoardPlugin {
    fn build(&self, app: &mut App) {
        // ..
        #[cfg(feature = "debug")]
        {
            // registering custom component to be able to edit it in inspector
            // 注册自定义组件以便能够在检查器中编辑它
            app.register_inspectable::<Coordinates>();
            app.register_inspectable::<BombNeighbor>();
            app.register_inspectable::<Bomb>();
            app.register_inspectable::<Uncover>();
        }
    }
}

组件使用

让我们为BoardPlugin创建一个函数,来为我们的炸弹计数器创建一个:Text2DBundle

/// Generates the bomb counter text 2D Bundle for a given value
/// 生成给定值的炸弹计数器文本 2D Bundle
fn bomb_count_text_bundle(count: u8, font: Handle<Font>, size: f32) -> Text2dBundle {
    // We retrieve the text and the correct color
    // 我们检索文本和正确的颜色
    let (text, color) = (
        count.to_string(),
        match count {
            1 => Color::WHITE,
            2 => Color::GREEN,
            3 => Color::YELLOW,
            4 => Color::ORANGE,
            _ => Color::PURPLE,
        },
    );
    // We generate a text bundle
    // 我们生成一个文本 bundle
    Text2dBundle {
        text: Text {
            sections: vec![TextSection {
                value: text,
                style: TextStyle {
                    color,
                    font,
                    font_size: size,
                },
            }],
            alignment: TextAlignment {
                vertical: VerticalAlign::Center,
                horizontal: HorizontalAlign::Center,
            },
        },
        transform: Transform::from_xyz(0., 0., 1.),
        ..Default::default()
    }
}

它接受参数:

  • count:要打印的相邻炸弹数量
  • fontHandle:我们字体的资产
  • size:文字大小

颜色是完全任意的。
再次,我们将Transform(平移z值设置为1,以便将文本打印在图块的顶部。

现在我们可以将图块生成循环移至一个独立的函数中:

// lib.rs
fn spawn_tiles(
        parent: &mut ChildBuilder,
        tile_map: &TileMap,
        size: f32,
        padding: f32,
        color: Color,
        bomb_image: Handle<Image>,
        font: Handle<Font>,
    ) {
        // Tiles  图块
        for (y, line) in tile_map.iter().enumerate() {
            for (x, tile) in line.iter().enumerate() {
                let coordinates = Coordinates {
                  x: x as u16,
                  y: y as u16,
                };
                let mut cmd = parent.spawn();
                cmd.insert_bundle(SpriteBundle {
                    sprite: Sprite {
                        color,
                        custom_size: Some(Vec2::splat(size - padding)),
                        ..Default::default()
                    },
                    transform: Transform::from_xyz(
                        (x as f32 * size) + (size / 2.),
                        (y as f32 * size) + (size / 2.),
                        1.,
                    ),
                    ..Default::default()
                })
                .insert(Name::new(format!("Tile ({}, {})", x, y)))
                .insert(coordinates);
            }
        }
    }
  • 请注意,我们现在为实体构建器使用cmd临时值*

我们现在可以在 create_board 启动系统中调用它,在生成背景后的 with_children 块中:

// lib.rs
// ..
    Self::spawn_tiles(
        parent,
        &tile_map,
        tile_size,
        options.tile_padding,
        Color::GRAY,
        bomb_image,
        font,
    );

然后我们完成我们的 spawn_tiles 函数,添加我们的炸弹精灵和计数器文本进入双循环里面:

// lib.rs
use resources::tile::Tile;

// ..
    match tile {
                    // If the tile is a bomb we add the matching component and a sprite child
                      // 如果图块是炸弹,我们添加匹配的组件和一个精灵子对象
                    Tile::Bomb => {
                        cmd.insert(Bomb);
                        cmd.with_children(|parent| {
                            parent.spawn_bundle(SpriteBundle {
                                sprite: Sprite {
                                    custom_size: Some(Vec2::splat(size - padding)),
                                    ..Default::default()
                                },
                                transform: Transform::from_xyz(0., 0., 1.),
                                texture: bomb_image.clone(),
                                ..Default::default()
                            });
                        });
                    }
                    // If the tile is a bomb neighbour we add the matching component and a text child
                    // 如果图块是炸弹邻居,我们添加匹配的组件和一个文本子对象
                    Tile::BombNeighbor(v) => {
                        cmd.insert(BombNeighbor { count: *v });
                        cmd.with_children(|parent| {
                            parent.spawn_bundle(Self::bomb_count_text_bundle(
                                *v,
                                font.clone(),
                                size - padding,
                            ));
                        });
                    }
                    Tile::Empty => (),
                }
// ..

到目前为止,所有图块都具有相同的组件,例如CoordinatesTransformSprite等。
但现在有些图块具有:

  • 一个Bomb组件和一个带有炸弹精灵的子实体
  • 一个BombNeighbor组件和一个带有计数器文本的子实体

我们给炸弹精灵添加了纹理,其他的呢?

默认情况下,如果SpriteBundle没有指定texture,则使用白色方形纹理。在第 9 部分中,我们将更详细地了解它。

让我们运行我们的应用程序,并获得漂亮的棋盘:

木板


6 Bevy 扫雷:输入管理

我们有一个漂亮的面板,但我们无法与它交互,让我们处理一些输入!

界限

为了检测主板内的鼠标输入,我们将使用名为 的常见游戏开发类型Bounds。奇怪的是,bevy 中缺少它,因此我们将为我们的插件编写一个简单版本board_plugin/src/bounds.rs

// bounds.rs
use bevy::prelude::Vec2;

#[derive(Debug, Copy, Clone)]
pub struct Bounds2 {
    pub position: Vec2,
    pub size: Vec2,
}

impl Bounds2 {
    pub fn in_bounds(&self, coords: Vec2) -> bool {
        coords.x >= self.position.x
            && coords.y >= self.position.y
            && coords.x <= self.position.x + self.size.x
            && coords.y <= self.position.y + self.size.y
    }
}

该结构定义了一个 2D 矩形,可以检查坐标是否包含在其范围内。

将文件连接到board_plugin/src/lib.rs

// lib.rs
mod bounds;

董事会资源

我们在启动系统中生成的瓦片地图create_board在系统启动后就会丢失,我们需要将其放入资源中以使其持续存在。
我们还需要存储我们的板Bounds以进行输入检测。

让我们board.rsresources文件夹中创建一个:

// mod.rs
// ..
pub use board_options::*;

mod board;
// board.rs
use crate::bounds::Bounds2;
use crate::{Coordinates, TileMap};
use bevy::prelude::*;

#[derive(Debug)]
pub struct Board {
    pub tile_map: TileMap,
    pub bounds: Bounds2,
    pub tile_size: f32,
}

impl Board {
    /// Translates a mouse position to board coordinates
    pub fn mouse_position(&self, window: &Window, position: Vec2) -> Option<Coordinates> {
        // Window to world space
        let window_size = Vec2::new(window.width(), window.height());
        let position = position - window_size / 2.;

        // Bounds check
        if !self.bounds.in_bounds(position) {
            return None;
        }
        // World space to board space
        let coordinates = position - self.bounds.position;
        Some(Coordinates {
            x: (coordinates.x / self.tile_size) as u16,
            y: (coordinates.y / self.tile_size) as u16,
        })
    }
}

我们的Board资源存储 a TileMap、棋盘Bounds和 a tile_size,其中 a 是各个方形瓷砖的大小。

我们提供了一种将鼠标位置转换为我们自己的坐标系的方法。这种计算看起来很奇怪,因为与我们的实体世界空间的原点
位于屏幕中心(基于相机位置)不同,窗口空间原点位于左下角。

因此,我们必须转换鼠标位置,使其与我们的世界空间相匹配,检查边界,然后将坐标转换为图块坐标。

create_board现在我们定义了我们的资源,我们需要在启动系统的末尾注册它

// lib.rs
use bounds::Bounds2;
use resources::Board;
use bevy::math::Vec3Swizzles;

// ..

// We add the main resource of the game, the board
        commands.insert_resource(Board {
            tile_map,
            bounds: Bounds2 {
                position: board_position.xy(),
                size: board_size,
            },
            tile_size,
        });
// ..

现在可Board用于任何系统

输入系统

我们现在可以创建或第一个常规系统,它将检查每一帧是否有鼠标单击事件。

让我们systems使用文件在板插件中创建一个模块input.rs

小层次结构回顾:

├── Cargo.lock
├── Cargo.toml
├── assets
├── board_plugin
│    ├── Cargo.toml
│    └── src
│         ├── bounds.rs
│         ├── components
│         │    ├── bomb.rs
│         │    ├── bomb_neighbor.rs
│         │    ├── coordinates.rs
│         │    ├── mod.rs
│         │    ├── uncover.rs
│         ├── lib.rs
│         ├── resources
│         │    ├── board.rs
│         │    ├── board_options.rs
│         │    ├── mod.rs
│         │    ├── tile.rs
│         │    └── tile_map.rs
│         └── systems
│              ├── input.rs
│              └── mod.rs
├── src
│    └── main.rs

不要忘记连接systems模块lib.rs

mod systems;

input模块systems/mod.rs

pub mod input;

让我们定义我们的输入系统!

// input.rs
use crate::Board;
use bevy::input::{mouse::MouseButtonInput, ElementState};
use bevy::log;
use bevy::prelude::*;

pub fn input_handling(
    windows: Res<Windows>,
    board: Res<Board>,
    mut button_evr: EventReader<MouseButtonInput>,
) {
    let window = windows.get_primary().unwrap();

    for event in button_evr.iter() {
        if let ElementState::Pressed = event.state {
            let position = window.cursor_position();
            if let Some(pos) = position {
                log::trace!("Mouse button pressed: {:?} at {}", event.button, pos);
                let tile_coordinates = board.mouse_position(window, pos);
                if let Some(coordinates) = tile_coordinates {
                    match event.button {
                        MouseButton::Left => {
                            log::info!("Trying to uncover tile on {}", coordinates);
                            // TODO: generate an event
                        }
                        MouseButton::Right => {
                            log::info!("Trying to mark tile on {}", coordinates);
                            // TODO: generate an event
                        }
                        _ => (),
                    }
                }
            }
        }
    }
}

这个函数是我们的输入系统,它需要三个参数:

  • 一种Windows资源
  • 我们自己的Board资源
  • MouseButtonInput事件读取器

我们迭代事件读取器来检索每个事件,仅保留Pressed事件。
我们检索鼠标位置并使用我们的Board将鼠标位置转换为图块坐标
,然后根据鼠标按钮记录操作(发现或标记)。

那么如果我们按下其他按钮我们仍然会执行转换吗?

是的,我们可以先检查按钮以进行一些优化,但这会使教程中的代码不太清晰。

我们现在可以在我们的方法中注册我们的系统BoardPlugin::build()

// lib.rs
// ..
//    app.add_startup_system(Self::create_board)
        .add_system(systems::input::input_handling);
// ..

运行应用程序,您现在可以使用窗口上的左键和右键单击按钮,并注意:

  • 如果您单击棋盘,它会记录坐标和操作
  • 如果您单击板外或使用其他按钮,则不会发生任何情况

7Bevy 扫雷:发现瓷砖

为了覆盖我们的图块,我们只需为每个带有精灵的图块添加一个子实体,隐藏下面的内容。
我们将使用第 4 部分Uncover中设置的组件来进行揭露。

董事会资源

为了揭开图块,我们需要存储对每个覆盖实体的引用,让我们编辑我们的Board资源:

// board.rs
use bevy::utils::HashMap;

#[derive(Debug)]
pub struct Board {
    // ..
    pub covered_tiles: HashMap<Coordinates, Entity>,
}

impl Board {
    // ..

    /// Retrieves a covered tile entity
    pub fn tile_to_uncover(&self, coords: &Coordinates) -> Option<&Entity> {
      self.covered_tiles.get(coords)
    }

    /// We try to uncover a tile, returning the entity
    pub fn try_uncover_tile(&mut self, coords: &Coordinates) -> Option<Entity> {
        self.covered_tiles.remove(coords)
    }

    /// We retrieve the adjacent covered tile entities of `coord`
    pub fn adjacent_covered_tiles(&self, coord: Coordinates) -> Vec<Entity> {
        self.tile_map
            .safe_square_at(coord)
            .filter_map(|c| self.covered_tiles.get(&c))
            .copied()
            .collect()
    }
}>

HashMap我们将使用包含实体的地图,而不是制作一些复杂的新图块地图。Entity作为一个简单的标识符实现CopyClone,因此可以安全地复制和存储。
每次我们发现一个图块时,我们都会从地图中删除该实体。

我们提供三种方法:

  • tile_to_uncover在某些图块坐标处检索被覆盖的图块实体
  • try_uncover_tile如果给定坐标处存在被覆盖的图块实体,则从地图中删除该实体
  • adjacent_covered_tiles它允许检索某些坐标周围的正方形中所有被覆盖的图块。

瓷砖盖

我们编辑spawn_tiles函数以添加以下参数:

// lib.rs
use bevy::utils::HashMap;

// ..
    fn spawn_tiles(
        // ..
        covered_tile_color: Color,
        covered_tiles: &mut HashMap<Coordinates, Entity>,
    )
// ..

我们可以为每个图块添加图块覆盖创建:

// lib.rs

// ..
    // .insert(coordinates);
    // We add the cover sprites
    cmd.with_children(|parent| {
                    let entity = parent
                        .spawn_bundle(SpriteBundle {
                            sprite: Sprite {
                                custom_size: Some(Vec2::splat(size - padding)),
                                color: covered_tile_color,
                                ..Default::default()
                            },
                            transform: Transform::from_xyz(0., 0., 2.),
                            ..Default::default()
                        })
                        .insert(Name::new("Tile Cover"))
                        .id();
                    covered_tiles.insert(coordinates, entity);
                });
    // match tile {
//.. 

让我们setup_board相应地编辑我们的系统:

// lib.rs
use bevy::utils::{AHashExt, HashMap};

// ..
let mut covered_tiles =
            HashMap::with_capacity((tile_map.width() * tile_map.height()).into());
// ..
Self::spawn_tiles(
    //..
    Color::DARK_GRAY,
    &mut covered_tiles
);
// ..
commands.insert_resource(Board {
    //..
    covered_tiles,
})
// ..

现在,每个棋盘图块将有一个子“图块盖”实体,并有一个精灵隐藏它。

活动

如上一部分所示,我们希望在单击图块时发送一个事件。
事件就像资源但可用于 1 帧。(查看更多活动信息

让我们events.rs为我们创建一个模块board_plugin

// board_plugin/src/events.rs
use crate::components::Coordinates;

#[derive(Debug, Copy, Clone)]
pub struct TileTriggerEvent(pub Coordinates);
// board_plugin/src/lib.rs
mod events;

就像组件资源一样,事件可以是任何 Rust 类型。
在这里,我们选择包含要揭开的图块的棋盘坐标的事件。

系统

输入

让我们编辑我们的系统并发送左键单击input_handling的新事件:

// input.rs
+ use crate::events::TileTriggerEvent;

pub fn input_handling(
    // ..
+   mut tile_trigger_ewr: EventWriter<TileTriggerEvent>,
) {
    // ..
    // log::info!("Trying to uncover tile on {}", coordinates);
-   // TODO: generate an event
+   tile_trigger_ewr.send(TileTriggerEvent(coordinates));
    // ..
}

EventWriter我们为新事件添加一个新参数,并用发送代码替换TODO 。
现在,每次我们在板上使用鼠标左键时,TileTriggerEvent都会发送一个。

揭露

触发事件处理程序

我们现在可以创建一个监听新事件的系统。让我们uncover.rs在模块中创建一个文件systems

// systems/mod.rs
pub mod uncover;
// systems/uncover.rs
use bevy::prelude::*;
use crate::{Board, Bomb, BombNeighbor, Coordinates, Uncover};
use crate::events::TileTriggerEvent;

pub fn trigger_event_handler(
    mut commands: Commands,
    board: Res<Board>,
    mut tile_trigger_evr: EventReader<TileTriggerEvent>,
) {
    for trigger_event in tile_trigger_evr.iter() {
        if let Some(entity) = board.tile_to_uncover(&trigger_event.0) {
            commands.entity(*entity).insert(Uncover);
        }
    }
}

就像我们的输入系统一样,我们迭代TileTriggerEvent事件。
对于每个事件,我们检查图块是否被覆盖,如果是,我们Uncover向其添加一个组件。

揭开瓷砖

现在让我们使用这个组件创建另一个系统Uncover

// uncover.rs

pub fn uncover_tiles(
    mut commands: Commands,
    mut board: ResMut<Board>,
    children: Query<(Entity, &Parent), With<Uncover>>,
    parents: Query<(&Coordinates, Option<&Bomb>, Option<&BombNeighbor>)>,
) {

}

我们的第一个查询

论据:

  • commands,与通常的实体操作一样
  • board我们的Board资源,但具有可变访问权限 ( ResMut)
  • Query<(Entity, &Parent), With<Uncover>>:我们查询EntityParent来查找具有组件的每个实体Uncover
  • Query<(&Coordinates, Option<&Bomb>, Option<&BombNeighbor>)>:我们查询每个Coordinate组件,也许Bomb还有BombNeighbor组件。

有两种方法可以从查询中获取数据:迭代数据或从指定实体获取查询的组件。(查看更多有关查询的信息

让我们迭代一下children查询:

// uncover.rs
// ..
    // We iterate through tile covers to uncover
    for (entity, parent) in children.iter() {
        // we destroy the tile cover entity
        commands
            .entity(entity)
            .despawn_recursive();
    }

我们从查询中获取每个Entity瓷砖封面和(父实体)。 我们从中检索瓷砖覆盖实体命令并销毁该实体Parent
Commands

为什么despawn_recursive

此方法还将消除潜在的子实体,并将取消图块盖与板图块实体的链接。

好的,我们销毁了触发的图块盖,但我们需要检查棋盘图块是否确实被触发,并获取其坐标。
让我们从第二个查询中获取父组件:

// uncover.rs
use bevy::log;

// ..
    let (coords, bomb, bomb_counter) = match parents.get(parent.0) {
        Ok(v) => v,
        Err(e) => {
            log::error!("{}", e);
            continue;
        }
    };

我们现在拥有板图块(图块盖父级)Coordinates组件以及两个Option<>其潜力BombBombNeighbor组件。

让我们完成我们的功能:

// uncover.rs
// ..
    // We remove the entity from the board covered tile map
    match board.try_uncover_tile(coords) {
        None => log::debug!("Tried to uncover an already uncovered tile"),
        Some(e) => log::debug!("Uncovered tile {} (entity: {:?})", coords, e),
    }
    if bomb.is_some() {
        log::info!("Boom !");
        // TODO: Add explosion event
    }
    // If the tile is empty..
    else if bomb_counter.is_none() {
        // .. We propagate the uncovering by adding the `Uncover` component to adjacent tiles
        // which will then be removed next frame
        for entity in board.adjacent_covered_tiles(*coords) {
            commands.entity(entity).insert(Uncover);
        }
    }

由于我们取消了触发的覆盖图块实体,因此我们需要将其从Board调用中删除try_uncover_tile
我们还检查棋盘是否是炸弹并记录Boom !

如果母板图块既不是炸弹也不是炸弹邻居Uncover,则最后一部分将向相邻图块盖插入新组件。 此操作会将揭开过程传播到下一帧的相邻覆盖图块。 这不一定是最优的,但它会延迟计算
 

剩下的就是将我们的新系统和我们的活动注册到我们AppBoardPlugin

// lib.rs
use crate::events::*;

// ..
//   .add_system(systems::input::input_handling)
    .add_system(systems::uncover::trigger_event_handler)
    .add_system(systems::uncover::uncover_tiles)
    .add_event::<TileTriggerEvent>();
//..

让我们运行我们的应用程序!

动图

揭开 GIF


8 Bevy 扫雷:安全开始

我们可以发现瓷砖,但是好的扫雷器应该提供一个安全的起始未覆盖区域。
让我们激活BoardOptions我们的参数main.rs

// main.rs
// ..
    .insert_resource(BoardOptions {
        // ..
+        safe_start: true,
        // ..
    }
// ..

这个选项现在什么也不做,我们需要在我们的board_plugin.
我们想要找到一块瓷砖并安排其瓷砖盖进行揭开,因为我们已经有了揭开系统,我们只需Uncover在其上插入一个组件即可。

我们需要检索电流covered_tile_entity才能发现它。
由于我们只想要一个安全的开始,因此我们将再次为我们的spawn_tiles函数添加一个新参数。

Clippy 已经说它已经太多了!

我们将在第 9 部分重构它,让我们开始工作:

// lib.rs
// ..
fn spawn_tiles(
        // ..
+        safe_start_entity: &mut Option<Entity>,
    ) {
        // ..
        covered_tiles.insert(coordinates, entity);
+       if safe_start_entity.is_none() && *tile == Tile::Empty {
+         *safe_start_entity = Some(entity);
+       }
}

现在我们setup_board相应地改变我们的启动系统:

// lib.rs
// ..
+ let mut safe_start = None;
commands
     .spawn()
     .insert(Name::new("Board"))
// ..
Self::spawn_tiles(
    //..
+     &mut safe_start,
);
// ..
+ if options.safe_start {
+    if let Some(entity) = safe_start {
+        commands.entity(entity).insert(Uncover);
+    }
+ }
// ..

就是这样!如果我们运行我们的应用程序,它将发现一个安全区域!

安全启动

我们得到一个预先发现的区域


9Bevy 扫雷:通用状态

我们的板插件可以通过 自定义BoardOptions,但是 pour 应用程序无法与其交互。
我们需要使我们的BoardPlugin通用化,以允许通过状态进行控制。

插入

让我们编辑我们的插件结构:

// lib.rs
use bevy::ecs::schedule::StateData;

pub struct BoardPlugin<T> {
    pub running_state: T,
}

impl<T: StateData> Plugin for BoardPlugin<T> {
    fn build(&self, app: &mut App) {
        // ..
    }
}

impl<T> BoardPlugin<T> {
    // ..
}

我们的插件无法知道使用它的应用程序定义了什么状态,它需要是通用的。

我们现在可以改变我们的系统结构以考虑到这一点running_state

// lib.rs
// ..
fn build(&self, app: &mut App) {
    // When the running states comes into the stack we load a board
        app.add_system_set(
            SystemSet::on_enter(self.running_state.clone()).with_system(Self::create_board),
        )
        // We handle input and trigger events only if the state is active
        .add_system_set(
            SystemSet::on_update(self.running_state.clone())
                .with_system(systems::input::input_handling)
                .with_system(systems::uncover::trigger_event_handler),
        )
        // We handle uncovering even if the state is inactive
        .add_system_set(
            SystemSet::on_in_stack_update(self.running_state.clone())
                .with_system(systems::uncover::uncover_tiles),
        )
        .add_event::<TileTriggerEvent>();
}

Bevy 的状态位于堆栈中:

  • 如果某个状态位于堆栈顶部,则该状态被视为活动状态
  • 如果某个状态位于堆栈中但不在顶部,则认为该状态处于非活动状态
  • 如果某个状态离开堆栈,则认为已退出
  • 如果一个状态进入堆栈,则被视为已进入

那么我们在这里做了什么:

  • 由于我们现在用状态条件来约束系统,所以一切都是SystemSet
  • 当我们的状态进入堆栈时,startup_system我们调用我们的系统,而不是 asetup_board
  • 我们仅在状态处于活动状态时处理输入和触发事件(我们允许暂停状态)
  • 揭示系统不应该暂停,因此如果状态在堆栈中(无论是否活动) ,我们都会运行它。

通过此配置,使用插件的应用程序可以具有菜单或其他内容,并使用running_state.
但我们需要能够清理棋盘,以防状态退出堆栈。

为此,Board资源应该有一个对其自身的引用,Entity以使其与其所有子资源一起消失:

// board.rs
// ..
#[derive(Debug)]
pub struct Board {
    pub tile_map: TileMap,
    pub bounds: Bounds2,
    pub tile_size: f32,
    pub covered_tiles: HashMap<Coordinates, Entity>,
+    pub entity: Entity,
}
// ..

让我们编辑我们的create_board系统来检索实体:

// lib.rs
fn create_board(
    // ..
) {
    // .. 
    let board_entity = commands
    //        .spawn()
    //        .insert(Name::new("Board"))
    // ..
    .id();
    // ..
    commands.insert_resource(Board {
            // ..
            entity: board_entity,
        })

}

现在我们可以为我们的插件注册一个清洁系统:

// lib.rs

// ..
fn build(&self, app: &mut App) {
    //..
    .add_system_set(
        SystemSet::on_exit(self.running_state.clone())
            .with_system(Self::cleanup_board),
    )
    // ..
}

impl<T> BoardPlugin<T> {
    // ..
    fn cleanup_board(board: Res<Board>, mut commands: Commands) {
        commands.entity(board.entity).despawn_recursive();
        commands.remove_resource::<Board>();
    }
}

所有的图块、文本、精灵、封面等怎么样?

由于我们将每个棋盘实体作为我们的子实体生成board_entity,因此使用despawn_recursive也会取消其子实体:

  • 的背景
  • 瓷砖
  • 平铺文字
  • 瓷砖精灵
  • 瓷砖盖
  • ETC。

应用程序

让我们定义一些基本状态:

// main.rs
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub enum AppState {
    InGame,
    Out,
}

fn main() {
    // ..
    .add_state(AppState::InGame)
    .add_plugin(BoardPlugin {
        running_state: AppState::InGame,
    })
}

如果我们现在运行应用程序,没有任何变化,但如果我们编辑状态,我们可以完全控制我们的棋盘系统:

// main.rs
use bevy::log;

fn main() {
    // ..
    // State handling
    .add_system(state_handler);
    // ..
}

fn state_handler(mut state: ResMut<State<AppState>>, keys: Res<Input<KeyCode>>) {
    if keys.just_pressed(KeyCode::C) {
        log::debug!("clearing detected");
        if state.current() == &AppState::InGame {
            log::info!("clearing game");
            state.set(AppState::Out).unwrap();
        }
    }
    if keys.just_pressed(KeyCode::G) {
        log::debug!("loading detected");
        if state.current() == &AppState::Out {
            log::info!("loading game");
            state.set(AppState::InGame).unwrap();
        }
    }
}

这里的一切都应该是熟悉的,

  • state被包装在 中ResMut<>,因为状态的处理方式与任何资源一样,但有一个额外的包装器:State<>
  • keys是一个Input<>参数,允许使用检查键盘交互(它可以与鼠标交互KeyCode一起使用)MouseButton

现在按C应该完全清理棋盘,按G应该生成一个新棋盘。

锻炼

状态可能很棘手,因此最好练习使用它。

实现以下功能:

  1. 当我按Escape时,游戏暂停,并且我无法与棋盘交互,如果我再次按Escape ,游戏将恢复。
  2. 当我按G时,会生成一个新板,而不必先按C。

请在 Twitter 上@ManevilleF告诉我你的答案


10Bevy 扫雷:资产

我们的BoardOptions资源具有出色的电路板配置,但我们对每种颜色、纹理和字体进行了硬编码。
让我们为board_assets.rs我们的:创建一个新的配置资源board_plugin

// board_assets.rs
use bevy::prelude::*;
use bevy::render::texture::DEFAULT_IMAGE_HANDLE;

/// Material of a `Sprite` with a texture and color
#[derive(Debug, Clone)]
pub struct SpriteMaterial {
    pub color: Color,
    pub texture: Handle<Image>,
}

impl Default for SpriteMaterial {
    fn default() -> Self {
        Self {
            color: Color::WHITE,
            texture: DEFAULT_IMAGE_HANDLE.typed(),
        }
    }
}

/// Assets for the board. Must be used as a resource.
///
/// Use the loader for partial setup
#[derive(Debug, Clone)]
pub struct BoardAssets {
    /// Label
    pub label: String,
    ///
    pub board_material: SpriteMaterial,
    ///
    pub tile_material: SpriteMaterial,
    ///
    pub covered_tile_material: SpriteMaterial,
    ///
    pub bomb_counter_font: Handle<Font>,
    ///
    pub bomb_counter_colors: Vec<Color>,
    ///
    pub flag_material: SpriteMaterial,
    ///
    pub bomb_material: SpriteMaterial,
}

impl BoardAssets {
    /// Default bomb counter color set
    pub fn default_colors() -> Vec<Color> {
        vec![
            Color::WHITE,
            Color::GREEN,
            Color::YELLOW,
            Color::ORANGE,
            Color::PURPLE,
        ]
    }

    /// Safely retrieves the color matching a bomb counter
    pub fn bomb_counter_color(&self, counter: u8) -> Color {
        let counter = counter.saturating_sub(1) as usize;
        match self.bomb_counter_colors.get(counter) {
            Some(c) => *c,
            None => match self.bomb_counter_colors.last() {
                None => Color::WHITE,
                Some(c) => *c,
            },
        }
    }
}

在以下位置声明模块resources/mod.rs

// mod.rs
// ..
pub use board_assets::*;
mod board_assets;

这个新资源将存储我们需要的所有视觉数据并允许定制。

我们还添加了一个bomb_counter_colors字段来自定义炸弹邻居文本颜色,并创建了一个实用bomb_counter_color方法来检索它。

这个DEFAULT_IMAGE_HANDLE常数值是多少?

我们使用与白色纹理SpriteBundle相同的硬编码来复制处理其默认纹理的方式。 现在我们可以选择从图块到棋盘背景的所有内容的自定义纹理,我们将启用我们省略的每个字段。Handle<Image>
texture

插入

create_board让我们在我们的系统中使用现在的资源board_plugin

// lib.rs
+ use resources::BoardAssets;
// ..

    pub fn create_board(
        mut commands: Commands,
        board_options: Option<Res<BoardOptions>>,
+       board_assets: Res<BoardAssets>,
        window: Res<WindowDescriptor>,
-       asset_server: Res<AssetServer>,
    ) {
        // ..
-     let font = asset_server.load("fonts/pixeled.ttf");
-     let bomb_image = asset_server.load("sprites/bomb.png");
      // ..

      // Board background sprite:
      parent
                    .spawn_bundle(SpriteBundle {
                        sprite: Sprite {
-                            color: Color::WHITE
+                            color: board_assets.board_material.color,
                            custom_size: Some(board_size),
                            ..Default::default()
                        },
+                       texture: board_assets.board_material.texture.clone(),
                        transform: Transform::from_xyz(board_size.x / 2., board_size.y / 2., 0.),
                        ..Default::default()
                    })
        // ..
        Self::spawn_tiles(
                    parent,
                    &tile_map,
                    tile_size,
                    options.tile_padding,
-                   Color::GRAY,
-                   bomb_image,
-                   font,
-                   Color::DARK_GRAY,
+                   &board_assets,
                    &mut covered_tiles,
                    &mut safe_start,
                );
        // ..
    }

我们删除这个asset_server参数。

为什么不是board_assets可选的?

使其成为可选字体并不容易,因为 bevy 不提供默认字体Handle。它需要高级的引擎操作,例如使用FromWorldAssets实现以及使用硬编码字体或字体路径。

Handle实现了Default

确实如此,但是应用程序在尝试打印文本时会出现恐慌,或者什么也不会显示。


我们的spawn_tilesbomb_count_text_bundle函数也应该被清理:

// lib.rs

fn spawn_tiles(
        parent: &mut ChildBuilder,
        tile_map: &TileMap,
        size: f32,
        padding: f32,
-       color: Color,
-       bomb_image: Handle<Image>,
-       font: Handle<Font>,
-       covered_tile_color: Color,
+       board_assets: &BoardAssets,
        covered_tiles: &mut HashMap<Coordinates, Entity>,
        safe_start_entity: &mut Option<Entity>,
    ) {
        // ..
        // Tile sprite
        cmd.insert_bundle(SpriteBundle {
                    sprite: Sprite {
-                       color
+                       color: board_assets.tile_material.color,
                        custom_size: Some(Vec2::splat(size - padding)),
                        ..Default::default()
                    },
                    transform: Transform::from_xyz(
                        (x as f32 * size) + (size / 2.),
                        (y as f32 * size) + (size / 2.),
                        1.,
                    ),
+                   texture: board_assets.tile_material.texture.clone(),
                    ..Default::default()
                })
                // ..
                // Tile Cover
           let entity = parent
                        .spawn_bundle(SpriteBundle {
                            sprite: Sprite {
                                custom_size: Some(Vec2::splat(size - padding)),
-                               color: covered_tile_color,
+                               color: board_assets.covered_tile_material.color,
                                ..Default::default()
                            },
+                           texture: board_assets.covered_tile_material.texture.clone(),
                            transform: Transform::from_xyz(0., 0., 2.),
                            ..Default::default()
                        })
                        .insert(Name::new("Tile Cover"))
                        .id();
                // ..
                // Bomb neighbor text
                parent.spawn_bundle(Self::bomb_count_text_bundle(
                                *v,
-                               font.clone(),
+                               board_assets,
                                size - padding,
                            ));
}

fn bomb_count_text_bundle(
        count: u8,
-       font: Handle<Font>,        
+       board_assets: &BoardAssets,
        size: f32,
    ) -> Text2dBundle {
        // We retrieve the text and the correct color
-       let (text, color) = (
-           count.to_string(),
-           match count {
-               1 => Color::WHITE,
-               2 => Color::GREEN,
-               3 => Color::YELLOW,
-               4 => Color::ORANGE,
-               _ => Color::PURPLE,
-           },
-       );
+       let color = board_assets.bomb_counter_color(count);
        // We generate a text bundle
        Text2dBundle {
            text: Text {
                sections: vec![TextSection {
-                   value: text,
+                   value: count.to_string(),
                    style: TextStyle {
                        color,
-                       font,
+                       font: board_assets.bomb_counter_font.clone(),
                        font_size: size,
                    },
                }],
     // ..           

现在,我们仅将我们的BoardAssets资源用于董事会的每个视觉元素。

应用程序

我们需要设置BoardAssets资源,但有一个问题。加载我们的资源必须在一个系统中,这里是一个启动系统,但是我们需要在我们的插件启动它的系统之前完成它setup_board,否则它会崩溃。

因此,让我们通过将状态设置为来防止这种情况Out

// main.rs

fn main() {
    // ..
-   .add_state(AppState::InGame)
+   .add_state(AppState::Out)
    // ..
}

并注册一个setup_board启动系统,并将之前的板设置移入其中

// main.rs

fn main() {
    // ..
-   app.insert_resource(BoardOptions {
-       map_size: (20, 20),
-       bomb_count: 40,
-       tile_padding: 3.0,
-       safe_start: true,
-       ..Default::default()
-   })
    // ..
+    .add_startup_system(setup_board)
    // ..
}

我们可以声明新系统:

// main.rs
use board_plugin::resources::{BoardAssets, SpriteMaterial};

// ..
fn setup_board(
    mut commands: Commands,
    mut state: ResMut<State<AppState>>,
    asset_server: Res<AssetServer>,
) {
    // Board plugin options
    commands.insert_resource(BoardOptions {
        map_size: (20, 20),
        bomb_count: 40,
        tile_padding: 1.,
        safe_start: true,
        ..Default::default()
    });
    // Board assets
    commands.insert_resource(BoardAssets {
        label: "Default".to_string(),
        board_material: SpriteMaterial {
            color: Color::WHITE,
            ..Default::default()
        },
        tile_material: SpriteMaterial {
            color: Color::DARK_GRAY,
            ..Default::default()
        },
        covered_tile_material: SpriteMaterial {
            color: Color::GRAY,
            ..Default::default()
        },
        bomb_counter_font: asset_server.load("fonts/pixeled.ttf"),
        bomb_counter_colors: BoardAssets::default_colors(),
        flag_material: SpriteMaterial {
            texture: asset_server.load("sprites/flag.png"),
            color: Color::WHITE,
        },
        bomb_material: SpriteMaterial {
            texture: asset_server.load("sprites/bomb.png"),
            color: Color::WHITE,
        },
    });
    // Plugin activation
    state.set(AppState::InGame).unwrap();
}

使用我们在上一部分中设置的通用状态系统,我们可以控制插件何时启动。
在这里,我们希望它在加载资产并设置资源BoardAssets启动。
这就是为什么我们首先将状态设置为,并在资产准备好后将Out其设置为。InGame

我们的插件现在完全模块化,硬编码值为零,从板尺寸到图块颜色的所有内容都可以定制。

我们可以在运行时编辑主题吗?

是的 !该BoardAssets资源可供每个系统使用,
但所有不可用的资源Handle直到下一代才会应用。对于更动态的系统,您可以检查我的插件bevy_sprite_material


11 Bevy 扫雷:标记方块

我们的扫雷插件已经快完成了,但我们仍然错过了一个非常重要的功能:标记图块。
此外,您可能会注意到该应用程序非常慢,尤其是对于大型图块地图。让我们先解决这个问题。

优化

在我们的应用程序中添加以下优化级别Cargo.toml

# Enable optimizations for dependencies (incl. Bevy), but not for our code:
[profile.dev.package."*"]
opt-level = 3

# Maybe also enable only a small amount of optimization for our code:
[profile.dev]
opt-level = 1

这不会实现与release构建一样多的优化,但您应该注意到显着的改进

活动

为了完成游戏,我们需要在我们的board_plugin/src/events.rs.

董事会完成事件

此事件将在棋盘完成时发送,允许使用我们的插件的应用程序检测胜利并可能触发一些元素(胜利屏幕、得分等)

#[derive(Debug, Copy, Clone)]
pub struct BoardCompletedEvent;

炸弹爆炸事件

每次玩家发现炸弹时都会发送此事件,允许使用我们的插件的应用程序检测到它。
我们的插件不会中断游戏本身,通过执行一个事件,我们允许应用程序在 3 个炸弹或立即触发损失。
这种方法比添加配置更加模块化。

#[derive(Debug, Copy, Clone)]
pub struct BombExplosionEvent;

瓷砖标记事件

此事件相当于TileTriggerEvent右键单击事件。

#[derive(Debug, Copy, Clone)]
pub struct TileMarkEvent(pub Coordinates);

资源

让我们编辑我们的Board资源来处理图块标记(标志):

// board.rs
use bevy::log;

#[derive(Debug)]
pub struct Board {
    // ..
    pub marked_tiles: Vec<Coordinates>,
}

impl Board {
    // ..

    /// Removes the `coords` from `marked_tiles`
    fn unmark_tile(&mut self, coords: &Coordinates) -> Option<Coordinates> {
        let pos = match self.marked_tiles.iter().position(|a| a == coords) {
            None => {
                log::error!("Failed to unmark tile at {}", coords);
                return None;
            }
            Some(p) => p,
        };
        Some(self.marked_tiles.remove(pos))
    }

    /// Is the board complete
    pub fn is_completed(&self) -> bool {
        self.tile_map.bomb_count() as usize == self.covered_tiles.len()
    }
}

我们增加:

  • 存储各种标记坐标的新字段
  • 检查董事会完成情况的公共方法。
  • 从新字段中删除坐标的私有方法

让我们编辑该tile_to_uncover方法并检查标记的图块:

// board.rs
pub fn tile_to_uncover(&self, coords: &Coordinates) -> Option<&Entity> {
+   if self.marked_tiles.contains(coords) {
+       None
+   } else {
        self.covered_tiles.get(coords)
+   }
}

我们不直接返回被覆盖的图块实体,而是检查图块是否被标记。这将防止通过单击标记的图块来发现它。

让我们更改try_uncover_tile方法以删除发现时标记的坐标。

// board.rs
pub fn try_uncover_tile(&mut self, coords: &Coordinates) -> Option<Entity> {
 +   if self.marked_tiles.contains(coords) {
 +      self.unmark_tile(coords)?;
 +  }
    self.covered_tiles.remove(coords)
}

我们现在可以创建该try_toggle_mark方法:

// board.rs

    /// We try to mark or unmark a tile, returning the entity and if the tile is marked
    pub fn try_toggle_mark(&mut self, coords: &Coordinates) -> Option<(Entity, bool)> {
        let entity = *self.covered_tiles.get(coords)?;
        let mark = if self.marked_tiles.contains(coords) {
            self.unmark_tile(coords)?;
            false
        } else {
            self.marked_tiles.push(*coords);
            true
        };
        Some((entity, mark))
    }

系统

揭露

让我们编辑我们的揭露系统来检查电路板是否完成。

我们还需要发送新的BombExplosionEvent.

// uncover.rs
+ use crate::{BoardCompletedEvent, BombExplosionEvent};

pub fn uncover_tiles(
    // ..
+   mut board_completed_event_wr: EventWriter<BoardCompletedEvent>,
+   mut bomb_explosion_event_wr: EventWriter<BombExplosionEvent>,
) {
    // match board.try_uncover_tile(coords) {}
    // ..
+   if board.is_completed() {
+       log::info!("Board completed");
+       board_completed_event_wr.send(BoardCompletedEvent);
+   }
    if bomb.is_some() {
        log::info!("Boom !");
-       // TODO: generate an event
+       bomb_explosion_event_wr.send(BombExplosionEvent);
    }
    //..
}

输入

让我们编辑我们的input_handling系统并发送我们的新事件以进行右键单击

// input.rs
+ use crate::TileMarkEvent;

pub fn input_handling(
    // ..
+   mut tile_mark_ewr: EventWriter<TileMarkEvent>,
) {
    // ..
-   // TODO: generate an event
+   tile_mark_ewr.send(TileMarkEvent(coordinates));
    // ..
}

标记

让我们在带有系统的mark插件中创建一个模块:systemsmark_tiles

// systems/mod.rs
pub mod mark;
// mark.rs
se crate::{Board, BoardAssets, TileMarkEvent};
use bevy::log;
use bevy::prelude::*;

pub fn mark_tiles(
    mut commands: Commands,
    mut board: ResMut<Board>,
    board_assets: Res<BoardAssets>,
    mut tile_mark_event_rdr: EventReader<TileMarkEvent>,
    query: Query<&Children>,
) {
    for event in tile_mark_event_rdr.iter() {
        if let Some((entity, mark)) = board.try_toggle_mark(&event.0) {
            if mark {
                commands.entity(entity).with_children(|parent| {
                    parent
                        .spawn_bundle(SpriteBundle {
                            texture: board_assets.flag_material.texture.clone(),
                            sprite: Sprite {
                                custom_size: Some(Vec2::splat(board.tile_size)),
                                color: board_assets.flag_material.color,
                                ..Default::default()
                            },
                            transform: Transform::from_xyz(0., 0., 1.),
                            ..Default::default()
                        })
                        .insert(Name::new("Flag"));
                });
            } else {
                let children = match query.get(entity) {
                    Ok(c) => c,
                    Err(e) => {
                        log::error!("Failed to retrieve flag entity components: {}", e);
                        continue;
                    }
                };
                for child in children.iter() {
                    commands.entity(*child).despawn_recursive();
                }
            }
        }
    }
}

我们有熟悉的参数,以及我们将用于图块覆盖Query实体(标志)的子级的新实体。Children

注意: 此查询可以使用新TileCover组件进行优化,因此避免查询每个具有子实体的实体

该函数迭代我们的TileMarkEvent阅读器,并尝试切换Board资源上的标记。
如果图块被标记,我们会使用 生成一个标志精灵,BoardAssets否则我们会消失所有图块覆盖子项。
我们可以存储实体引用或使用自定义Flag组件来避免这种硬性消失操作,但我们不会向图块覆盖实体添加任何其他子项。

插入

让我们注册我们的新事件和系统:

// lib.rs

impl<T: StateData> Plugin for BoardPlugin<T> {
    fn build(&self, app: &mut AppBuilder) {
        // ..
        // We handle uncovering even if the state is inactive
        .add_system_set(
            SystemSet::on_in_stack_update(self.running_state.clone())
                .with_system(systems::uncover::uncover_tiles)
+               .with_system(systems::mark::mark_tiles), // We add our new mark system
        )
        .add_system_set(
            SystemSet::on_exit(self.running_state.clone()).with_system(Self::cleanup_board),
        )
        .add_event::<TileTriggerEvent>()
+       .add_event::<TileMarkEvent>()
+       .add_event::<BombExplosionEvent>()
+       .add_event::<BoardCompletedEvent>();
    }
}

impl<T> Board<T> {
    fn setup_board(
        // ..
    ) {
        // ..
        commands.insert_resource(Board {
            // ..
+           marked_tiles: Vec::new(),
            // ..
        })

    }
}

就是这样 !我们有一个完整的扫雷插件!

游戏玩法


12 Bevy 扫雷:WASM 构建

让我们的项目在浏览器上运行:

让我们改变我们的board_plugin/Cargo.toml

[dependencies]
- # Engine
- bevy = "0.6"

# Serialization
serde = "1.0"

# Random
rand = "0.8"

# Console Debug
colored = { version = "2.0", optional = true }
# Hierarchy inspector debug
bevy-inspector-egui = { version = "0.8", optional = true }

+ # Engine
+ [dependencies.bevy]
+ version = "0.6"
+ default-features = false
+ features = ["render"]

+ # Dependencies for WASM only
+ [target.'cfg(target_arch = "wasm32")'.dependencies.getrandom]
+ version="0.2"
+ features=["js"]

我们只需要依赖项的一项功能,因此我们改进了它的声明,并且我们仅针对改进浏览器的目标bevy添加了新的依赖项。wasmrand

现在我们编辑 main Cargo.toml

[dependencies]
- bevy = "0.6"
board_plugin = { path = "board_plugin" }

# Hierarchy inspector debug
bevy-inspector-egui = { version = "0.8", optional = true }


+ [dependencies.bevy]
+ version = "0.6"
+ default-features = false
+ features = ["render", "bevy_winit", "png"]

+ # Dependencies for native only.
+ [target.'cfg(not(target_arch = "wasm32"))'.dependencies.bevy]
+ version = "0.6"
+ default-features = false
+ features = ["x11"]

[workspace]
members = [
    "board_plugin"
]

我们还可以禁用默认功能bevy,只启用有用的功能。我们添加的x11功能仅供本机使用,以避免 Web 程序集的编译问题。

而且..就是这样!该应用程序现在可以在 wasm 上本地编译和运行。

让我们通过添加一个货物配置来改进一下.cargo/config.toml

[target.wasm32-unknown-unknown]
runner = "wasm-server-runner"

[alias]
serve = "run --target wasm32-unknown-unknown"

然后安装运行器:
cargo install wasm-server-runner

您现在可以直接在浏览器上执行cargo serve和测试您的应用程序!
您也可以尝试现场版


Bevy 扫雷(12 部分系列)

(本节结束了,以上全是0.6版的内容)

(下一节是0.12.1版的内容)

  • 21
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值