前言#
昨天我们成功将我们的瓦片图渲染到窗口里
坏蛋Dan:rust基础学习--基于Bevy实现扫雷小游戏day3
今天我们继续实现这个小游戏
上色#
我们距离扫雷demo还剩下一部分:交互。
不过今天我们先不做这一块,我们来完善下之前做的东西。
现在我们的瓦片长得太难看了,并且后面还会被变成炸弹,也要支持插旗,所以我们还需要一些静态资源。
资源下载地址:assets · master · Qonfucius / Minesweeper Tutorial · GitLab
这些资源得放到mine_sweeper里,和src同级,这个是相对路径的默认规则。

加完静态资源之后,我们回到board_plugin/src/lib.rs中,找到create_board方法,我们新增一个参数asset_server
pub fn create_board(
mut commands: Commands,
board_options: Option<Res<BoardOptions>>,
window: Res<WindowDescriptor>,
asset_server: Res<AssetServer>, // The AssetServer resource
) {assets翻译过来是断言,但是前端中这个文件夹里的东西一般我自己叫做静态资源,所以我这里就沿用了,静态资源(assets)和资源(resources)是俩不同的东西,虽然assets也是Resources的一种。
AssetServer:在后台通过filesystem也就是文件系统加载静态资源。用法可以看这个https://github.com/bevyengine/bevy/tree/latest/examples/asset/asset_loading.rs
接着我们创建三个component,用来表示炸弹、炸弹邻居以及空瓦片。
回到board_plugin/src/component文件夹里,创建bomb.rs、bomb_neighbor.rs和uncover.rs三个文件
bomb.rs
// bomb.rs
use bevy::prelude::Component;
/// Bomb component
#[cfg(feature = "debug")]
use bevy_inspector_egui::prelude::*;
#[cfg(feature = "debug")]
use bevy::prelude::Reflect;
#[cfg_attr(feature = "debug", derive(Reflect, InspectorOptions))]
#[cfg_attr(feature = "debug", reflect(InspectorOptions))]
#[cfg_attr(feature = "debug", inspector(validate = |ability| ability.current_charges <= ability.max_charges))]
#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Component)]
pub struct Bomb;
bomb_neighbor.rs
use bevy::prelude::Component;
/// Bomb neighbor component
#[cfg(feature = "debug")]
use bevy_inspector_egui::prelude::*;
#[cfg(feature = "debug")]
use bevy::prelude::Reflect;
#[cfg_attr(feature = "debug", derive(Reflect, InspectorOptions))]
#[cfg_attr(feature = "debug", reflect(InspectorOptions))]
#[cfg_attr(feature = "debug", inspector(validate = |ability| ability.current_charges <= ability.max_charges))]
#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Component)]
pub struct BombNeighbor {
/// Number of neighbor bombs
#[cfg_attr(feature = "debug", inspector(min = 0, max = 8))]
pub count: u8,
}
uncover.rs
use bevy::prelude::Component;
/// Uncover component, indicates a covered tile that should be uncovered
#[cfg(feature = "debug")]
use bevy_inspector_egui::prelude::*;
#[cfg(feature = "debug")]
use bevy::prelude::Reflect;
#[cfg_attr(feature = "debug", derive(Reflect, InspectorOptions))]
#[cfg_attr(feature = "debug", reflect(InspectorOptions))]
#[cfg_attr(feature = "debug", inspector(validate = |ability| ability.current_charges <= ability.max_charges))]
#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Component)]
pub struct Uncover;然后在component/mod.rs中将它们导出
mod coordinates;
pub use coordinates::Coordinates;
mod bomb;
mod bomb_neighbor;
mod uncover;
pub use bomb::Bomb;
pub use bomb_neighbor::BombNeighbor;
pub use uncover::Uncover;这一块内容相当的繁琐,尤其是用于侦测的,你可以自己搞个宏简化写法(我自己试了一天多,跪了,后面有必要把macro那篇文档的学习提前了)。
最后我们在main.rs中引入并注册到app里,这样才能被侦测到
// ...
#[cfg(feature = "debug")]
use board_plugin::{ components::* };
// ...
#[cfg(feature = "debug")]
// Debug hierarchy inspector
app.add_plugin(WorldInspectorPlugin).register_type::<Coordinates>().register_type::<Bomb>().register_type::<BombNeighbor>().register_type::<Uncover>();
// ...不过想了下,其实我们并不需要放在main.rs中,因为这块类型注册实际上也算是业务逻辑相关的,所以应该放到board_plugin/src/lib.rs中的build方法中,这样就不会污染main.rs了。
use components::*;
pub struct BoardPlugin;
impl Plugin for BoardPlugin {
fn build(&self, app: &mut App) {
app.add_startup_system(Self::create_board);
#[cfg(feature = "debug")]
{
app.register_type::<Coordinates>()
.register_type::<Bomb>()
.register_type::<BombNeighbor>()
.register_type::<Uncover>();
}
log::info!("Loaded Board Plugin");
}
}
// ...这样就舒服很多了。
接着我们来尝试把这些assets渲染到窗口上。
现在我们窗口上的瓦片都是灰色的,我们来给它们上色。
之前我们也有上色的逻辑,但是那是输出在终端的。
回到lib.rs中,新增一个method
/// Generates the bomb counter text 2D Bundle for a given value
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
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()
}
}这个方法很简单,就是给炸弹邻居们上色
Text2dBundle:看名字就知道了,一堆和Text相关的component组合体。
我们把上色的字放在这个瓦片的中间。
这里有一点需要注意,那就是Transform的z轴这里给了1.,这样文案才不会被覆盖。
然后我们回到create_board的方法中,在之前给瓦片上色的地方。
为了让代码结构更清晰,先不接入上色代码,我们先来把这块逻辑迁移到另一个方法中。
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(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);
}
}
}注意,不是BoardPlugin的method,是关联函数,或者你直接拿出来当做function也不是不行,不过后面有些写法需要调整。
抽出来之后我们再回到create_board方法中替换原来的代码
// ,,,
let font = asset_server.load("fonts/pixeled.ttf");
let bomb_image = asset_server.load("sprites/bomb.png");
// ,,,
Self::spawn_tiles(
parent,
&tile_map,
tile_size,
options.tile_padding,
Color::GRAY,
bomb_image,
font,
);资源加载一般放到方法的顶部,确保资源的正常加载。
ok,现在我们可以来接入上色逻辑了。
回到我们的spawn_tiles方法中。
之前我们是无差别上色,现在我们可以match匹配分别上色。
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 tile_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()
};
let mut cmd = parent.spawn(tile_bundle);
cmd.insert(Name::new(format!("Tile ({}, {})", x, y)))
.insert(coordinates);
match tile {
Tile::Bomb => {
cmd.insert(Bomb)
.with_children(|parent| {
parent.spawn(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()
});
});
}
Tile::BombNeighbor(count) => {
let bomb_neighbor = BombNeighbor { count: *count };
cmd.insert(bomb_neighbor)
.with_children(|parent| {
parent.spawn(Self::bomb_count_text_bundle(
*count,
font.clone(),
size - padding,
));
});
}
_ => (),
};
}
}
}如果是炸弹就给它上张图,而炸弹邻居则是不同颜色的数字。
注意,这里的炸弹图和文字都是实体,层级在瓦片下。
那么这里就上色完毕了,我们来运行下cargo run

你也可以--features debug看下component检测是否正常。
代码#
这里只放lib.rs的,其它没多大改变。
// lib.rs
pub mod components;
pub mod resources;
use bevy::log;
use bevy::prelude::*;
use resources::tile::Tile;
use resources::tile_map::TileMap;
use resources::BoardOptions;
use resources::BoardPosition;
use resources::TileSize;
use components::*;
pub struct BoardPlugin;
impl Plugin for BoardPlugin {
fn build(&self, app: &mut App) {
app.add_startup_system(Self::create_board);
#[cfg(feature = "debug")]
{
app.register_type::<Coordinates>()
.register_type::<Bomb>()
.register_type::<BombNeighbor>()
.register_type::<Uncover>();
}
log::info!("Loaded Board Plugin");
}
}
impl BoardPlugin {
/// Computes a tile size that matches the window according to the tile map size
fn adaptative_tile_size(
window: Res<Windows>,
(min, max): (f32, f32), // Tile size constraints
(width, height): (u16, u16), // Tile map dimensions
) -> f32 {
let window = window.get_primary().expect("get window primary error");
let max_width = window.width() / width as f32;
let max_heigth = window.height() / height as f32;
max_width.min(max_heigth).clamp(min, max)
}
/// System to generate the complete board
pub fn create_board(
mut commands: Commands,
board_options: Option<Res<BoardOptions>>,
window: Res<Windows>,
asset_server: Res<AssetServer>, // The AssetServer resource
) {
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());
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()),
),
};
// 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,
};
commands
.spawn(SpatialBundle {
visibility: Visibility::VISIBLE,
transform: Transform::from_translation(board_position.into()),
..Default::default()
})
.insert(Name::new("Board"))
.with_children(|parent| {
// We spawn the board background sprite at the center of the board, since the sprite pivot is centered
parent
.spawn(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"));
let font = asset_server.load("fonts/pixeled.ttf");
let bomb_image = asset_server.load("sprites/bomb.png");
Self::spawn_tiles(
parent,
&tile_map,
tile_size,
options.tile_padding,
Color::GRAY,
bomb_image,
font,
);
});
}
/// Generates the bomb counter text 2D Bundle for a given value
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
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()
}
}
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 tile_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()
};
let mut cmd = parent.spawn(tile_bundle);
cmd.insert(Name::new(format!("Tile ({}, {})", x, y)))
.insert(coordinates);
match tile {
Tile::Bomb => {
cmd.insert(Bomb)
.with_children(|parent| {
parent.spawn(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()
});
});
}
Tile::BombNeighbor(count) => {
let bomb_neighbor = BombNeighbor { count: *count };
cmd.insert(bomb_neighbor)
.with_children(|parent| {
parent.spawn(Self::bomb_count_text_bundle(
*count,
font.clone(),
size - padding,
));
});
}
_ => (),
};
}
}
}
}总结#
莫得。
参考#
- ^Bevy Minesweeper: Tiles and Components https://dev.to/qongzi/bevy-minesweeper-part-4-2co9
编辑于 2023-02-12 12:40・IP 属地广东
