Skip to content

Instantly share code, notes, and snippets.

@ChristopherBiscardi
Created August 27, 2024 23:43
Show Gist options
  • Save ChristopherBiscardi/d0c6d6032432d94a696c92c0c1739ca2 to your computer and use it in GitHub Desktop.
Save ChristopherBiscardi/d0c6d6032432d94a696c92c0c1739ca2 to your computer and use it in GitHub Desktop.
center-of-tile bevy_ecs_tilemap
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() {
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