Skip to content

Commit

Permalink
Follow up fixes and improvements to new directions (#157)
Browse files Browse the repository at this point in the history
> Follow up to #154 and #156 

I identified mutlitple issues in layout management were due to the fact
that `hexx` neighbors and directions were following a counter clockwise
order. This was causing some problems in angle management and visual
errors on pointy layouts.

Therefore, breaking change: From now on the hexx neighbors and
directions will follow the same order of the redblobgames docs, a
clockwise order.

I also added a new example, `hex_area` to reproduce one of the issues
found in #154 which also showcases how to use two different layouts in a
same grid.
  • Loading branch information
ManevilleF authored Mar 25, 2024
1 parent c37d360 commit e51bc9b
Show file tree
Hide file tree
Showing 16 changed files with 584 additions and 262 deletions.
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,19 @@
* Removed methods deprecated in previous versions
* Added `z` field in the `Debug` impl of `Hex` (#156)
* Added `xyz` fields in the `Debug` impl of directions (#156)
* (**BREAKING**) Hex neighbors are now following a clockwise order (#157)
* (**BREAKING**) Hex diagonal neighbors are now following a clockwise order (#157)
* Added new `hex_area` example

### New grid utilities (#154)

* Added new `grid` feature gate
* Added `GridVertex` and `GridEgde` types, representing oriented grid vertices
and edges

### New directions (#156)
### New directions (#156, #157)

* (**BREAKING**) Direction types are now following a clockwise order
* (**BREAKING**) Renamed `Direction` to `EdgeDirection`, and is no longer an enum.
Instead of the oriented variants use associated const values:
* `Direction::TopRight` -> `EdgeDirection::FLAT_TOP_RIGHT` or `EdgeDirection::POINTY_RIGHT`
Expand Down
5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,16 @@ features = ["html_reports"]
rand = "0.8"
bevy-inspector-egui = "0.23"
bevy_egui = "0.25"
approx = "0.5"

[[example]]
name = "hex_grid"
path = "examples/hex_grid.rs"

[[example]]
name = "hex_area"
path = "examples/hex_area.rs"

[[example]]
name = "scroll_map"
path = "examples/scroll_map.rs"
Expand Down
18 changes: 14 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,9 @@

* `hexx = { version = "0.15", features = ["packed"] }`

`hexx` supports [Bevy Reflection](https://docs.rs/bevy_reflect/latest/bevy_reflect) through the
`bevy_reflect` feature. To enable it add the following line to your
`Cargo.toml`:
`hexx` supports [Bevy Reflection](https://docs.rs/bevy_reflect/latest/bevy_reflect)
through the `bevy_reflect` feature. To enable it add the following line to
your `Cargo.toml`:

* `hexx = { version = "0.15", features = ["bevy_reflect"] }`

Expand Down Expand Up @@ -138,7 +138,8 @@
It can be used for boundary and interesection checks but also for wrapping
coordinates.
Coordinate wrapping transform a point outside of the bounds to a point
inside. This allows for seamless or repeating [wraparound](https://www.redblobgames.com/grids/hexagons/#wraparound) maps.
inside. This allows for seamless or repeating [wraparound](https://www.redblobgames.com/grids/hexagons/#wraparound)
maps.

```rust
use hexx::*;
Expand Down Expand Up @@ -250,6 +251,15 @@

This example showcases hex ranges, rings, wedges, rotation, and lines

### Hex Area

![hex_grid](docs/hex_area.png "hex area example")

> `cargo run --example hex_area`

This example showcases how to generate hexagonal areas using grid utilities and gizmos
and how to use two layouts on the same grid.

### Scroll Map

![scroll_map](docs/scroll_map.gif "scroll map example")
Expand Down
Binary file added docs/hex_area.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
240 changes: 240 additions & 0 deletions examples/hex_area.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
use bevy::{
prelude::*,
render::{mesh::Indices, render_asset::RenderAssetUsages, render_resource::PrimitiveTopology},
utils::HashSet,
window::PrimaryWindow,
};
use hexx::*;
use std::collections::HashMap;

/// World size of the hexagons (outer radius)
const HEX_SIZE: Vec2 = Vec2::splat(15.0);

pub fn main() {
App::new()
.init_resource::<HexArea>()
.add_plugins(DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
resolution: (1_920.0, 1_080.0).into(),
..default()
}),
..default()
}))
.add_systems(Startup, (setup_camera, setup_grid))
.add_systems(Update, (handle_input, gizmos).chain())
.run();
}

#[derive(Debug, Default, Resource)]
struct HexArea {
pub area: HashSet<Hex>,
}

#[derive(Debug, Resource)]
struct Map {
flat_layout: HexLayout,
pointy_layout: HexLayout,
flat_entities: HashMap<Hex, Entity>,
pointy_entities: HashMap<Hex, Entity>,
flat_cursor_entity: Entity,
pointy_cursor_entity: Entity,
area_material: Handle<ColorMaterial>,
default_material: Handle<ColorMaterial>,
}

/// 3D Orthogrpahic camera setup
fn setup_camera(mut commands: Commands) {
commands.spawn(Camera2dBundle::default());
}

/// Hex grid setup
fn setup_grid(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<ColorMaterial>>,
) {
let flat_layout = HexLayout {
hex_size: HEX_SIZE,
orientation: HexOrientation::Flat,
origin: Vec2::new(-480.0, 0.0),
..default()
};
let pointy_layout = HexLayout {
hex_size: HEX_SIZE,
orientation: HexOrientation::Pointy,
origin: Vec2::new(480.0, 0.0),
..default()
};
// materials
let area_material = materials.add(Color::GOLD);
let default_material = materials.add(Color::WHITE);
let cursor_material = materials.add(Color::RED);

// mesh
let mut spawn_map = |layout: &HexLayout| {
let mesh_handle = meshes.add(hexagonal_plane(layout));
let cursor_mesh = meshes.add(border_plane(layout));

let cursor_entity = commands
.spawn(ColorMesh2dBundle {
mesh: cursor_mesh.into(),
material: cursor_material.clone(),
transform: Transform::from_xyz(0.0, 0.0, 10.0),
..default()
})
.id();
let entities = Hex::ZERO
.range(15)
.map(|hex| {
let pos = layout.hex_to_world_pos(hex);
let id = commands
.spawn(ColorMesh2dBundle {
transform: Transform::from_xyz(pos.x, pos.y, 0.0),
mesh: mesh_handle.clone().into(),
material: default_material.clone(),
..default()
})
.id();
(hex, id)
})
.collect();
(cursor_entity, entities)
};

let (flat_cursor_entity, flat_entities) = spawn_map(&flat_layout);
let (pointy_cursor_entity, pointy_entities) = spawn_map(&pointy_layout);
commands.insert_resource(Map {
flat_layout,
pointy_layout,
flat_entities,
pointy_entities,
flat_cursor_entity,
pointy_cursor_entity,
area_material,
default_material,
});
}

/// Input interaction
fn handle_input(
mut commands: Commands,
windows: Query<&Window, With<PrimaryWindow>>,
cameras: Query<(&Camera, &GlobalTransform)>,
map: Res<Map>,
mut area: ResMut<HexArea>,
mouse: Res<ButtonInput<MouseButton>>,
mut cursors: Query<&mut Transform>,
) {
let window = windows.single();
let (camera, cam_transform) = cameras.single();
let Some(pos) = window
.cursor_position()
.and_then(|p| camera.viewport_to_world_2d(cam_transform, p))
else {
return;
};
let mut to_add = Vec::new();
let mut to_remove = Vec::new();
for (layout, entities, cursor) in [
(
&map.flat_layout,
&map.flat_entities,
&map.flat_cursor_entity,
),
(
&map.pointy_layout,
&map.pointy_entities,
&map.pointy_cursor_entity,
),
] {
let coord = layout.world_pos_to_hex(pos);
if entities.get(&coord).is_none() {
continue;
};
let mut cursor = cursors.get_mut(*cursor).unwrap();
let pos = layout.hex_to_world_pos(coord);
cursor.translation.x = pos.x;
cursor.translation.y = pos.y;
if mouse.pressed(MouseButton::Left) {
to_add.push(coord);
} else if mouse.pressed(MouseButton::Right) {
to_remove.push(coord);
}
}
for coord in to_add {
area.area.insert(coord);
let entity = map.flat_entities.get(&coord).unwrap();
commands.entity(*entity).insert(map.area_material.clone());
let entity = map.pointy_entities.get(&coord).unwrap();
commands.entity(*entity).insert(map.area_material.clone());
}
for coord in to_remove {
area.area.remove(&coord);
let entity = map.flat_entities.get(&coord).unwrap();
commands
.entity(*entity)
.insert(map.default_material.clone());
let entity = map.pointy_entities.get(&coord).unwrap();
commands
.entity(*entity)
.insert(map.default_material.clone());
}
}

fn gizmos(mut gizmos: Gizmos, area: Res<HexArea>, map: Res<Map>) {
let mut edges = Vec::new();
for hex in &area.area {
for neighbor in hex
.all_neighbors()
.iter()
.filter(|c| !area.area.contains(*c))
{
edges.push(GridEdge {
origin: *hex,
direction: hex.neighbor_direction(*neighbor).unwrap(),
});
}
}
for layout in [&map.flat_layout, &map.pointy_layout] {
for edge in &edges {
let [a, b] = layout.edge_coordinates(*edge);
gizmos.line_2d(a, b, Color::ORANGE);
}
}
}

/// Compute a bevy mesh from the layout
fn hexagonal_plane(hex_layout: &HexLayout) -> Mesh {
let mesh_info = PlaneMeshBuilder::new(hex_layout)
.facing(Vec3::Z)
.center_aligned()
.build();
Mesh::new(
PrimitiveTopology::TriangleList,
RenderAssetUsages::RENDER_WORLD,
)
.with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, mesh_info.vertices)
.with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, mesh_info.normals)
.with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, mesh_info.uvs)
.with_inserted_indices(Indices::U16(mesh_info.indices))
}

fn border_plane(hex_layout: &HexLayout) -> Mesh {
let mesh_info = PlaneMeshBuilder::new(hex_layout)
.facing(Vec3::Z)
.with_inset_options(InsetOptions {
keep_inner_face: false,
scale: 0.2,
..default()
})
.center_aligned()
.build();
Mesh::new(
PrimitiveTopology::TriangleList,
RenderAssetUsages::RENDER_WORLD,
)
.with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, mesh_info.vertices)
.with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, mesh_info.normals)
.with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, mesh_info.uvs)
.with_inserted_indices(Indices::U16(mesh_info.indices))
}
Loading

0 comments on commit e51bc9b

Please sign in to comment.