diff --git a/examples/hexagon_generation.rs b/examples/hexagon_generation.rs new file mode 100644 index 00000000..7a06554c --- /dev/null +++ b/examples/hexagon_generation.rs @@ -0,0 +1,238 @@ +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 a tile to highlight its label (red) and those of its +// neighbors (blue). Press and hold one of keys 0-5 to mark the neighbor in that direction (green). + +// You can increase the MAP_SIDE_LENGTH, in order to test larger maps but just make sure that you run +// in release mode (`cargo run --release --example hexagon_generation`) otherwise things might be too +// slow. +const MAP_SIDE_LENGTH: u32 = 8; + +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 }; + +#[derive(Component, Deref)] +pub struct TileHandleHexRow(Handle); + +#[derive(Component, Deref)] +pub struct TileHandleHexCol(Handle); + +#[derive(Component, Deref)] +pub struct TileHandleSquare(Handle); + +#[derive(Component, Deref)] +pub struct TileHandleIso(Handle); + +// Spawns different tiles that are used for this example. +fn spawn_assets(mut commands: Commands, asset_server: Res) { + let tile_handle_hex_row: Handle = asset_server.load("bw-tile-hex-row.png"); + let tile_handle_hex_col: Handle = asset_server.load("bw-tile-hex-col.png"); + let font: Handle = asset_server.load("fonts/FiraSans-Bold.ttf"); + + commands.insert_resource(TileHandleHexCol(tile_handle_hex_col)); + commands.insert_resource(TileHandleHexRow(tile_handle_hex_row)); + commands.insert_resource(font); +} + +// Generates the initial tilemap, which is a square grid. +fn spawn_tilemap(mut commands: Commands, tile_handle_hex_row: Res) { + commands.spawn_bundle(Camera2dBundle::default()); + + let total_size = TilemapSize { + x: MAP_SIDE_LENGTH, + y: MAP_SIDE_LENGTH, + }; + + let mut tile_storage = TileStorage::empty(total_size); + let tilemap_entity = commands.spawn().id(); + let tilemap_id = TilemapId(tilemap_entity); + + let hex_coord_system = HexCoordSystem::Row; + + fill_tilemap_hexagon( + TileTexture(0), + TilePos { + x: MAP_SIDE_LENGTH / 2, + y: MAP_SIDE_LENGTH / 2, + }, + MAP_SIDE_LENGTH / 2, + hex_coord_system, + tilemap_id, + &mut commands, + &mut tile_storage, + ); + + let tile_size = TILE_SIZE_HEX_ROW; + let grid_size = GRID_SIZE_HEX_ROW; + + commands + .entity(tilemap_entity) + .insert_bundle(TilemapBundle { + grid_size, + size: total_size, + storage: tile_storage, + texture: TilemapTexture(tile_handle_hex_row.clone()), + tile_size, + map_type: TilemapType::Hexagon(hex_coord_system), + ..Default::default() + }); +} + +#[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>, + windows: Res, + 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 commands: Commands, + mut tilemap_query: Query<( + Entity, + &mut TilemapType, + &mut TilemapGridSize, + &mut TilemapTexture, + &mut TilemapTileSize, + &mut TileStorage, + )>, + keyboard_input: Res>, + mut map_type_label_q: Query< + (&mut Text, &mut Transform), + (With, Without), + >, + tile_handle_hex_row: Res, + tile_handle_hex_col: Res, + font_handle: Res>, + windows: Res, +) { + if keyboard_input.just_pressed(KeyCode::Space) { + for ( + map_id, + mut map_type, + mut grid_size, + mut map_texture, + mut tile_size, + mut tile_storage, + ) in tilemap_query.iter_mut() + { + // Remove all previously spawned tiles. + for possible_entity in tile_storage.iter_mut() { + // see documentation for take to understand how it works: + // https://doc.rust-lang.org/std/option/enum.Option.html#method.take + if let Some(entity) = possible_entity.take() { + commands.entity(entity).despawn_recursive(); + } + } + + let new_coord_sys = match map_type.as_ref() { + TilemapType::Hexagon(HexCoordSystem::Row) => HexCoordSystem::Column, + TilemapType::Hexagon(HexCoordSystem::Column) => HexCoordSystem::Row, + _ => unreachable!(), + }; + + *map_type = TilemapType::Hexagon(new_coord_sys); + + if new_coord_sys == HexCoordSystem::Column { + *map_texture = TilemapTexture((*tile_handle_hex_col).clone()); + *tile_size = TILE_SIZE_HEX_COL; + *grid_size = GRID_SIZE_HEX_COL; + } else if new_coord_sys == HexCoordSystem::Row { + *map_texture = TilemapTexture((*tile_handle_hex_row).clone()); + *tile_size = TILE_SIZE_HEX_ROW; + *grid_size = GRID_SIZE_HEX_ROW; + } + + // Re-generate tiles in a hexagonal pattern. + fill_tilemap_hexagon( + TileTexture(0), + TilePos { + x: MAP_SIDE_LENGTH / 2, + y: MAP_SIDE_LENGTH / 2, + }, + MAP_SIDE_LENGTH / 2, + new_coord_sys, + TilemapId(map_id), + &mut commands, + &mut tile_storage, + ); + + 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); + } + } + } + } +} + +fn main() { + App::new() + .insert_resource(WindowDescriptor { + width: 1270.0, + height: 720.0, + title: String::from("Generating a hexagonal hex map"), + ..Default::default() + }) + .insert_resource(ImageSettings::default_nearest()) + .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_map_type_label) + .add_system(camera_movement) + .add_system(swap_map_type) + .run(); +} diff --git a/src/helpers/filling.rs b/src/helpers/filling.rs index 94174efe..905ff3eb 100644 --- a/src/helpers/filling.rs +++ b/src/helpers/filling.rs @@ -1,4 +1,7 @@ +use crate::helpers::hex_grid::axial::AxialPos; +use crate::helpers::hex_grid::neighbors::{HexDirection, HEX_DIRECTIONS}; use crate::map::TilemapId; +use crate::prelude::HexCoordSystem; use crate::tiles::{TileBundle, TileColor, TilePos, TileTexture}; use crate::{TileStorage, TilemapSize}; use bevy::hierarchy::BuildChildren; @@ -97,3 +100,78 @@ pub fn fill_tilemap_rect_color( } } } + +/// Generates a vector of hex positions that form a ring of given `radius` around the specified +/// `origin`. +/// +/// If `radius` is zero, `origin` is the only position in the returned vector. +pub fn generate_hex_ring(origin: AxialPos, radius: u32) -> Vec { + if radius == 0 { + vec![origin] + } else { + let mut ring = Vec::with_capacity((radius * 6) as usize); + let corners = HEX_DIRECTIONS + .iter() + .map(|direction| origin + radius * AxialPos::from(direction)) + .collect::>(); + // The "tangent" is the direction we must travel in to reach the next corner + let tangents = (0..6) + .map(|ix| HexDirection::from(ix + 2).into()) + .collect::>(); + + for (&corner, &tangent) in corners.iter().zip(tangents.iter()) { + for k in 0..radius { + ring.push(corner + k * tangent); + } + } + + ring + } +} + +/// Generates a vector of hex positions that form a hexagon of given `radius` around the specified +/// `origin`. +pub fn generate_hexagon(origin: AxialPos, radius: u32) -> Vec { + let mut hexagon = Vec::with_capacity((6 * radius * (radius + 1) / 2) as usize); + for r in 0..radius { + hexagon.extend(generate_hex_ring(origin, r)); + } + hexagon +} + +/// Fills a hexagonal region with the given `tile_texture`. +/// +/// The rectangular region is defined by an `origin` in [`TilePos`](crate::tiles::TilePos), and a +/// `radius`. +/// +/// Tiles that do not fit in the tilemap will not be created. +pub fn fill_tilemap_hexagon( + tile_texture: TileTexture, + origin: TilePos, + radius: u32, + hex_coord_system: HexCoordSystem, + tilemap_id: TilemapId, + commands: &mut Commands, + tile_storage: &mut TileStorage, +) { + let tile_positions = generate_hexagon( + AxialPos::from_tile_pos_given_coord_system(&origin, hex_coord_system), + radius, + ) + .into_iter() + .map(|axial_pos| axial_pos.as_tile_pos_given_coord_system(hex_coord_system)) + .collect::>(); + + for tile_pos in tile_positions { + let tile_entity = commands + .spawn() + .insert_bundle(TileBundle { + position: tile_pos, + tilemap_id, + texture: tile_texture, + ..Default::default() + }) + .id(); + tile_storage.checked_set(&tile_pos, tile_entity) + } +} diff --git a/src/helpers/hex_grid/axial.rs b/src/helpers/hex_grid/axial.rs index a0307622..b7108e2f 100644 --- a/src/helpers/hex_grid/axial.rs +++ b/src/helpers/hex_grid/axial.rs @@ -82,6 +82,17 @@ impl Mul for i32 { } } +impl Mul for u32 { + type Output = AxialPos; + + fn mul(self, rhs: AxialPos) -> Self::Output { + AxialPos { + q: (self as i32) * rhs.q, + r: (self as i32) * rhs.r, + } + } +} + fn ceiled_division_by_2(x: i32) -> i32 { if x < 0 { (x - 1) / 2 @@ -365,3 +376,12 @@ impl From for FractionalAxialPos { FractionalAxialPos { q: v.x, r: v.y } } } + +impl From for FractionalAxialPos { + fn from(axial_pos: AxialPos) -> Self { + FractionalAxialPos { + q: axial_pos.q as f32, + r: axial_pos.r as f32, + } + } +} diff --git a/src/helpers/hex_grid/cube.rs b/src/helpers/hex_grid/cube.rs index a6c9a560..4f66f3b6 100644 --- a/src/helpers/hex_grid/cube.rs +++ b/src/helpers/hex_grid/cube.rs @@ -76,6 +76,18 @@ impl Mul for i32 { } } +impl Mul for u32 { + type Output = CubePos; + + fn mul(self, rhs: CubePos) -> Self::Output { + CubePos { + q: (self as i32) * rhs.q, + r: (self as i32) * rhs.r, + s: (self as i32) * rhs.s, + } + } +} + impl CubePos { /// The magnitude of a cube position is its distance away from the `[0, 0, 0]` hex_grid. /// diff --git a/src/helpers/hex_grid/neighbors.rs b/src/helpers/hex_grid/neighbors.rs index d092d99d..625ab369 100644 --- a/src/helpers/hex_grid/neighbors.rs +++ b/src/helpers/hex_grid/neighbors.rs @@ -44,6 +44,18 @@ pub const HEX_OFFSETS: [AxialPos; 6] = [ AxialPos { q: 1, r: -1 }, ]; +impl From for AxialPos { + fn from(direction: HexDirection) -> Self { + HEX_OFFSETS[direction as usize] + } +} + +impl From<&HexDirection> for AxialPos { + fn from(direction: &HexDirection) -> Self { + AxialPos::from(*direction) + } +} + impl From for HexDirection { fn from(choice: usize) -> Self { let ix = choice % 6;