Skip to content

Commit

Permalink
Merge pull request #296 from StarArawn/hex-generation
Browse files Browse the repository at this point in the history
Generating hexagonal hex maps.
  • Loading branch information
bzm3r committed Oct 3, 2022
2 parents 5cd3988 + 089dcde commit 9ab8702
Show file tree
Hide file tree
Showing 5 changed files with 360 additions and 0 deletions.
238 changes: 238 additions & 0 deletions examples/hexagon_generation.rs
Original file line number Diff line number Diff line change
@@ -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<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_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 font: Handle<Font> = 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<TileHandleHexRow>) {
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<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 commands: Commands,
mut tilemap_query: Query<(
Entity,
&mut TilemapType,
&mut TilemapGridSize,
&mut TilemapTexture,
&mut TilemapTileSize,
&mut TileStorage,
)>,
keyboard_input: Res<Input<KeyCode>>,
mut map_type_label_q: Query<
(&mut Text, &mut Transform),
(With<MapTypeLabel>, Without<TilemapType>),
>,
tile_handle_hex_row: Res<TileHandleHexRow>,
tile_handle_hex_col: Res<TileHandleHexCol>,
font_handle: Res<Handle<Font>>,
windows: Res<Windows>,
) {
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();
}
78 changes: 78 additions & 0 deletions src/helpers/filling.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<AxialPos> {
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::<Vec<AxialPos>>();
// 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::<Vec<AxialPos>>();

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<AxialPos> {
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::<Vec<TilePos>>();

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)
}
}
20 changes: 20 additions & 0 deletions src/helpers/hex_grid/axial.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,17 @@ impl Mul<AxialPos> for i32 {
}
}

impl Mul<AxialPos> 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
Expand Down Expand Up @@ -365,3 +376,12 @@ impl From<Vec2> for FractionalAxialPos {
FractionalAxialPos { q: v.x, r: v.y }
}
}

impl From<AxialPos> for FractionalAxialPos {
fn from(axial_pos: AxialPos) -> Self {
FractionalAxialPos {
q: axial_pos.q as f32,
r: axial_pos.r as f32,
}
}
}
12 changes: 12 additions & 0 deletions src/helpers/hex_grid/cube.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,18 @@ impl Mul<CubePos> for i32 {
}
}

impl Mul<CubePos> 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.
///
Expand Down
12 changes: 12 additions & 0 deletions src/helpers/hex_grid/neighbors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,18 @@ pub const HEX_OFFSETS: [AxialPos; 6] = [
AxialPos { q: 1, r: -1 },
];

impl From<HexDirection> 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<usize> for HexDirection {
fn from(choice: usize) -> Self {
let ix = choice % 6;
Expand Down

0 comments on commit 9ab8702

Please sign in to comment.