DEV Community

Cover image for Bevy Minesweeper: Tiles and Components
Qongzi
Qongzi

Posted on

Bevy Minesweeper: Tiles and Components

Check the repository

Assets

Let's complete our board, for this we will need assets:

  • A bomb png texture
  • A font

(You can use the assets from the tutorial repository)

Place your assets in an assets folder at the root of your project

├── Cargo.lock
├── Cargo.toml
├── assets
│   ├── fonts
│   │   └── my_font.ttf
│   └── sprites
│       ├── bomb.png
├── board_plugin
│   ├── Cargo.toml
│   └── src
│       ├── components
│       ├── lib.rs
│       └── resources
├── src
│   └── main.rs
Enter fullscreen mode Exit fullscreen mode

Let's load these assets in our create_board startup system.
For this we need to add an argument to the system:

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

The AssetServer resource allows loading files from the assets folder.

We can now load our assets right at the beginning of the function and retrieve handles:

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

Component declaration

Our tile map knows which tile is a bomb, a bomb neighbor or empty, but the ECS doesn't.
Let's declare components we will attach to our tile entities in our plugin under board_plugin/components with our Coordinates component:

  • 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;
Enter fullscreen mode Exit fullscreen mode

Bomb

This component will identify a tile as a bomb

// 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;
Enter fullscreen mode Exit fullscreen mode

Bomb neighbor

This component will identify a tile as bomb neighbor

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,
}
Enter fullscreen mode Exit fullscreen mode

Uncover

This component will identify tiles to uncover, we will use it in part 6

use bevy::prelude::Component;

/// Uncover component, indicates a covered tile that should be uncovered
#[cfg_attr(feature = "debug", derive(bevy_inspector_egui::Inspectable))]
#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Component)]
pub struct Uncover;
Enter fullscreen mode Exit fullscreen mode

Let's register the components for the debug inspector in our plugin:

// 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>();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Component use

Let's create a function for BoardPlugin creating a Text2DBundle for our bomb counters:

/// 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()
    }
}
Enter fullscreen mode Exit fullscreen mode

It takes in parameter:

  • count: the neighboring bomb count to print
  • font: a asset Handle of our font
  • size: a text size

The colors are completely arbitrary.
Again, we put the z value of the Transform translation to 1. so the text is printed on top of the tile.

We can now move our tile spawning loop into a distinct function:

// 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);
            }
        }
    }
Enter fullscreen mode Exit fullscreen mode
  • Notice we now use a temporary cmd value for our entity builder*

We can now call it from our create_board startup system, in our with_children block after the background spawn:

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

Then we complete our spawn_tiles function to add our bombs sprites and counter texts inside the double for loop:

// 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 => (),
                }
// ..
Enter fullscreen mode Exit fullscreen mode

Until now all tiles had identical components like Coordinates, Transform, Sprite, etc.
But now some tiles have:

  • A Bomb component and a child entity with the bomb sprite
  • A BombNeighbor component and a child entity with the counter text

We added a texture to the bomb sprite, what about the others?

By default, a white square texture is used if no texture is specified on SpriteBundle. In Part 9 we will see it in more detail.

Let's run our app and get our beautiful board:

Board


Previous Chapter -- Next Chapter


Author: Félix de Maneville
Follow me on Twitter

Published by Qongzi

Top comments (5)

Collapse
 
tomuxmon profile image
Tomas Dambrauskas • Edited

When I run using "cargo run" assets are loaded correctly. But when I try to launch debugger with visual studio code it fails loading assets.
my configuration looks like this:

{
            "type": "lldb",
            "request": "launch",
            "name": "Debug executable 'minesweeper-tutorial'",
            "cargo": {
                "args": [
                    "build",
                    "--bin=minesweeper-tutorial",
                    "--package=minesweeper-tutorial",
                    "--features",
                    "debug"
                ],
                "filter": {
                    "name": "minesweeper-tutorial",
                    "kind": "bin"
                }
            },
            "args": [],
            "cwd": "${workspaceFolder}"
        }
Enter fullscreen mode Exit fullscreen mode

should "cwd" (program working directory) be changed to something else?

Oh and also had to change BoardPlugin build debug section to use registry instead to make it work.:

        #[cfg(feature = "debug")]
        {
            let mut registry = app
                .world
                .get_resource_or_insert_with(InspectableRegistry::default);
            // registering custom component to be able to edit it in inspector
            registry.register::<Coordinates>();
            registry.register::<BombNeighbor>();
            registry.register::<Bomb>();
            registry.register::<Uncover>();
        }
Enter fullscreen mode Exit fullscreen mode
Collapse
 
tomuxmon profile image
Tomas Dambrauskas • Edited

Had to add environment variable in launch json :

"env": {
    "CARGO_MANIFEST_DIR": "${workspaceFolder}"
}
Enter fullscreen mode Exit fullscreen mode

a clue found in : docs.rs/bevy_asset/0.6.0/bevy_asse...

Collapse
 
qongzi profile image
Qongzi

I don't use VS Code so I don't know about the issues with the built in debugger.

About the registry, you are using the old way of registering the components
Are you using the latest version of bevy_inspector_egui ? It should be 0.8.x.
Accessing the inspectable registry is no longer required

Collapse
 
tomuxmon profile image
Tomas Dambrauskas • Edited

hmm. set the version in cargo to version = "~0.8". checked to see that version 0.8.2 is being downloaded. still had the same issue. Got panic:

thread 'main' panicked at 'called `Option::unwrap()` on a `None` value', /home/tomas/.cargo/registry/src/github.com-1ecc6299db9ec823/bevy-inspector-egui-0.8.2/src/lib.rs:375:14
Enter fullscreen mode Exit fullscreen mode

bevy inspector egui panicked when unwrapping:

impl RegisterInspectable for App {
    fn register_inspectable<T: Inspectable + 'static>(&mut self) -> &mut Self {
        self.world
            .get_resource_mut::<InspectableRegistry>()
            .unwrap() // <-- panic here
            .register::<T>();
        self
    }
}
//..
Enter fullscreen mode Exit fullscreen mode

also. it seems that order of adding world inspector plugin matters (somehow expecting it to be strictly declarative). If I register the plugin at the start like this:

let mut app = App::new();
// Debug hierarchy inspector
#[cfg(feature = "debug")]
app.add_plugin(WorldInspectorPlugin::new());
Enter fullscreen mode Exit fullscreen mode

I get no panic, but sadly I also do not get any world inspector plugin.
If I add the world inspector plugin just before the app run it ends up panicking.

 // Debug hierarchy inspector
#[cfg(feature = "debug")]
app.add_plugin(WorldInspectorPlugin::new());

app.run();
Enter fullscreen mode Exit fullscreen mode

but anyways... registry.register::<T>() work for me so I do not really care at this point :P. Thanks for your time!

Collapse
 
leonidv profile image
Leonid Vygovskiy

github.com/leonidv/bevy-minesweepe... - full tutorial updated to 12.1. One chapter per commit.