Created
August 27, 2024 23:43
-
-
Save ChristopherBiscardi/d0c6d6032432d94a696c92c0c1739ca2 to your computer and use it in GitHub Desktop.
center-of-tile bevy_ecs_tilemap
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() { |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment