Skip to content

Instantly share code, notes, and snippets.

@Kielan
Last active January 17, 2025 13:27
Show Gist options
  • Save Kielan/b67937272a385c21ed9e3c1df827a130 to your computer and use it in GitHub Desktop.
Save Kielan/b67937272a385c21ed9e3c1df827a130 to your computer and use it in GitHub Desktop.
Get World Position from Cursor OnClick Event
use bevy::math::Vec4Swizzles;
use bevy::{prelude::*, render::texture::ImageSettings};
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(Component, Deref)]
pub struct TileHandleHexRow(Handle<Image>);
#[derive(Component, Deref)]
pub struct TileHandleHexCol(Handle<Image>);
#[derive(Component, Deref)]
pub struct TileHandleSquare(Handle<Image>);
#[derive(Component, Deref)]
pub struct TileHandleIso(Handle<Image>);
// Spawns different tiles that are used for this example.
fn spawn_assets(mut commands: Commands, asset_server: Res<AssetServer>) {
let tile_handle_iso: Handle<Image> = asset_server.load("bw-tile-iso.png");
let tile_handle_hex_row: Handle<Image> = asset_server.load("bw-tile-hex-row.png");
let tile_handle_hex_col: Handle<Image> = asset_server.load("bw-tile-hex-col.png");
let tile_handle_square: Handle<Image> = asset_server.load("bw-tile-square.png");
let font: Handle<Font> = asset_server.load("fonts/FiraSans-Bold.ttf");
commands.insert_resource(TileHandleIso(tile_handle_iso));
commands.insert_resource(TileHandleHexCol(tile_handle_hex_col));
commands.insert_resource(TileHandleHexRow(tile_handle_hex_row));
commands.insert_resource(TileHandleSquare(tile_handle_square));
commands.insert_resource(font);
}
// Generates the initial tilemap, which is a square grid.
fn spawn_tilemap(mut commands: Commands, tile_handle_square: Res<TileHandleSquare>) {
commands.spawn_bundle(Camera2dBundle::default());
let total_size = TilemapSize {
x: MAP_SIDE_LENGTH_X,
y: MAP_SIDE_LENGTH_Y,
};
let mut tile_storage = TileStorage::empty(total_size);
let tilemap_entity = commands.spawn().id();
let tilemap_id = TilemapId(tilemap_entity);
fill_tilemap(
TileTexture(0),
total_size,
tilemap_id,
&mut commands,
&mut tile_storage,
);
let tile_size = TILE_SIZE_SQUARE;
let grid_size = GRID_SIZE_SQUARE;
commands
.entity(tilemap_entity)
.insert_bundle(TilemapBundle {
grid_size,
size: total_size,
storage: tile_storage,
texture: TilemapTexture(tile_handle_square.clone()),
tile_size,
map_type: TilemapType::Square {
diagonal_neighbors: false,
},
..Default::default()
});
}
#[derive(Component)]
struct TileLabel;
// 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<Handle<Font>>,
) {
let text_style = TextStyle {
font: font_handle.clone(),
font_size: 20.0,
color: Color::BLACK,
};
let text_alignment = TextAlignment::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);
commands
.entity(*tile_entity)
.insert_bundle(Text2dBundle {
text: Text::from_section(
format!("{}, {}", tile_pos.x, tile_pos.y),
text_style.clone(),
)
.with_alignment(text_alignment),
transform,
..default()
})
.insert(TileLabel);
}
}
}
#[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<Handle<Font>>,
windows: Res<Windows>,
map_type_q: Query<&TilemapType>,
) {
let text_style = TextStyle {
font: font_handle.clone(),
font_size: 20.0,
color: Color::BLACK,
};
let text_alignment = TextAlignment::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_bundle(Text2dBundle {
text: Text::from_section(format!("{map_type:?}"), text_style.clone())
.with_alignment(text_alignment),
transform,
..default()
})
.insert(MapTypeLabel);
}
}
}
// Swaps the map type, when user presses SPACE
#[allow(clippy::too_many_arguments)]
fn swap_map_type(
mut tilemap_query: Query<(
&Transform,
&mut TilemapType,
&mut TilemapGridSize,
&mut TilemapTexture,
&mut TilemapTileSize,
)>,
keyboard_input: Res<Input<KeyCode>>,
mut tile_label_q: Query<
(&TilePos, &mut Transform),
(With<TileLabel>, Without<MapTypeLabel>, Without<TilemapType>),
>,
mut map_type_label_q: Query<
(&mut Text, &mut Transform),
(With<MapTypeLabel>, Without<TileLabel>, Without<TilemapType>),
>,
tile_handle_square: Res<TileHandleSquare>,
tile_handle_hex_row: Res<TileHandleHexRow>,
tile_handle_hex_col: Res<TileHandleHexCol>,
tile_handle_iso: Res<TileHandleIso>,
font_handle: Res<Handle<Font>>,
windows: Res<Windows>,
) {
if keyboard_input.just_pressed(KeyCode::Space) {
for (map_transform, 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 {
diagonal_neighbors: false,
coord_system: IsoCoordSystem::Diamond,
};
*map_texture = TilemapTexture((*tile_handle_iso).clone());
*tile_size = TILE_SIZE_ISO;
*grid_size = GRID_SIZE_ISO;
}
TilemapType::Isometric {
coord_system: IsoCoordSystem::Diamond,
..
} => {
*map_type = TilemapType::Isometric {
diagonal_neighbors: false,
coord_system: IsoCoordSystem::Staggered,
};
*map_texture = TilemapTexture((*tile_handle_iso).clone());
*tile_size = TILE_SIZE_ISO;
*grid_size = GRID_SIZE_ISO;
}
TilemapType::Isometric {
coord_system: IsoCoordSystem::Staggered,
..
} => {
*map_type = TilemapType::Hexagon(HexCoordSystem::Row);
*map_texture = TilemapTexture((*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((*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 {
diagonal_neighbors: false,
};
*map_texture = TilemapTexture((*tile_handle_square).clone());
*tile_size = TILE_SIZE_SQUARE;
*grid_size = GRID_SIZE_SQUARE;
}
}
for (tile_pos, mut tile_label_transform) in tile_label_q.iter_mut() {
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 window in windows.iter() {
for (mut label_text, mut label_transform) in map_type_label_q.iter_mut() {
*label_transform = Transform {
translation: Vec2::new(
-0.5 * window.width() / 2.0,
0.8 * window.height() / 2.0,
)
.extend(1.0),
..Default::default()
};
*label_text = Text::from_section(
format!("{:?}", map_type.as_ref()),
TextStyle {
font: font_handle.clone(),
font_size: 20.0,
color: Color::BLACK,
},
)
.with_alignment(TextAlignment::CENTER);
}
}
}
}
}
#[derive(Component)]
struct HighlightedLabel;
// Converts the cursor position into a world position, taking into account any transforms applied
// the camera.
pub fn cursor_pos_in_world(
windows: &Windows,
cursor_pos: Vec2,
cam_t: &Transform,
cam: &Camera,
) -> Vec3 {
let window = windows.primary();
let window_size = Vec2::new(window.width(), window.height());
// Convert screen position [0..resolution] to ndc [-1..1]
// (ndc = normalized device coordinates)
let ndc_to_world = cam_t.compute_matrix() * cam.projection_matrix().inverse();
let ndc = (cursor_pos / window_size) * 2.0 - Vec2::ONE;
ndc_to_world.project_point3(ndc.extend(0.0))
}
#[derive(Default)]
pub struct CursorPos(Vec3);
// We need to keep the cursor position updated based on any `CursorMoved` events.
pub fn update_cursor_pos(
windows: Res<Windows>,
camera_q: Query<(&Transform, &Camera)>,
mut cursor_moved_events: EventReader<CursorMoved>,
mut cursor_pos: ResMut<CursorPos>,
) {
for cursor_moved in cursor_moved_events.iter() {
// 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() {
*cursor_pos = CursorPos(cursor_pos_in_world(
&windows,
cursor_moved.position,
cam_t,
cam,
));
}
}
}
// 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>>,
mut tile_label_q: Query<&mut Text, (With<TileLabel>, Without<MapTypeLabel>)>,
) {
// Un-highlight any previously highlighted tile labels.
for highlighted_tile_entity in highlighted_tiles_q.iter() {
if let Ok(mut tile_text) = tile_label_q.get_mut(highlighted_tile_entity) {
for mut 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: Vec3 = 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 1.0
let cursor_pos = Vec4::from((cursor_pos, 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 let Ok(mut tile_text) = tile_label_q.get_mut(tile_entity) {
for mut section in tile_text.sections.iter_mut() {
section.style.color = Color::RED;
}
commands.entity(tile_entity).insert(HighlightedLabel);
}
}
}
}
}
fn main() {
App::new()
.insert_resource(WindowDescriptor {
width: 1270.0,
height: 720.0,
title: String::from("Mouse Position to Tile Position"),
..Default::default()
})
.insert_resource(ImageSettings::default_nearest())
// Initialize the cursor pos at some far away place. It will get updated
// correctly when the cursor moves.
.insert_resource(CursorPos(Vec3::new(-100.0, -100.0, 0.0)))
.add_plugins(DefaultPlugins)
.add_plugin(TilemapPlugin)
.add_startup_system_to_stage(StartupStage::PreStartup, spawn_assets)
.add_startup_system_to_stage(StartupStage::Startup, spawn_tilemap)
.add_startup_system_to_stage(StartupStage::PostStartup, spawn_tile_labels)
.add_startup_system_to_stage(StartupStage::PostStartup, spawn_map_type_label)
.add_system_to_stage(CoreStage::First, camera_movement)
.add_system_to_stage(CoreStage::First, update_cursor_pos.after(camera_movement))
.add_system_to_stage(CoreStage::Update, swap_map_type)
.add_system_to_stage(
CoreStage::Update,
highlight_tile_labels.after(swap_map_type),
)
.run();
}
//https://github.com/StarArawn/bevy_ecs_tilemap/blob/de8d005331dde7d9ebb8c4fe1b36769b84e44be5/examples/mouse_to_tile.rs#L291-L306
//from bevy_ecs_tilemap/examples/mouse_to_tile.rs
pub fn cursor_pos_in_world(
windows: &Windows,
cursor_pos: Vec2,
cam_t: &Transform,
cam: &Camera,
) -> Vec3 {
let window = windows.primary();
let window_size = Vec2::new(window.width(), window.height());
// Convert screen position [0..resolution] to ndc [-1..1]
// (ndc = normalized device coordinates)
let ndc_to_world = cam_t.compute_matrix() * cam.projection_matrix().inverse();
let ndc = (cursor_pos / window_size) * 2.0 - Vec2::ONE;
ndc_to_world.project_point3(ndc.extend(0.0))
}
// We need to keep the cursor position updated based on any `CursorMoved` events.
pub fn update_cursor_pos(
windows: Res<Windows>,
camera_q: Query<(&Transform, &Camera)>,
mut cursor_moved_events: EventReader<CursorMoved>,
mut cursor_pos: ResMut<CursorPos>,
) {
for cursor_moved in cursor_moved_events.iter() {
// 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() {
*cursor_pos = CursorPos(cursor_pos_in_world(
&windows,
cursor_moved.position,
cam_t,
cam,
));
}
}
}
// 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>>,
mut tile_label_q: Query<&mut Text, (With<TileLabel>, Without<MapTypeLabel>)>,
) {
// Un-highlight any previously highlighted tile labels.
for highlighted_tile_entity in highlighted_tiles_q.iter() {
if let Ok(mut tile_text) = tile_label_q.get_mut(highlighted_tile_entity) {
for mut 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: Vec3 = 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 1.0
let cursor_pos = Vec4::from((cursor_pos, 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 let Ok(mut tile_text) = tile_label_q.get_mut(tile_entity) {
for mut section in tile_text.sections.iter_mut() {
section.style.color = Color::RED;
}
commands.entity(tile_entity).insert(HighlightedLabel);
}
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment