Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generating hexagonal hex maps. #296

Merged
merged 4 commits into from
Oct 3, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
240 changes: 240 additions & 0 deletions examples/hexagon_generation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
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(
"Hexagon Neighbors - Hover over a tile, and then press 0-5 to mark neighbors",
bzm3r marked this conversation as resolved.
Show resolved Hide resolved
),
..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(
bzm3r marked this conversation as resolved.
Show resolved Hide resolved
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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will be measurably faster with spawn_batch. Probably want to change the sibling to match? Or just open an issue over it.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See: #300

.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