前言#
昨天我们完成了核心部分
坏蛋Dan:rust基础学习--基于Bevy实现扫雷小游戏day6
今天我们来完善以及优化下
代码过多,建议直接拉github代码
git clone https://github.com/1714080902120/rust_mine_sweeper.git 完善and优化#
safe start#
现在我们的交互的时候点击第一个是有可能点击到炸弹的,按传统扫雷来说,这是正常的,不过我们可以加个选项用来避免这个问题,稍微提升下体验感。
回到main.rs中,我们在BoardOptions资源插入的时候新增一个字段
// main.rs
// ..
.insert_resource(BoardOptions {
// ..
+ safe_start: true,
// ..
}
// ..这个字段是之前就已经在BoardOptions结构体中定义好的了
不过现在还没有效果,因为我们没有实现相关的逻辑。
我们回到lib.rs中, 给spawn_lines新增一个参数safe_start_entity。
// lib.rs
// ...
impl BoardPlugin {
// ...
pub fn create_board(
// ...
) {
// ...
+ let mut safe_start = None;
// ...
Self::spawn_tiles(
// ...
+ &mut safe_start,
);
});
+ if options.safe_start {
+ if let Some(entity) = safe_start {
+ commands.entity(entity).insert(Uncover);
+ }
+ }
// ...
}
//...
fn spawn_tiles(
// ...
+ safe_start_entity: &mut Option<Entity>,
) {
// Tiles
for (y, line) in tile_map.iter().enumerate() {
for (x, tile) in line.iter().enumerate() {
// ...
cmd.with_children(|parent| {
// ...
covered_tiles.insert(coordinates, entity);
+ if safe_start_entity.is_none() && *tile == Tile::Empty {
+ *safe_start_entity = Some(entity);
+ }
});
// ...
}
}
}
}直接拿第一个空瓦片,然后往里直接插一个Uncover组件,这样第一帧就能直接监听这个事件然后去掉这个覆盖瓦片了。
然后重新运行下,正常初始情况是会自动有一块展示的了

代码可能有些乱,可以直接pull下之前拉的代码
一键重置#
现在我们的system中的逻辑是每一帧都会执行的,但如果我们想要停下行为咋办?(吃饭时你妈:还打游戏,先暂停,饭菜都凉了,吃完再打。打csgo中的你:?)
有时候我们是需要有暂停开关的,暂停之后所有的任务都将被堵塞。
但是我们这里不需要,我们想要的是每回游戏不是通过ctrl + c来停止以及cargo run来运行,我们想要通过某个按键就重置一波。
state会被存放到一个栈里面,这里之后简单称之为状态栈。
一个state有四种状态(一个状态有四个状态。。怪怪的):
active:处于栈顶的状态。inactive:在栈里,但是没有处于栈顶。exited:state出栈。enters:state入栈 。
我们现在的BoardPlugin是没法知道当前是哪个state的,所以我们需要和state关联起来,我们来改下lib.rs中的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> {
// ..
} stateData[5]这货官方没有解释,不过它实现了Send和Sync,所以它跨线程是安全的,如果不用它在实现Plugin这个trait的时候就会报错,所以这里其实可以猜出Plugin和多线程多少沾点。
现在我们和当前的state关联了,我们来监听几个状态来优化上面的问题
回到build方法中
// 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>();
} add_system_set[6]: 将一个SystemSet加入到update stages[7]也就是更新阶段,这里可以理解为hooks,会在调度update阶段触发。SystemSet[8]: 就字面上的意思,一堆system的集合,官方的描述是一个builder也就是构建器在同一时间包含的一堆system。on_enter[9]: 接收一个stateData,返回一个SystemSet。with_system[10] :往这堆system中插入一个system,相当于监听hooks注册回调。on_update[11]:类比on_enter,不过状态是update。on_in_stack_update[12]:类比on_enter,不过是状态栈更新(即栈中任何状态发生改变时触发)?(官方文档没解释)
这里事件触发和监听只有在当前state激活的时候才会执行,create_board依旧只会触发一次,而查询处理包含Uncover组件的实体是任何状态有变化时就会执行。
不过现在还是没有变的,因为我们并没有改变入口,也就是main.rs中的状态注册。
不过在注册状态之前,我们先来处理另一个问题,那就是当我们的board状态变成exited,也就是出状态栈了的时候,应该是要被清除的。
我们来再监听exited状态让board清空自己。
// 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,
}
// .. 加一个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,
})
}
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>();
}
}然后新增一个cleanup_board方法作为监听on_exit的回调。
这里的清除是很暴力的,它里面所有子孙组件、实体、资源等全都会被清除。
ok,那么自我清除就完成了(拥有较强的自我管理意识)。
现在我们回到入口注册状态。
// 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,
})
}我们这个app的状态有俩,一个是ing,另一个是out。
现在我们重新跑一下你会发现其实没啥改变,因为我们并没有操纵状态,是的,我们要手动操作。
// 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();
}
}
}我们这里监听了俩键盘点击事件,分别是C和G,C会清除,也就是触发on_exit的hooks,而G则是重新执行一波这个流程。

现在我们可以不停止程序而重置游戏了。
优化assets#
我们还遗漏了一些逻辑:插旗和判断胜利/失败的逻辑。
不过在这之前,我们先来优化下我们assets相关的代码,之前我们是直接硬编码获取的,这样不太好,一般是通过配置项的方式获取。
我们回到resources文件夹下创建一个board_assets.rs的文件。
// 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,
},
}
}
}没啥好说的,我们把前面静态资源相关的都抽出来了,比如上色、炸弹图啥的。
last方法返回切片最后一个元素。
别忘了在mod.rs中导出
那么我们回过头来用它来替换lib.rs中的资源相关的代码。
// 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,
);
// ..
}
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,
},
}],
// .. 乱死了,这就是前期没考虑好后期要遭得罪。如果看的眼睛疼,可以看原文章或者直接拉github代码即可。
那么接下来应该就是插入assets了,不过在这之前我们还需要解决一个问题。
我们的资源也就是BoardAssets得在create_board之前加载。
资源加载得在system中,而create_board在startup_system中,压根没得在它前面插入。
在它之后插又有问题,拿不到assets。
那咋办呢?好办,直接堵塞这个create_board的system即可,我们把AppState切换到Out,这样初始就堵塞了,然后等加载完资源之后再给他InGame。
// main.rs
fn main() {
// ..
// ..
}
fn main() {
// ...
- .add_state(AppState::InGame)
+ .add_state(AppState::Out)
// ...
- 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.overwrite_replace(AppState::InGame).expect("666");
}现在我们重新运行正常

现在我们的资源是配置项的了,也就是说可以自定义了。
降低开发阶段编译要求#
现在我们的项目启动非常的慢,因为依赖很多,我们来改下开发阶段的编译要求等级,这个之前在bevy入门那篇文章中说过,这里直接贴代码了
# 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 现在稍微快了那么一点。
更多的优化你可以去看入门那篇,里面有提到各种方案来减少编译的时间。
等级相关的:https://bevy-cheatbook.github.io/pitfalls/performance.html
剩余部分:胜利失败逻辑以及插旗#
在实现之前,需要确定胜利的条件:整个瓦片图只有炸弹的覆盖瓦片没有被掀开,或者容错几个炸弹,在结束之后如果小于容错个数就算胜利。
我们回到events.rs文件中
#[derive(Debug, Copy, Clone)]
pub struct BoardCompletedEvent;
#[derive(Debug, Copy, Clone)]
pub struct BombExplosionEvent;
#[derive(Debug, Copy, Clone)]
pub struct TileMarkEvent(pub Coordinates);我们新增了三个事件
BoardCompletedEvent:用于在我们board结束后,这样app就能使用我们的插件去侦测是否胜利等。BombExplosionEvent:看名字就知道是和炸弹有关的事件,这个事件用于炸弹被掀起头盖的时候,也是暴露给app的, 我们的插件不做阻断,这个行为交由app去做。Tile mark event:和TileTriggerEvent行为类似,它是监听鼠标右键,并且不是掀开头盖骨,而是插旗。
然后回到board.rs文件中,我们给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()
}
}marked_tiles:自然是用于存放被flag了瓦片。unmark_tile:移除这个瓦片上的flag。is_completed:是否完成游戏。
然后我们来修改下board.rs文件中的tile_to_uncover方法,现在我们不能直接掀起头盖骨了,得先知道这个覆盖瓦片是否被mark flag了,如果是则无作为才对(参考传统扫雷)。
// 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)
+ }
}不过这里还有个问题,那就是空瓦片周围的瓦片如果被mark flag了,那么它应该是会被掀开的,包括flag也得被清除。即:只有点击的flag不能处理,其余flag都能处理。
回到board.rs中的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)
} 掀开的同时去掉这个mark。
那么差不多了,我们可以实现这个mark行为的逻辑了
在同文件中创建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))
}然后我们来发送事件。
回到systems/uncover.rs文件中
// 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.rs中
// 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));
// ..
} 发送事件的流程已经打通,接下来就是接收事件了,老规矩,我们在systems/文件夹下创建mark.rs文件。
// 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(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();
}
}
}
}
} 和uncover做的类似,这里是执行插旗逻辑,没啥好说的。
然后别忘了在mod.rs中导出。
那么万事俱备了,就差在app中注册了。
回到lib.rs中
// 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(),
// ..
})
}
} ok,现在我们重新运行,正常情况是可以插旗了。
官方代码到这里就结束了,但实际上这里还有胜利/失败的判断逻辑没写。。。
所以我这里做了补充
失败逻辑#
我们事件注册等已经实现了,剩下的其实就只有监听到对应事件之后执行的行为逻辑没有实现。
我们先来完成失败的,失败的条件自然就是点击到了炸弹(这里就不考虑支持容错了)
然后失败的结果就是堵塞不允许再点击其它的瓦片,除非用于键盘按C清除。
我这里做简单的处理方案:
设置一个标志位,通过这个标志位来限制是否继续监听事件
先回到board.rs文件中
// board.rs
// ...
#[derive(Debug, Resource)]
pub struct Board {
// ...
+ pub is_failed: bool,
}
impl Board {
// ...
+ pub fn get_bound (&self) -> &Vec2 {
+ &self.bounds.position
+ }
+ pub fn get_size (&self) -> Vec2 {
+ self.bounds.size
+ }
+ pub fn get_middle_pos (&self) -> Vec2 {
+ let position = self.get_bound();
+ let size = self.get_size();
+ Vec2::new(position.x + size.x, position.y + size.y)
+ }
+ pub fn set_fail_state (&mut self, state: bool) {
+ self.is_failed = state;
+ }
}加了一个标志位:is_failed,另外加了几个计算位置的方法,到时候我们会用到。
然后我们回到input.rs文件中,我们在这里做的事件监听
// input.rs
// ...
pub fn input_handling(
// ...
) {
let window = windows.get_primary().unwrap();
for event in button_evr.iter() {
if board.is_failed {
return;
}
// ...
}
}
is_failed`为`true`的时候直接`return然后我们回到board_assets.rs文件中加几个assets,我们等会会用到。
// board_assets.rs
// ...
/// Use the loader for partial setup
#[derive(Debug, Clone, Resource)]
pub struct BoardAssets {
// ...
pub fail_material: SpriteMaterial,
///
pub fail_cover_board_material: SpriteMaterial,
///
pub game_over_font_size: f32,
}然后我们回到main.rs中注册这些资源
// ...
fn setup_board(
// ...
) {
// ...
fail_material: SpriteMaterial {
color: Color::rgb(100., 255., 100.),
..Default::default()
},
fail_cover_board_material: SpriteMaterial {
color: Color::Rgba {
red: 10.,
green: 10.,
blue: 50.,
alpha: 0.2,
},
..Default::default()
},
game_over_font_size: 72.
};
// Plugin activation
state.overwrite_replace(AppState::InGame).expect("666");
}看到这里你应该已经知道了这个是一个遮罩
最后回到systems中新建fail.rs
use crate::{events::BombExplosionEvent, resources::BoardAssets, Board};
use bevy::prelude::*;
pub fn fail(
mut commands: Commands,
mut board: ResMut<Board>,
mut bomb_explose_rdr: EventReader<BombExplosionEvent>,
board_assets: Res<BoardAssets>,
) {
for _ in bomb_explose_rdr.iter() {
board.as_mut().set_fail_state(true);
let middle_pos = board.get_middle_pos();
let board_entry = board.as_ref().entity;
let board_size = board.get_size();
let text_bundle = Text2dBundle {
text: Text {
sections: vec![TextSection {
value: "GAME OVER!".to_string(),
style: TextStyle {
color: board_assets.fail_material.color,
font: board_assets.bomb_counter_font.clone(),
font_size: board_assets.game_over_font_size,
},
}],
alignment: TextAlignment {
vertical: VerticalAlign::Center,
horizontal: HorizontalAlign::Center,
},
},
transform: Transform::from_xyz(middle_pos.x, middle_pos.y, 11.),
..Default::default()
};
let cover_board_entry = commands.entity(board_entry).with_children(|parent| {
parent
.spawn(SpriteBundle {
sprite: Sprite {
color: board_assets.fail_cover_board_material.color,
custom_size: Some(board_size),
..Default::default()
},
texture: board_assets.board_material.texture.clone(),
..Default::default()
})
.insert(Name::new("Cover_board")).insert(text_bundle);
});
}
}这里我在board实体里创建一个子实体:cover_board,它用来遮罩整个扫雷,然后在它里面正中央还有一个GAME OVER!的文案,用来提示已经结束游戏。
注意这里开头我把is_failed的状态设置为了true,因为这个事件是点到了炸弹才会触发的。
最后别忘了导出事件和注册这个事件
// lib.rs
fn build(&self, app: &mut App) {
// ...
.add_system_set(
SystemSet::on_in_stack_update(self.running_state.clone())
.with_system(systems::uncover::uncover_tiles)
.with_system(systems::mark::mark_tiles)
+ .with_system(systems::fail::fail),
)
// ...我们来运行下

此时已经无法点击了,只有按下C和G重置才能继续交互。
接下来我们来实现胜利的逻辑,这块和失败差不多。
胜利逻辑#
我们直接在systems中创建一个completed.rs即可。
不过在这之前,我们来改下is_failed字段,把它改成通用的need_stop_listening_pressed,因为它不应该用来表示胜利/失败状态,已经有别的地方判断过了。
pub struct Board {
// ...
pub need_stop_listening_pressed: bool,
}其它地方同步改动,这里就不贴上代码了。
然后回到completed.rs中
use crate::{events::{BoardCompletedEvent}, resources::BoardAssets, Board};
use bevy::prelude::*;
pub fn completed(
mut commands: Commands,
mut board: ResMut<Board>,
mut completed_rdr: EventReader<BoardCompletedEvent>,
board_assets: Res<BoardAssets>,
) {
for _ in completed_rdr.iter() {
board.as_mut().set_need_stop_state(true);
let middle_pos = board.get_middle_pos();
let board_entry = board.as_ref().entity;
let board_size = board.get_size();
let text_bundle = Text2dBundle {
text: Text {
sections: vec![TextSection {
value: "YOU WIN!!".to_string(),
style: TextStyle {
color: board_assets.fail_material.color,
font: board_assets.bomb_counter_font.clone(),
font_size: board_assets.game_over_font_size,
},
}],
alignment: TextAlignment {
vertical: VerticalAlign::Center,
horizontal: HorizontalAlign::Center,
},
},
transform: Transform::from_xyz(middle_pos.x, middle_pos.y, 11.),
..Default::default()
};
let cover_board_entry = commands.entity(board_entry).with_children(|parent| {
parent
.spawn(SpriteBundle {
sprite: Sprite {
color: board_assets.fail_cover_board_material.color,
custom_size: Some(board_size),
..Default::default()
},
texture: board_assets.board_material.texture.clone(),
..Default::default()
})
.insert(Name::new("Cover_board")).insert(text_bundle);
});
}
}这里省事直接把fail.rs中的copy过来改了下文案,如果你有其他想法可自行实现。
不过切记要换掉监听的事件
同样别忘了导入导出,代码我就不贴了。
好家伙,玩了几把就为了一个胜利的截图,结果发现了个bug
这里调用is_completed的代码位置是错的
应该调整到board.try_uncover_tile后面
// systems/uncover.rs
// ...
for (entity, parent) in children.iter() {
// ...
// 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 board.is_completed() {
log::info!("Board completed");
board_completed_event_wr.send(BoardCompletedEvent);
}
// ...
}现在我们重新运行下

这回就成功啦~
总结#
这一块耗时有些久了,一方面是最近有个内容比较多的需求,没得摸鱼。另一方面这块有些点涉及到新知识,原文章又没解释,官方文档也没多说。。。。。。
现在这个项目已经是完成了的,明天我们来结合wasm_pack打包成webAssembly,这样就能放到浏览器上了。
参考#
- ^Bevy Minesweeper: Safe Start https://dev.to/qongzi/bevy-minesweeper-part-7-1ko2
- ^Bevy Minesweeper: Generic States https://dev.to/qongzi/bevy-minesweeper-part-8-4apn
- ^state https://docs.rs/bevy/0.9.1/bevy/ecs/prelude/struct.State.html
- ^state例子 https://github.com/bevyengine/bevy/blob/v0.9.1/examples/ecs/state.rs
- ^stateData https://docs.rs/bevy/0.9.1/bevy/ecs/schedule/trait.StateData.html
- ^add_system_set https://docs.rs/bevy/0.9.1/bevy/app/struct.App.html#method.add_system_set
- ^add_default_stages https://docs.rs/bevy/0.9.1/bevy/app/struct.App.html#method.add_default_stages
- ^systemSet https://docs.rs/bevy/0.9.1/bevy/ecs/prelude/struct.SystemSet.html
- ^on_enter https://docs.rs/bevy/0.9.1/bevy/ecs/schedule/struct.SystemSet.html#method.on_enter
- ^with_system https://docs.rs/bevy/0.9.1/bevy/ecs/schedule/struct.SystemSet.html#method.with_system
- ^on_update https://docs.rs/bevy/0.9.1/bevy/ecs/schedule/struct.SystemSet.html#method.on_update
- ^on_in_stack_update https://docs.rs/bevy/0.9.1/bevy/ecs/schedule/struct.SystemSet.html#method.on_in_stack_update
- ^flags https://dev.to/qongzi/bevy-minesweeper-part-9-534e
- ^flags https://dev.to/qongzi/bevy-minesweeper-part-10-5hie
编辑于 2023-02-16 00:41・IP 属地广东
