Skip to content

Instantly share code, notes, and snippets.

@ChristopherBiscardi
Created August 27, 2024 23:43

Revisions

  1. ChristopherBiscardi created this gist Aug 27, 2024.
    42 changes: 42 additions & 0 deletions changes.diff
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,42 @@
    diff --git a/examples/mouse_to_tile.rs b/examples/mouse_to_tile.rs
    index 9ea13ec..3b03fc0 100644
    --- a/examples/mouse_to_tile.rs
    +++ b/examples/mouse_to_tile.rs
    @@ -1,4 +1,8 @@
    -use bevy::{color::palettes, math::Vec4Swizzles};
    +use bevy::{
    + color::palettes::{self, tailwind::GREEN_400},
    + math::Vec4Swizzles,
    + sprite::MaterialMesh2dBundle,
    +};
    use bevy::{ecs::system::Resource, prelude::*};
    use bevy_ecs_tilemap::prelude::*;
    mod helpers;
    @@ -325,6 +329,9 @@ fn highlight_tile_labels(
    highlighted_tiles_q: Query<Entity, With<HighlightedLabel>>,
    tile_label_q: Query<&TileLabel>,
    mut text_q: Query<&mut Text>,
    + input: Res<ButtonInput<MouseButton>>,
    + mut meshes: ResMut<Assets<Mesh>>,
    + mut materials: ResMut<Assets<ColorMaterial>>,
    ) {
    // Un-highlight any previously highlighted tile labels.
    for highlighted_tile_entity in highlighted_tiles_q.iter() {
    @@ -357,6 +364,17 @@ fn highlight_tile_labels(
    {
    // Highlight the relevant tile's label
    if let Some(tile_entity) = tile_storage.get(&tile_pos) {
    + if input.just_pressed(MouseButton::Left) {
    + dbg!(cursor_in_map_pos, cursor_pos, tile_pos);
    + let pos = tile_pos.center_in_world(grid_size, map_type);
    + let pos = map_transform.compute_matrix() * Vec4::from((pos, 0., 1.));
    + commands.spawn(MaterialMesh2dBundle {
    + mesh: meshes.add(Circle { radius: 2.0 }).into(),
    + material: materials.add(Color::from(GREEN_400)),
    + transform: Transform::from_xyz(pos.x, pos.y, 2.0),
    + ..default()
    + });
    + }
    if let Ok(label) = tile_label_q.get(tile_entity) {
    if let Ok(mut tile_text) = text_q.get_mut(label.0) {
    for section in tile_text.sections.iter_mut() {
    428 changes: 428 additions & 0 deletions main.rs
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,428 @@
    use bevy::{
    color::palettes::{self, tailwind::GREEN_400},
    math::Vec4Swizzles,
    sprite::MaterialMesh2dBundle,
    };
    use bevy::{ecs::system::Resource, prelude::*};
    use bevy_ecs_tilemap::prelude::*;
    mod helpers;
    use helpers::camera::movement as camera_movement;

    // Press SPACE to change map type. Hover over mouse tiles to highlight their labels.
    //
    // The most important function here is the `highlight_tile_labels` systems, which shows how to
    // convert a mouse cursor position into a tile position.

    // You can increase the MAP_SIDE_LENGTH, in order to test that mouse picking works for larger maps,
    // but just make sure that you run in release mode (`cargo run --release --example mouse_to_tile`)
    // otherwise things might be too slow.
    const MAP_SIDE_LENGTH_X: u32 = 4;
    const MAP_SIDE_LENGTH_Y: u32 = 4;

    const TILE_SIZE_SQUARE: TilemapTileSize = TilemapTileSize { x: 50.0, y: 50.0 };
    const TILE_SIZE_ISO: TilemapTileSize = TilemapTileSize { x: 100.0, y: 50.0 };
    const TILE_SIZE_HEX_ROW: TilemapTileSize = TilemapTileSize { x: 50.0, y: 58.0 };
    const TILE_SIZE_HEX_COL: TilemapTileSize = TilemapTileSize { x: 58.0, y: 50.0 };
    const GRID_SIZE_SQUARE: TilemapGridSize = TilemapGridSize { x: 50.0, y: 50.0 };
    const GRID_SIZE_HEX_ROW: TilemapGridSize = TilemapGridSize { x: 50.0, y: 58.0 };
    const GRID_SIZE_HEX_COL: TilemapGridSize = TilemapGridSize { x: 58.0, y: 50.0 };
    const GRID_SIZE_ISO: TilemapGridSize = TilemapGridSize { x: 100.0, y: 50.0 };

    #[derive(Deref, Resource)]
    pub struct TileHandleHexRow(Handle<Image>);

    #[derive(Deref, Resource)]
    pub struct TileHandleHexCol(Handle<Image>);

    #[derive(Deref, Resource)]
    pub struct TileHandleSquare(Handle<Image>);

    #[derive(Deref, Resource)]
    pub struct TileHandleIso(Handle<Image>);

    #[derive(Deref, Resource)]
    pub struct FontHandle(Handle<Font>);

    impl FromWorld for TileHandleHexCol {
    fn from_world(world: &mut World) -> Self {
    let asset_server = world.resource::<AssetServer>();
    Self(asset_server.load("bw-tile-hex-col.png"))
    }
    }
    impl FromWorld for TileHandleHexRow {
    fn from_world(world: &mut World) -> Self {
    let asset_server = world.resource::<AssetServer>();
    Self(asset_server.load("bw-tile-hex-row.png"))
    }
    }
    impl FromWorld for TileHandleIso {
    fn from_world(world: &mut World) -> Self {
    let asset_server = world.resource::<AssetServer>();
    Self(asset_server.load("bw-tile-iso.png"))
    }
    }
    impl FromWorld for TileHandleSquare {
    fn from_world(world: &mut World) -> Self {
    let asset_server = world.resource::<AssetServer>();
    Self(asset_server.load("bw-tile-square.png"))
    }
    }
    impl FromWorld for FontHandle {
    fn from_world(world: &mut World) -> Self {
    let asset_server = world.resource::<AssetServer>();
    Self(asset_server.load("fonts/FiraSans-Bold.ttf"))
    }
    }

    // Generates the initial tilemap, which is a square grid.
    fn spawn_tilemap(mut commands: Commands, tile_handle_square: Res<TileHandleSquare>) {
    commands.spawn(Camera2dBundle::default());

    let map_size = TilemapSize {
    x: MAP_SIDE_LENGTH_X,
    y: MAP_SIDE_LENGTH_Y,
    };

    let mut tile_storage = TileStorage::empty(map_size);
    let tilemap_entity = commands.spawn_empty().id();
    let tilemap_id = TilemapId(tilemap_entity);

    fill_tilemap(
    TileTextureIndex(0),
    map_size,
    tilemap_id,
    &mut commands,
    &mut tile_storage,
    );

    let tile_size = TILE_SIZE_SQUARE;
    let grid_size = GRID_SIZE_SQUARE;
    let map_type = TilemapType::Square;

    commands.entity(tilemap_entity).insert(TilemapBundle {
    grid_size,
    size: map_size,
    storage: tile_storage,
    texture: TilemapTexture::Single(tile_handle_square.clone()),
    tile_size,
    map_type,
    transform: get_tilemap_center_transform(&map_size, &grid_size, &map_type, 0.0),
    ..Default::default()
    });
    }

    #[derive(Component)]
    struct TileLabel(Entity);

    // Generates tile position labels of the form: `(tile_pos.x, tile_pos.y)`
    fn spawn_tile_labels(
    mut commands: Commands,
    tilemap_q: Query<(&Transform, &TilemapType, &TilemapGridSize, &TileStorage)>,
    tile_q: Query<&mut TilePos>,
    font_handle: Res<FontHandle>,
    ) {
    let text_style = TextStyle {
    font: font_handle.clone(),
    font_size: 20.0,
    color: Color::BLACK,
    };
    let text_justify = JustifyText::Center;
    for (map_transform, map_type, grid_size, tilemap_storage) in tilemap_q.iter() {
    for tile_entity in tilemap_storage.iter().flatten() {
    let tile_pos = tile_q.get(*tile_entity).unwrap();
    let tile_center = tile_pos.center_in_world(grid_size, map_type).extend(1.0);
    let transform = *map_transform * Transform::from_translation(tile_center);

    let label_entity = commands
    .spawn(Text2dBundle {
    text: Text::from_section(
    format!("{}, {}", tile_pos.x, tile_pos.y),
    text_style.clone(),
    )
    .with_justify(text_justify),
    transform,
    ..default()
    })
    .id();
    commands
    .entity(*tile_entity)
    .insert(TileLabel(label_entity));
    }
    }
    }

    #[derive(Component)]
    pub struct MapTypeLabel;

    // Generates the map type label: e.g. `Square { diagonal_neighbors: false }`
    fn spawn_map_type_label(
    mut commands: Commands,
    font_handle: Res<FontHandle>,
    windows: Query<&Window>,
    map_type_q: Query<&TilemapType>,
    ) {
    let text_style = TextStyle {
    font: font_handle.clone(),
    font_size: 20.0,
    color: Color::BLACK,
    };
    let text_alignment = JustifyText::Center;

    for window in windows.iter() {
    for map_type in map_type_q.iter() {
    // Place the map type label somewhere in the top left side of the screen
    let transform = Transform {
    translation: Vec2::new(-0.5 * window.width() / 2.0, 0.8 * window.height() / 2.0)
    .extend(1.0),
    ..Default::default()
    };
    commands.spawn((
    Text2dBundle {
    text: Text::from_section(format!("{map_type:?}"), text_style.clone())
    .with_justify(text_alignment),
    transform,
    ..default()
    },
    MapTypeLabel,
    ));
    }
    }
    }

    // Swaps the map type, when user presses SPACE
    #[allow(clippy::too_many_arguments)]
    fn swap_map_type(
    mut tilemap_query: Query<(
    &mut Transform,
    &TilemapSize,
    &mut TilemapType,
    &mut TilemapGridSize,
    &mut TilemapTexture,
    &mut TilemapTileSize,
    )>,
    keyboard_input: Res<ButtonInput<KeyCode>>,
    tile_label_q: Query<
    (&TileLabel, &TilePos),
    (With<TileLabel>, Without<MapTypeLabel>, Without<TilemapType>),
    >,
    mut map_type_label_q: Query<&mut Text, With<MapTypeLabel>>,
    mut transform_q: Query<&mut Transform, Without<TilemapType>>,
    tile_handle_square: Res<TileHandleSquare>,
    tile_handle_hex_row: Res<TileHandleHexRow>,
    tile_handle_hex_col: Res<TileHandleHexCol>,
    tile_handle_iso: Res<TileHandleIso>,
    ) {
    if keyboard_input.just_pressed(KeyCode::Space) {
    for (
    mut map_transform,
    map_size,
    mut map_type,
    mut grid_size,
    mut map_texture,
    mut tile_size,
    ) in tilemap_query.iter_mut()
    {
    match map_type.as_ref() {
    TilemapType::Square { .. } => {
    *map_type = TilemapType::Isometric(IsoCoordSystem::Diamond);
    *map_texture = TilemapTexture::Single((*tile_handle_iso).clone());
    *tile_size = TILE_SIZE_ISO;
    *grid_size = GRID_SIZE_ISO;
    }
    TilemapType::Isometric(IsoCoordSystem::Diamond) => {
    *map_type = TilemapType::Isometric(IsoCoordSystem::Staggered);
    *map_texture = TilemapTexture::Single((*tile_handle_iso).clone());
    *tile_size = TILE_SIZE_ISO;
    *grid_size = GRID_SIZE_ISO;
    }
    TilemapType::Isometric(IsoCoordSystem::Staggered) => {
    *map_type = TilemapType::Hexagon(HexCoordSystem::Row);
    *map_texture = TilemapTexture::Single((*tile_handle_hex_row).clone());
    *tile_size = TILE_SIZE_HEX_ROW;
    *grid_size = GRID_SIZE_HEX_ROW;
    }
    TilemapType::Hexagon(HexCoordSystem::Row) => {
    *map_type = TilemapType::Hexagon(HexCoordSystem::RowEven);
    }
    TilemapType::Hexagon(HexCoordSystem::RowEven) => {
    *map_type = TilemapType::Hexagon(HexCoordSystem::RowOdd);
    }
    TilemapType::Hexagon(HexCoordSystem::RowOdd) => {
    *map_type = TilemapType::Hexagon(HexCoordSystem::Column);
    *map_texture = TilemapTexture::Single((*tile_handle_hex_col).clone());
    *tile_size = TILE_SIZE_HEX_COL;
    *grid_size = GRID_SIZE_HEX_COL;
    }
    TilemapType::Hexagon(HexCoordSystem::Column) => {
    *map_type = TilemapType::Hexagon(HexCoordSystem::ColumnEven);
    }
    TilemapType::Hexagon(HexCoordSystem::ColumnEven) => {
    *map_type = TilemapType::Hexagon(HexCoordSystem::ColumnOdd);
    }
    TilemapType::Hexagon(HexCoordSystem::ColumnOdd) => {
    *map_type = TilemapType::Square;
    *map_texture = TilemapTexture::Single((*tile_handle_square).clone());
    *tile_size = TILE_SIZE_SQUARE;
    *grid_size = GRID_SIZE_SQUARE;
    }
    }

    *map_transform = get_tilemap_center_transform(map_size, &grid_size, &map_type, 0.0);

    for (label, tile_pos) in tile_label_q.iter() {
    if let Ok(mut tile_label_transform) = transform_q.get_mut(label.0) {
    let tile_center = tile_pos.center_in_world(&grid_size, &map_type).extend(1.0);
    *tile_label_transform =
    *map_transform * Transform::from_translation(tile_center);
    }
    }

    for mut label_text in map_type_label_q.iter_mut() {
    label_text.sections[0].value = format!("{:?}", map_type.as_ref());
    }
    }
    }
    }

    #[derive(Component)]
    struct HighlightedLabel;

    #[derive(Resource)]
    pub struct CursorPos(Vec2);
    impl Default for CursorPos {
    fn default() -> Self {
    // Initialize the cursor pos at some far away place. It will get updated
    // correctly when the cursor moves.
    Self(Vec2::new(-1000.0, -1000.0))
    }
    }

    // We need to keep the cursor position updated based on any `CursorMoved` events.
    pub fn update_cursor_pos(
    camera_q: Query<(&GlobalTransform, &Camera)>,
    mut cursor_moved_events: EventReader<CursorMoved>,
    mut cursor_pos: ResMut<CursorPos>,
    ) {
    for cursor_moved in cursor_moved_events.read() {
    // To get the mouse's world position, we have to transform its window position by
    // any transforms on the camera. This is done by projecting the cursor position into
    // camera space (world space).
    for (cam_t, cam) in camera_q.iter() {
    if let Some(pos) = cam.viewport_to_world_2d(cam_t, cursor_moved.position) {
    *cursor_pos = CursorPos(pos);
    }
    }
    }
    }

    // This is where we check which tile the cursor is hovered over.
    fn highlight_tile_labels(
    mut commands: Commands,
    cursor_pos: Res<CursorPos>,
    tilemap_q: Query<(
    &TilemapSize,
    &TilemapGridSize,
    &TilemapType,
    &TileStorage,
    &Transform,
    )>,
    highlighted_tiles_q: Query<Entity, With<HighlightedLabel>>,
    tile_label_q: Query<&TileLabel>,
    mut text_q: Query<&mut Text>,
    input: Res<ButtonInput<MouseButton>>,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<ColorMaterial>>,
    ) {
    // Un-highlight any previously highlighted tile labels.
    for highlighted_tile_entity in highlighted_tiles_q.iter() {
    if let Ok(label) = tile_label_q.get(highlighted_tile_entity) {
    if let Ok(mut tile_text) = text_q.get_mut(label.0) {
    for section in tile_text.sections.iter_mut() {
    section.style.color = Color::BLACK;
    }
    commands
    .entity(highlighted_tile_entity)
    .remove::<HighlightedLabel>();
    }
    }
    }

    for (map_size, grid_size, map_type, tile_storage, map_transform) in tilemap_q.iter() {
    // Grab the cursor position from the `Res<CursorPos>`
    let cursor_pos: Vec2 = cursor_pos.0;
    // We need to make sure that the cursor's world position is correct relative to the map
    // due to any map transformation.
    let cursor_in_map_pos: Vec2 = {
    // Extend the cursor_pos vec3 by 0.0 and 1.0
    let cursor_pos = Vec4::from((cursor_pos, 0.0, 1.0));
    let cursor_in_map_pos = map_transform.compute_matrix().inverse() * cursor_pos;
    cursor_in_map_pos.xy()
    };
    // Once we have a world position we can transform it into a possible tile position.
    if let Some(tile_pos) =
    TilePos::from_world_pos(&cursor_in_map_pos, map_size, grid_size, map_type)
    {
    // Highlight the relevant tile's label
    if let Some(tile_entity) = tile_storage.get(&tile_pos) {
    if input.just_pressed(MouseButton::Left) {
    dbg!(cursor_in_map_pos, cursor_pos, tile_pos);
    let pos = tile_pos.center_in_world(grid_size, map_type);
    let pos = map_transform.compute_matrix() * Vec4::from((pos, 0., 1.));
    commands.spawn(MaterialMesh2dBundle {
    mesh: meshes.add(Circle { radius: 2.0 }).into(),
    material: materials.add(Color::from(GREEN_400)),
    transform: Transform::from_xyz(pos.x, pos.y, 2.0),
    ..default()
    });
    }
    if let Ok(label) = tile_label_q.get(tile_entity) {
    if let Ok(mut tile_text) = text_q.get_mut(label.0) {
    for section in tile_text.sections.iter_mut() {
    section.style.color = palettes::tailwind::RED_600.into();
    }
    commands.entity(tile_entity).insert(HighlightedLabel);
    }
    }
    }
    }
    }
    }

    #[derive(SystemSet, Clone, Copy, Hash, PartialEq, Eq, Debug)]
    pub struct SpawnTilemapSet;

    fn main() {
    App::new()
    .add_plugins(
    DefaultPlugins
    .set(WindowPlugin {
    primary_window: Some(Window {
    title: String::from("Mouse Position to Tile Position"),
    ..Default::default()
    }),
    ..default()
    })
    .set(ImagePlugin::default_nearest()),
    )
    .init_resource::<CursorPos>()
    .init_resource::<TileHandleIso>()
    .init_resource::<TileHandleHexCol>()
    .init_resource::<TileHandleHexRow>()
    .init_resource::<TileHandleSquare>()
    .init_resource::<FontHandle>()
    .add_plugins(TilemapPlugin)
    .add_systems(
    Startup,
    (spawn_tilemap, apply_deferred)
    .chain()
    .in_set(SpawnTilemapSet),
    )
    .add_systems(
    Startup,
    (spawn_tile_labels, spawn_map_type_label).after(SpawnTilemapSet),
    )
    .add_systems(First, (camera_movement, update_cursor_pos).chain())
    .add_systems(Update, swap_map_type)
    .add_systems(Update, highlight_tile_labels.after(swap_map_type))
    .run();
    }