00 A Hexagon

Today, before we dive into the actual development talk, I’d like to take a moment to explore in more detail what we hope to achieve through this project—sprinkling in a fair share of personal thoughts along the way.
Why Hexagon?
I’m a huge fan of JRPGs, especially turn‐based SRPGs—or more broadly, turn‐based strategy games. Consider:
- Farland Saga series, Fire Emblem series, Tactics Ogre, The recent Octopath Traveler
- And strategic masterpieces like XCOM or Battle Brothers
Some of these use square‐grid maps, while many employ hex‐grids. (Of course, games like Baldur’s Gate 3 use continuous coordinates, but classic turn‐based SRPGs typically stick to grids.)

battle-brothers-example
The Crucial Differences: Square vs. Hex
Forming a Front
- Squares: When multiple units line up, each unit only contacts one neighbor edge.
- Hexes: Units pack tighter—any gap lets enemies surround you, and each cell touches at least two adjacent cells.
Range & Diagonals
- Squares: Diagonal distance ≠ straight‐line distance, making “fair” ranged combat tricky.
- Hexes: All center-to-center distances are equal—no diagonal quirks.
Zone of Control (ZOC)
- Hard to impose ZOC naturally on squares, since each unit only influences four directions.
- On a hex grid, a unit controls six neighboring cells, easily constraining enemy movement.
Battle Brothers brilliantly leverages these hex characteristics to evoke that gritty, medieval, sweat-dripping melee combat. I loved it—and it inspired me to build my own take on “Battle Brothers on hexes.”

tile_difference_rect_vs_hexa
The Bigger Picture
Beyond just hex-grid combat, this project will rest on three pillars
Roguelike-Economy Systems
Beyond just hex-grid combat, we’ll build the kind of evolving, repeat-play economies you see in Darkest Dungeon, XCOM, Battle Brothers, Wartales, and similar titles—so every run feels fresh and strategically deep.
Smart AI
Rather than the usual “higher difficulty = massive resource bonuses for the AI,” I want a true test of wits: at higher difficulty levels, the AI should actually get smarter, managing its civilization more efficiently (without any unfair perks), so that I can experience being out-played by a genuinely superior strategist.
The Web
Steam is great, but with modern WebAssembly (wasm), pure-web games are finally practical. I’m excited to push the boundaries of what a browser-only strategy RPG can be.
So, now...
“A journey of a thousand miles begins with a single step.”
Let’s start by drawing a hexagon.
A Hexagon
Hex Geometry & Size
We’ll choose an edge length of 10 units for each hex cell.
- Outer Radius (distance from center to a corner) = 10
- Inner Radius (distance from center to a side)
This matters because the distance between centers of adjacent hexes is 2 x Inner Radius
To keep this handy, let’s define a static metrics class:
#[derive(Resource)]pub struct HexMetrics {pub outer_radius: f32,pub inner_radius: f32,}impl Default for HexMetrics {fn default() -> Self {let outer_radius = 10.0;Self {outer_radius,inner_radius: outer_radius * 0.866025404, // sqrt(3)/2}}}
File: hex_system.rs
Flat-Top vs. Pointy-Top
We’ll use flat-top hexes (horizontal faces) because:
- Vertical lines form naturally, making front-line layouts intuitive.
- From left/right perspectives, each hex exposes exactly 2 faces. Pointy-top hexes can expose up to 3 faces—often too many.

flat-top-and-pointy-top
Computing Hex Vertices
For a flat-top hex centered at (0,0)(0,0)(0,0), the seven vertices(including a center point) are:
In code:
let positions = vec![[0.0, 0.0, 0.0][hex_metrics.outer_radius, 0.0, 0.0],[0.5 * hex_metrics.outer_radius, 0.0, hex_metrics.inner_radius],[-0.5 * hex_metrics.outer_radius, 0.0, hex_metrics.inner_radius],[-hex_metrics.outer_radius, 0.0, 0.0],[-0.5 * hex_metrics.outer_radius, 0.0, -hex_metrics.inner_radius],[0.5 * hex_metrics.outer_radius, 0.0, -hex_metrics.inner_radius],];
File: create_hex_mesh.rs
By translating and adding these vectors, we can place each cell and navigate to its neighbors.
Rendering with Bevy Custom Mesh
In Bevy, to draw a hex cell as a 3D mesh you need:
- Vertex Positions
- UV Coordinates
- Normals
- Index Buffer (which vertices form each triangle)
- Material (color or texture)
We can represent our hex as a triangle fan: one center vertex plus the six outer vertices, forming six triangles. Here is a full example of custom bevy mesh:
#[rustfmt::skip]pub fn create_hex_mesh(hex_metrics: &HexMetrics) -> Mesh {let mut mesh = Mesh::new(PrimitiveTopology::TriangleList,RenderAssetUsages::MAIN_WORLD | RenderAssetUsages::RENDER_WORLD,);let positions = vec![[0.0, 0.0, 0.0],[hex_metrics.outer_radius, 0.0, 0.0],[0.5 * hex_metrics.outer_radius, 0.0, hex_metrics.inner_radius],[-0.5 * hex_metrics.outer_radius, 0.0, hex_metrics.inner_radius],[-hex_metrics.outer_radius, 0.0, 0.0],[-0.5 * hex_metrics.outer_radius, 0.0, -hex_metrics.inner_radius],[0.5 * hex_metrics.outer_radius, 0.0, -hex_metrics.inner_radius],];mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, positions.clone());info!("positions: {:?}", positions);// UV mapping: map positions from the xz-plane into 0-1 space.let uvs: Vec<[f32; 2]> = positions.iter().map(|[x, _y, z]| {[(x / (2.0 * hex_metrics.outer_radius)) + 0.5,(z / (2.0 * hex_metrics.outer_radius)) + 0.5,]}).collect();mesh.insert_attribute(Mesh::ATTRIBUTE_UV_0, uvs);// All normals point upward (y-axis up).let normals = vec![[0.0, 1.0, 0.0]; positions.len()];mesh.insert_attribute(Mesh::ATTRIBUTE_NORMAL, normals);// Define triangles by connecting center (0) with pairs of adjacent verticeslet indices = vec![0, 2, 1, // First triangle0, 3, 2, // Second triangle0, 4, 3, // Third triangle0, 5, 4, // Fourth triangle0, 6, 5, // Fifth triangle0, 1, 6, // Sixth triangle];mesh.insert_indices(Indices::U32(indices));mesh}
File: create_hex_mesh.rs
Building a simple app
Let’s quickly build a simple application to render this mesh. For that, we’ll need to set up a basic camera, mesh, and material.
One useful configuration to keep in mind is the WindowPlugin, which is required to embed a WASM-compiled Bevy application into a target HTML element.
fn main() {App::new().init_resource::<HexMetrics>().add_plugins(DefaultPlugins.set(WindowPlugin {primary_window: Some(Window {canvas: Some("#bevy-canvas".into()),resolution: WindowResolution::new(300.0, 300.0),fit_canvas_to_parent: true,prevent_default_event_handling: false,..default()}),..default()}),).add_systems(Startup, setup).run();}fn setup(mut commands: Commands,mut materials: ResMut<Assets<StandardMaterial>>,mut meshes: ResMut<Assets<Mesh>>,hex_metrics: Res<HexMetrics>,asset_server: Res<AssetServer>,) {let hex_mesh_handle = meshes.add(create_hex_mesh(&hex_metrics));let hex_material = materials.add(StandardMaterial {base_color: Color::srgb(0.8, 0.0, 0.0),cull_mode: None,double_sided: true,..default()});let entity = commands.spawn((Mesh3d(hex_mesh_handle),MeshMaterial3d(hex_material.clone()),Transform::from_translation(Vec3::new(0.0, 0.0, 0.0)),GlobalTransform::default(),HexCell,HexCoord::new(0, 0),)).id();commands.spawn((Camera3d::default(),Transform::from_xyz(0.0, 150.0, 0.0).looking_at(Vec3::ZERO, -Vec3::Z),Camera {clear_color: Color::from(SLATE_950).into(),hdr: true,..default()},Msaa::Off,));commands.spawn((DirectionalLight {illuminance: light_consts::lux::OVERCAST_DAY,shadows_enabled: true,..default()},Transform {translation: Vec3::new(0.0, 2.0, 0.0),rotation: Quat::from_rotation_x(-PI / 4.),..default()},));commands.insert_resource(AmbientLight {color: Color::WHITE,brightness: 100.0,..default()});}
File: main.rs
The result is the same as the cover image above. I originally planned to include the WASM binary in every post, but since the file size is large and there’s no meaningful interaction, it doesn’t seem very engaging, so I won’t include it each time. After a few more posts, I plan to add interactive demo pages in the project where you can explore the built WASM files directly.

simple_hexagon_from_bevy
Next Steps
We’ve laid the groundwork: hexagon geometry, coordinate calculations, and rendering a single cell in Bevy. Next up, we’ll explore Map‐wide coordinate systems (axial, cube coordinates)
With our first step complete, I’m excited for the journey ahead.