diff --git a/examples/hex_neighbors_radius_chunks.rs b/examples/hex_neighbors_radius_chunks.rs new file mode 100644 index 00000000..a6211bfb --- /dev/null +++ b/examples/hex_neighbors_radius_chunks.rs @@ -0,0 +1,560 @@ +use bevy::{math::Vec4Swizzles, prelude::*}; +use bevy_ecs_tilemap::{helpers::hex_grid::offset::*, prelude::*}; +mod helpers; +use helpers::camera::movement as camera_movement; + +const CHUNK_MAP_SIDE_LENGTH_X: u32 = 4; +const CHUNK_MAP_SIDE_LENGTH_Y: u32 = 4; + +const CHUNKS_X: i32 = 2; +const CHUNKS_Y: i32 = 2; + +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_HEX_ROW: TilemapGridSize = TilemapGridSize { x: 50.0, y: 58.0 }; +const GRID_SIZE_HEX_COL: TilemapGridSize = TilemapGridSize { x: 58.0, y: 50.0 }; + +const MAX_RADIUS: u32 = 6; + +#[derive(Deref, Resource)] +pub struct TileHandleHexRow(Handle); + +#[derive(Deref, Resource)] +pub struct TileHandleHexCol(Handle); +#[derive(Deref, Resource)] +pub struct FontHandle(Handle); + +impl FromWorld for TileHandleHexCol { + fn from_world(world: &mut World) -> Self { + let asset_server = world.resource::(); + 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::(); + Self(asset_server.load("bw-tile-hex-row.png")) + } +} +impl FromWorld for FontHandle { + fn from_world(world: &mut World) -> Self { + let asset_server = world.resource::(); + Self(asset_server.load("fonts/FiraSans-Bold.ttf")) + } +} + +#[derive(Deref, Component, Clone, Copy)] +pub struct ChunkPos(IVec2); + +fn chunk_in_world_position(pos: IVec2, map_type: TilemapType) -> Vec3 { + let tile_size = match map_type { + TilemapType::Hexagon(HexCoordSystem::RowEven) + | TilemapType::Hexagon(HexCoordSystem::RowOdd) => TILE_SIZE_HEX_ROW, + TilemapType::Hexagon(HexCoordSystem::ColumnEven) + | TilemapType::Hexagon(HexCoordSystem::ColumnOdd) => TILE_SIZE_HEX_COL, + _ => unreachable!(), + }; + let grid_size = match map_type { + TilemapType::Hexagon(HexCoordSystem::RowEven) + | TilemapType::Hexagon(HexCoordSystem::RowOdd) => GRID_SIZE_HEX_ROW, + TilemapType::Hexagon(HexCoordSystem::ColumnEven) + | TilemapType::Hexagon(HexCoordSystem::ColumnOdd) => GRID_SIZE_HEX_COL, + _ => unreachable!(), + }; + if matches!( + map_type, + TilemapType::Hexagon(HexCoordSystem::RowEven) + | TilemapType::Hexagon(HexCoordSystem::RowOdd) + ) { + Vec3::new( + tile_size.x * CHUNK_MAP_SIDE_LENGTH_X as f32 * pos.x as f32, + TilePos { + x: 0, + y: CHUNK_MAP_SIDE_LENGTH_Y, + } + .center_in_world(&grid_size, &map_type) + .y * pos.y as f32, + 0.0, + ) + } else if matches!( + map_type, + TilemapType::Hexagon(HexCoordSystem::ColumnEven) + | TilemapType::Hexagon(HexCoordSystem::ColumnOdd) + ) { + Vec3::new( + TilePos { + x: CHUNK_MAP_SIDE_LENGTH_X, + y: 0, + } + .center_in_world(&grid_size, &map_type) + .x * pos.x as f32, + tile_size.y * CHUNK_MAP_SIDE_LENGTH_Y as f32 * pos.y as f32, + 0.0, + ) + } else { + unreachable!() + } +} + +fn hex_pos_from_tile_pos( + tile_pos: &TilePos, + grid_size: &TilemapGridSize, + map_type: &TilemapType, + map_transform: &Transform, +) -> IVec2 { + let tile_translation = + *map_transform * tile_pos.center_in_world(grid_size, map_type).extend(0.0); + match map_type { + TilemapType::Hexagon(HexCoordSystem::RowEven) => { + let pos = RowEvenPos::from_world_pos(&tile_translation.truncate(), grid_size); + IVec2 { x: pos.q, y: pos.r } + } + TilemapType::Hexagon(HexCoordSystem::RowOdd) => { + let pos = RowOddPos::from_world_pos(&tile_translation.truncate(), grid_size); + IVec2 { x: pos.q, y: pos.r } + } + TilemapType::Hexagon(HexCoordSystem::ColumnEven) => { + let pos = ColEvenPos::from_world_pos(&tile_translation.truncate(), grid_size); + IVec2 { x: pos.q, y: pos.r } + } + TilemapType::Hexagon(HexCoordSystem::ColumnOdd) => { + let pos = ColOddPos::from_world_pos(&tile_translation.truncate(), grid_size); + IVec2 { x: pos.q, y: pos.r } + } + _ => unreachable!(), + } +} + +fn hex_neighbors_radius(hex_pos: IVec2, radius: u32, map_type: &TilemapType) -> Vec { + let neighbors = generate_hexagon( + match map_type { + TilemapType::Hexagon(HexCoordSystem::RowEven) => RowEvenPos { + q: hex_pos.x, + r: hex_pos.y, + } + .into(), + TilemapType::Hexagon(HexCoordSystem::RowOdd) => RowOddPos { + q: hex_pos.x, + r: hex_pos.y, + } + .into(), + TilemapType::Hexagon(HexCoordSystem::ColumnEven) => ColEvenPos { + q: hex_pos.x, + r: hex_pos.y, + } + .into(), + TilemapType::Hexagon(HexCoordSystem::ColumnOdd) => ColOddPos { + q: hex_pos.x, + r: hex_pos.y, + } + .into(), + _ => unreachable!(), + }, + radius, + ); + neighbors + .into_iter() + .map(|axial_pos| match map_type { + TilemapType::Hexagon(HexCoordSystem::RowEven) => { + let pos = RowEvenPos::from(axial_pos); + IVec2 { x: pos.q, y: pos.r } + } + TilemapType::Hexagon(HexCoordSystem::RowOdd) => { + let pos = RowOddPos::from(axial_pos); + IVec2 { x: pos.q, y: pos.r } + } + TilemapType::Hexagon(HexCoordSystem::ColumnEven) => { + let pos = ColEvenPos::from(axial_pos); + IVec2 { x: pos.q, y: pos.r } + } + TilemapType::Hexagon(HexCoordSystem::ColumnOdd) => { + let pos = ColOddPos::from(axial_pos); + IVec2 { x: pos.q, y: pos.r } + } + _ => unreachable!(), + }) + .collect() +} + +fn hex_neighbors_radius_from_tile_pos( + tile_pos: &TilePos, + grid_size: &TilemapGridSize, + map_type: &TilemapType, + map_transform: &Transform, + radius: u32, +) -> Vec { + let hex_pos = hex_pos_from_tile_pos(tile_pos, grid_size, map_type, map_transform); + hex_neighbors_radius(hex_pos, radius, map_type) +} + +fn spawn_chunks(mut commands: Commands, tile_handle_hex_row: Res) { + commands.spawn(Camera2dBundle::default()); + + let map_size = TilemapSize { + x: CHUNK_MAP_SIDE_LENGTH_X, + y: CHUNK_MAP_SIDE_LENGTH_Y, + }; + + let tile_size = TILE_SIZE_HEX_ROW; + let grid_size = GRID_SIZE_HEX_ROW; + let map_type = TilemapType::Hexagon(HexCoordSystem::RowEven); + + // Makes it so chunks spawn around the world center + let lower_bound_x = -(CHUNKS_X / 2); + let lower_bound_y = -(CHUNKS_Y / 2); + for chunk_x in lower_bound_x..(CHUNKS_X + lower_bound_x) { + for chunk_y in lower_bound_y..(CHUNKS_Y + lower_bound_y) { + let chunk_pos = ChunkPos(IVec2 { + x: chunk_x, + y: chunk_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, + ); + + commands + .entity(tilemap_entity) + .insert(TilemapBundle { + grid_size, + size: map_size, + storage: tile_storage, + texture: TilemapTexture::Single(tile_handle_hex_row.clone()), + tile_size, + map_type, + transform: Transform::from_translation(chunk_in_world_position( + *chunk_pos, map_type, + )), + ..Default::default() + }) + .insert(chunk_pos); + } + } +} + +fn swap_map_type( + mut tilemap_query: Query<( + &mut Transform, + &mut TilemapType, + &mut TilemapGridSize, + &mut TilemapTexture, + &mut TilemapTileSize, + &TileStorage, + &ChunkPos, + )>, + keyboard_input: Res>, + tile_label_q: Query<(Entity, &TileLabel, &TilePos), Without>, + mut transform_q: Query<(&mut Transform, &mut Text), Without>, + tile_handle_hex_row: Res, + tile_handle_hex_col: Res, +) { + if keyboard_input.just_pressed(KeyCode::Space) { + for ( + mut map_transform, + mut map_type, + mut grid_size, + mut map_texture, + mut tile_size, + tile_storage, + chunk_pos, + ) in tilemap_query.iter_mut() + { + match map_type.as_ref() { + TilemapType::Hexagon(HexCoordSystem::RowEven) => { + *map_type = TilemapType::Hexagon(HexCoordSystem::RowOdd) + } + TilemapType::Hexagon(HexCoordSystem::RowOdd) => { + *map_type = TilemapType::Hexagon(HexCoordSystem::ColumnEven); + *map_texture = TilemapTexture::Single((*tile_handle_hex_col).clone()); + *tile_size = TILE_SIZE_HEX_COL; + *grid_size = GRID_SIZE_HEX_COL; + } + TilemapType::Hexagon(HexCoordSystem::ColumnEven) => { + *map_type = TilemapType::Hexagon(HexCoordSystem::ColumnOdd); + } + TilemapType::Hexagon(HexCoordSystem::ColumnOdd) => { + *map_type = TilemapType::Hexagon(HexCoordSystem::RowEven); + *map_texture = TilemapTexture::Single((*tile_handle_hex_row).clone()); + *tile_size = TILE_SIZE_HEX_ROW; + *grid_size = GRID_SIZE_HEX_ROW; + } + _ => unreachable!(), + } + + *map_transform = + Transform::from_translation(chunk_in_world_position(**chunk_pos, *map_type)); + + for (tile_entity, label, tile_pos) in tile_label_q.iter() { + if let Ok((mut tile_label_transform, mut tile_label_text)) = + transform_q.get_mut(label.0) + { + if let Some(ent) = tile_storage.checked_get(tile_pos) { + if ent == tile_entity { + 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); + let hex_pos = hex_pos_from_tile_pos( + tile_pos, + &grid_size, + &map_type, + &map_transform, + ); + tile_label_text.sections.get_mut(0).unwrap().value = + format!("{}, {}", hex_pos.x, hex_pos.y); + } + } + } + } + } + } +} + +#[derive(Component)] +struct TileLabel(Entity); + +fn spawn_tile_labels( + mut commands: Commands, + tilemap_q: Query<(&Transform, &TilemapType, &TilemapGridSize, &TileStorage)>, + tile_q: Query<&TilePos>, + font_handle: Res, +) { + 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); + + let hex_pos = hex_pos_from_tile_pos(tile_pos, grid_size, map_type, map_transform); + + let label_entity = commands + .spawn(Text2dBundle { + text: Text::from_section( + format!("{}, {}", hex_pos.x, hex_pos.y), + text_style.clone(), + ) + .with_alignment(text_alignment), + transform, + ..default() + }) + .id(); + commands + .entity(*tile_entity) + .insert(TileLabel(label_entity)); + } + } +} + +#[derive(Component)] +struct Hovered; + +#[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)) + } +} + +pub fn update_cursor_pos( + camera_q: Query<(&GlobalTransform, &Camera)>, + mut cursor_moved_events: EventReader, + mut cursor_pos: ResMut, +) { + 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() { + if let Some(pos) = cam.viewport_to_world_2d(cam_t, cursor_moved.position) { + *cursor_pos = CursorPos(pos); + } + } + } +} + +// This is where we check whicch tile the cursor is hovered over. +fn hover_highlight_tile_label( + mut commands: Commands, + cursor_pos: Res, + tilemap_q: Query<( + &TilemapSize, + &TilemapGridSize, + &TilemapType, + &TileStorage, + &Transform, + )>, + highlighted_tiles_q: Query>, + tile_label_q: Query<&TileLabel>, + mut text_q: Query<&mut Text>, +) { + // 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 mut section in tile_text.sections.iter_mut() { + section.style.color = Color::BLACK; + } + commands.entity(highlighted_tile_entity).remove::(); + } + } + } + + for (map_size, grid_size, map_type, tile_storage, map_transform) in tilemap_q.iter() { + let cursor_pos = cursor_pos.0; + let cursor_pos_in_map_pos = { + 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() + }; + if let Some(tile_pos) = + TilePos::from_world_pos(&cursor_pos_in_map_pos, map_size, grid_size, map_type) + { + if let Some(tile_entity) = tile_storage.get(&tile_pos) { + if let Ok(label) = tile_label_q.get(tile_entity) { + if let Ok(mut tile_text) = text_q.get_mut(label.0) { + for mut section in tile_text.sections.iter_mut() { + section.style.color = Color::RED; + } + commands.entity(tile_entity).insert(Hovered); + } + } + } + } + } +} + +#[derive(Deref, Resource)] +struct HighlightRadius(u32); +impl Default for HighlightRadius { + fn default() -> Self { + Self(2) + } +} + +fn update_radius(mut radius: ResMut, keyboard_input: Res>) { + if keyboard_input.just_pressed(KeyCode::Up) { + radius.0 = std::cmp::min(MAX_RADIUS, radius.0 + 1); + } else if keyboard_input.just_pressed(KeyCode::Down) { + radius.0 = std::cmp::max(1, radius.0 - 1); + } +} + +#[derive(Component)] +struct NeighborHighlight; + +// Highlight neighbors of a tile in a radius +fn highlight_neighbor_labels( + mut commands: Commands, + tilemap_query: Query<(&TilemapType, &TilemapGridSize, &TileStorage, &Transform)>, + highlighted_tiles_q: Query>, + hovered_tiles_q: Query<(Entity, &TilePos), With>, + tiles_q: Query<&TilePos, Without>, + tile_label_q: Query<&TileLabel>, + mut text_q: Query<&mut Text>, + radius: Res, +) { + 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 mut section in tile_text.sections.iter_mut() { + section.style.color = Color::BLACK; + } + commands + .entity(highlighted_tile_entity) + .remove::(); + } + } + } + + let mut neighbors: Option> = None; + + for (map_type, grid_size, tile_storage, map_t) in tilemap_query.iter() { + for (hovered_tile_entity, hovered_tile_pos) in hovered_tiles_q.iter() { + if let Some(ent) = tile_storage.checked_get(hovered_tile_pos) { + if ent == hovered_tile_entity { + neighbors = Some(hex_neighbors_radius_from_tile_pos( + hovered_tile_pos, + grid_size, + map_type, + map_t, + **radius, + )); + } + } + } + } + + if let Some(neighbors) = neighbors { + for (map_type, grid_size, tile_storage, map_t) in tilemap_query.iter() { + for opt_tile_entity in tile_storage.iter() { + if let Some(tile_entity) = opt_tile_entity { + if let Ok(tile_pos) = tiles_q.get(*tile_entity) { + let tile_hex_pos = + hex_pos_from_tile_pos(tile_pos, grid_size, map_type, map_t); + if neighbors.contains(&tile_hex_pos) { + if let Ok(label) = tile_label_q.get(*tile_entity) { + if let Ok(mut tile_text) = text_q.get_mut(label.0) { + for mut section in tile_text.sections.iter_mut() { + section.style.color = Color::BLUE; + } + commands.entity(*tile_entity).insert(NeighborHighlight); + } + } + } + } + } + } + } + } +} + +#[derive(SystemSet, Clone, Copy, Hash, PartialEq, Eq, Debug)] +pub struct SpawnChunksSet; + +fn main() { + App::new() + .add_plugins( + DefaultPlugins + .set(WindowPlugin { + primary_window: Some(Window { + title: String::from( + "Hexagon radius - Hover over a tile to higlight tiles in a radius, use up and down arrow to change radius", + ), + ..Default::default() + }), + ..default() + }) + .set(ImagePlugin::default_nearest()), + ) + .add_plugin(TilemapPlugin) + .init_resource::() + .init_resource::() + .init_resource::() + .init_resource::() + .init_resource::() + .add_startup_systems((spawn_chunks, apply_system_buffers).chain().in_set(SpawnChunksSet)) + .add_startup_system(spawn_tile_labels.after(SpawnChunksSet)) + .add_systems((camera_movement, update_cursor_pos).chain().in_base_set(CoreSet::First)) + .add_system(swap_map_type) + .add_system(hover_highlight_tile_label.after(swap_map_type)) + .add_system(update_radius.after(hover_highlight_tile_label)) + .add_system(highlight_neighbor_labels.after(update_radius)) + .run(); +}