bevy_ecs_tilemap icon indicating copy to clipboard operation
bevy_ecs_tilemap copied to clipboard

Feature request: mouse-to-tile events

Open mattdm opened this issue 4 years ago • 7 comments

It would be nice to have mouse events translated to tiles. At the minimum, a helper function which takes mouse coordinates and returns a tile. However, even better, a system which:

  • Generates custom events for:
    • a tile was clicked (left/right/middle/other, optionally subposition)
    • hover-in
    • hover-out
    • maybe a mouse-drag event which gives a list of tiles from first (tile where the button was pressed down) + all the others passed over in order + last (tile where button was released)
    • maybe also mouse-down / mouse-up on a tile (not sure how useful that really is vs "click" outside of the drag case)
  • Provides a way to query:
    • last-clicked tile (or, list of tiles clicked since last query?)
    • currently-hovered tile
    • subposition within tile of last click (scaled to tile size in view)
    • subposition within tile of hover
    • maybe the latest mouse-drag list?

I'm not sure how -- or if at all -- this should interact with non-tile sprites or other UI elements. I can image cases where it'd be nice to at least know that a click is "obstructed" by an object which is above the map.

mattdm avatar Jun 01 '21 16:06 mattdm

Related to #30

StarArawn avatar Jun 01 '21 16:06 StarArawn

And just a few more complications:

  • Should handle hex and isometric tiles
  • Layers... not sure how that should work at all

mattdm avatar Jun 02 '21 13:06 mattdm

Also connected to #44 -- if isometric tiles have height, they can extend out of their nominal space, and therefore mouse-picking should be based on render height. (Including the possibility of layers having their own Z axis.) But that adds a lot of complexity that I would love to not have block the basics.

mattdm avatar Jun 02 '21 14:06 mattdm

Implementing this logic by hand for hexagonal tiles is a giant pain: getting this working for hex tiles will make the library substantially more useful.

alice-i-cecile avatar Jun 27 '21 23:06 alice-i-cecile

Definitely not a full solution, but this might be helpful to some. I wrote some helper functions that can convert a world position to a tile coordinate for axial hex tiles (HexType::Row).

/// Returns the tile coordinates at the given world coordinates
///
/// Based on the pixel-to-hex algorithm described here: https://www.redblobgames.com/grids/hexagons/#pixel-to-hex
pub fn tile_from_world_coordinates(
    world_position: Vec2, 
    tile_size: Vec2
) -> Option<UVec2> {
    
    // Basis vectors for axial hex coordinates
    let hex_basis = Mat2::from_cols(Vec2::new(3f32.sqrt()/3.0, 0.0), Vec2::new(-1.0/3.0, 2.0/3.0));
    // Offsets tile position so that world (0,0) is the center of the (0,0) tile
    let tile_offset = tile_size * -0.5;
    
    let hex_basis_position = (hex_basis * (world_position + tile_offset)) / (tile_size.y * 0.5);

    let tile_position = hex_round(hex_basis_position);

    // Need to check for negative values here. Simply casting to UVec2 will not do
    // UVec2::as_u32() converts any negative values to 0
    if tile_position.min_element() < 0.0 {
        return None;
    } else {
        return Some(tile_position.as_u32());
    }
}

/// Takes fractional axial coordinates and "rounds" them to the nearest hex center
///
/// Based on the hex rounding algorithm described here: https://www.redblobgames.com/grids/hexagons/#rounding
fn hex_round(coords: Vec2) -> Vec2 {
    // Convert to cube coodinates
    let cube_coords = coords.extend( -1.0 * coords.x - coords.y);
    
    let mut rounded_cube_coords = cube_coords.round();
    let coords_diff = rounded_cube_coords - cube_coords;
    
    // Preserve coodinate invariants
    if coords_diff.x > coords_diff.y && coords_diff.x > coords_diff.z {
        rounded_cube_coords.x = -1.0 * rounded_cube_coords.y - rounded_cube_coords.z;
    } else if coords_diff.y > coords_diff.z {
        rounded_cube_coords.y = -1.0 * rounded_cube_coords.x - rounded_cube_coords.z;
    } else {
        rounded_cube_coords.z = -1.0 * rounded_cube_coords.x - rounded_cube_coords.y;
    }

    // Convert back to axial
    return rounded_cube_coords.truncate();
}

This definitely isn't perfect. Some of these values should really be constants (such as the hex basis matrix and the tile offset), the error handling should probably use Result<UVec2, E> instead of Option<UVec2>, and it's not terribly efficient. That being said, it does work reasonably well, save for that pesky hex layout issue (#58)

sixfold-origami avatar Jun 28 '21 02:06 sixfold-origami

Unfortunately @plof27 those values can't be consts: Rust does not support const math on floats.

alice-i-cecile avatar Jun 28 '21 02:06 alice-i-cecile

Definitely not a full solution, but this might be helpful to some. I wrote some helper functions that can convert a world position to a tile coordinate for axial hex tiles (HexType::Row).

/// Returns the tile coordinates at the given world coordinates
///
/// Based on the pixel-to-hex algorithm described here: https://www.redblobgames.com/grids/hexagons/#pixel-to-hex
pub fn tile_from_world_coordinates(
    world_position: Vec2, 
    tile_size: Vec2
) -> Option<UVec2> {
    
    // Basis vectors for axial hex coordinates
    let hex_basis = Mat2::from_cols(Vec2::new(3f32.sqrt()/3.0, 0.0), Vec2::new(-1.0/3.0, 2.0/3.0));
    // Offsets tile position so that world (0,0) is the center of the (0,0) tile
    let tile_offset = tile_size * -0.5;
    
    let hex_basis_position = (hex_basis * (world_position + tile_offset)) / (tile_size.y * 0.5);

    let tile_position = hex_round(hex_basis_position);

    // Need to check for negative values here. Simply casting to UVec2 will not do
    // UVec2::as_u32() converts any negative values to 0
    if tile_position.min_element() < 0.0 {
        return None;
    } else {
        return Some(tile_position.as_u32());
    }
}

/// Takes fractional axial coordinates and "rounds" them to the nearest hex center
///
/// Based on the hex rounding algorithm described here: https://www.redblobgames.com/grids/hexagons/#rounding
fn hex_round(coords: Vec2) -> Vec2 {
    // Convert to cube coodinates
    let cube_coords = coords.extend( -1.0 * coords.x - coords.y);
    
    let mut rounded_cube_coords = cube_coords.round();
    let coords_diff = rounded_cube_coords - cube_coords;
    
    // Preserve coodinate invariants
    if coords_diff.x > coords_diff.y && coords_diff.x > coords_diff.z {
        rounded_cube_coords.x = -1.0 * rounded_cube_coords.y - rounded_cube_coords.z;
    } else if coords_diff.y > coords_diff.z {
        rounded_cube_coords.y = -1.0 * rounded_cube_coords.x - rounded_cube_coords.z;
    } else {
        rounded_cube_coords.z = -1.0 * rounded_cube_coords.x - rounded_cube_coords.y;
    }

    // Convert back to axial
    return rounded_cube_coords.truncate();
}

This definitely isn't perfect. Some of these values should really be constants (such as the hex basis matrix and the tile offset), the error handling should probably use Result<UVec2, E> instead of Option<UVec2>, and it's not terribly efficient. That being said, it does work reasonably well, save for that pesky hex layout issue (#58)

According to redblobgames it should be let coords_diff = (rounded_cube_coords - cube_coords).abs();. Took me some time :)

X3CP4o2 avatar Sep 10 '22 20:09 X3CP4o2

A lot of the basic functionality is now in helpers: https://github.com/StarArawn/bevy_ecs_tilemap/blob/eabd66eac4c5204ec54646141ffb26935cace88d/examples/mouse_to_tile.rs#L372

As for the more advanced functionality still remains as a TODO. But, we should also be careful in giving thought to how much of it belongs in this crate, versus a more general crate (possibly bevy itself).

bzm3r avatar Oct 03 '22 18:10 bzm3r

I'm going to close this out; we can make more specific issues as needed :)

alice-i-cecile avatar Oct 03 '22 21:10 alice-i-cecile