前言#
昨天我们实现了交互,但是没有实现交互的具体逻辑
坏蛋Dan:rust基础学习--基于Bevy实现扫雷小游戏day5
今天我们继续
展示瓦片#
我们来拆分实现点击某个瓦片然后触发一系列其它瓦片的逻辑:
- 瓦片需要有改变自身状态(指的是从隐藏数字/炸弹到展示)的功能
- 瓦片相互触发的逻辑以及最终停下来的逻辑
我们先来处理第一点,这个比较简单。
我们之前留了一个mod没有动,那就是Uncover,它将会被用来实现第一块逻辑。
不过在这之前,我们需要改一下我们之前的代码,现在我们把所有的状态都暴露出来了,我们需要隐藏它们,等到点击的时候再触发。
逻辑一:实现覆盖瓦片#
那要怎么覆盖呢?文档里搞了个很暴力的方案,用灰色的块盖住原来瓦片的位置,点击的时候再去掉。
回到我们的board.rs中
// 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()
}
}这里没啥好说的,cover_tiles就是用来覆盖原瓦片的,key是瓦片的坐标,value就是这个用来覆盖瓦片的瓦片。
safe_square_at是我们之前写的方法,返回这个瓦片的周围瓦片的坐标。
之前忘了讲了,这个方法它里面将坐标move了,一般情况下是会报错的,因为这里所有权已经跑到迭代的第一个闭包里了,然而我们给这个Coodinates的struct实现了Clone和Copy两个trait,所以这里move的时候会尝试Copy。如果你移除这俩trait的话就会报错。
另外记住这个adjacent_coverd_tiles方法,它对于我们需要实现的逻辑二来说是非常重要的。
然后我们回到lib.rs的spawn_lines方法中,我们在这个方法里创建瓦片,那么瓦片的覆盖瓦片自然也在这里生成。
// ...
use bevy::utils::HashMap;
// ...
impl BoardPlugin {
// ...
/// System to generate the complete board
pub fn create_board(
// ...
) {
// options们初始化完毕...
// 用于覆盖的瓦片
let mut covered_tiles =
HashMap::with_capacity((tile_map.width() * tile_map.height()).into());
commands
.spawn(SpatialBundle {
visibility: Visibility::VISIBLE,
transform: Transform::from_translation(board_position.into()),
..Default::default()
})
.insert(Name::new("Board"))
.with_children(|parent| {
// ...插入background
Self::spawn_tiles(
// ...
Color::DARK_GRAY,
&mut covered_tiles,
);
});
commands.insert_resource(Board {
tile_map,
bounds: Bounds2 {
position: board_position.xy(),
size: board_size,
},
tile_size,
covered_tiles, // 用于覆盖的瓦片
});
}
// ...
fn spawn_tiles(
// ...
covered_tile_color: Color,
covered_tiles: &mut HashMap<Coordinates, Entity>,
) {
// Tiles
for (y, line) in tile_map.iter().enumerate() {
for (x, tile) in line.iter().enumerate() {
// ...
cmd.with_children(|parent| {
let entity = parent
.spawn(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 原瓦片上色
}
}
}
}改动的点有些多,实际上只是copy 一份原瓦片,然后覆盖在它上面。
不过注意这里用的是cmd.with_children,所以这个瓦片的cover正常应该是原瓦片的child。
现在我们重新执行下cargo run --features debug

另外你的窗口里的扫雷块应该又变回了灰色

ok,那么覆盖瓦片就完成了,接下来我们来接入昨天写的pressed监听,让它点击的时候去掉覆盖的瓦片。
逻辑一:接入交互#
我们来board_plugin/src文件夹下创建events.rs文件
我们在这里实现俩块代码的衔接,数据需要有一个中间变量作为载体,所以这里就有了TileTriggerEvent。
use crate::components::Coordinates;
#[derive(Debug, Copy, Clone)]
pub struct TileTriggerEvent(pub Coordinates); 然后我们导入到input.rs中,别忘了先导入到lib.rs中。
// 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));
// ..
}呃,前面好像没说过EventReader[2]和EventWriter[3] ,其实也没啥好说的,一个就是读这个类型的事件,也就是监听,另一个自然就是发送数据。
现在我们重新运行之后点击瓦片就会发送我们的这个载体给监听这个事件的对象。
注意,这个tileTriggerEvent既是数据载体,又是一个event。由于rust特有的struct tuple,这俩结合起来就有些抽象了。
接下来就该实现回调的具体内容了。
逻辑一:实现uncover#
和之前一样,具体逻辑自然也是放到systems里
我们在systems下创建一个uncover.rs文件。
// 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);
}
}
} 这里是遍历事件,然后如果回调存在,那就拿出它里面的参数也就是coordinates坐标,然后通过tile_to_uncover这个我们在board.rs中写的方法获取hashmap中对应的实体。拿到实体后通过commands.entity[4] 获取对应的实体,然后插入Uncover这个components,表示它将被揭开。
注意这里的Uncover是components/uncover.rs,是组件。
还需要注意这个*entity实际上是返回这个实体的id,然后commands通过id找到这个实体。
相信到这里你已经有些被绕晕了,不过先不要急,这里还有一部没有接上,接上后我们再画图分析执行流程。
另外别忘了从mod.rs中导出。
逻辑二:整体uncover处理#
systems/uncover.rs`中我们再来写一个`system
pub fn uncover_tiles(
mut commands: Commands,
mut board: ResMut<Board>,
children: Query<(Entity, &Parent), With<Uncover>>,
parents: Query<(&Coordinates, Option<&Bomb>, Option<&BombNeighbor>)>,
) {
for (entity, parent) in children.iter() {
// we destroy the tile cover entity
commands
.entity(entity)
.despawn_recursive();
let (coords, bomb, bomb_counter) = match parents.get(parent.get()) {
Ok(v) => v,
Err(e) => {
log::error!("{}", e);
continue;
}
};
// 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);
}
};
}
}Query[5]<(Entity, &Parent), With[6]>:查询是Uncover的实体以及它的parent。Query<(&Coordinates, Option<&Bomb>, Option<&BombNeighbor>)>:查询所有Coordinates组件以及Bomb或者BombNeighbor的组件
如何通过query获取数据具体可以看:https://bevy-cheatbook.github.io/programming/queries.html
这里做的事其实也挺好理解
- 先是找出
uncover的原瓦片,然后删除它们的覆盖瓦片。注意children里迭代出来的entity是覆盖瓦片,而不是原瓦片。 - 这个
parent是tile,也就是tile_cover的parent,get方法返回这个实体自身。而parents则是所有带有坐标组件或者炸弹(邻居)组件的实体 - 如果是炸弹自身,先
mark,但如果是炸弹邻居,就需要对这个炸弹邻居周围的坐标实体进行插入Uncover组件。
那么到这里就万事具备了。
最后就是将这些个system和event都注册到app里。
插入event和system#
回到src/lib.rs中的build方法
// 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>();
//.. 现在我们重新跑一次cargo run

现在我们的核心逻辑也处理完毕了,但你可能已经被绕晕了,所以这里会说一下整体的执行流程以及核心原理。
分析#
核心原理#
其实核心原理很简单
- 初始化时同一个坐标中创建两个瓦片,一个瓦片作为覆盖瓦片,一直都是灰色,当点击的时候才会去掉,另一个瓦片则是点击后需要展示的瓦片。
- 点击的时候标记这个瓦片为
uncover(通过插入Uncover组件),然后监听事件中发现了这个瓦片是uncover,于是将它的覆盖瓦片实体移除。 - 移除掉覆盖瓦片的实体后如果自身是一个空的,也就是周围都没有炸弹,那么标记这个瓦片周围的所有瓦片为
Uncover。 - 每一帧都会监听是否有
Uncover的(通过Query),如果有就执行上面的流程,直到没有被标记为Uncover的了。
这种实现方式比起递归的方式(我之前用js写的就是递归处理)来说性能强非常多。
执行流程#
这里直接以图的方式展示

图可能有些小。。。
代码#
这里就不贴修改的代码了,我把代码放到GitHub上了,可以直接拉
git clone https://github.com/1714080902120/rust_mine_sweeper.git总结#
今天这块内容是这个游戏的核心,所以画了上面这张图。
到这里其实已经差不多了,后面的内容就是完善和部署了。
参考#
- ^Bevy Minesweeper: Uncovering tiles https://dev.to/qongzi/bevy-minesweeper-part-6-46jh
- ^EventReader https://docs.rs/bevy/0.9.1/bevy/ecs/event/struct.EventReader.html
- ^EventWriter https://docs.rs/bevy/0.9.1/bevy/ecs/event/struct.EventWriter.html
- ^Commands::entity https://docs.rs/bevy/0.9.1/bevy/ecs/system/struct.Commands.html#method.entity
- ^Query https://docs.rs/bevy/0.9.1/bevy/ecs/prelude/struct.Query.html
- ^With https://docs.rs/bevy/0.9.1/bevy/ecs/prelude/struct.With.html
编辑于 2023-02-13 23:24・IP 属地广东
