2d hexgrid movement

Godot Version

4.4

Question

Hi im trying to make a 2d hexgrid movement but i cant get it to work. I have set the tile terrain cost with Tileset ID. But the player doesnt always take the path with the least movementpoint and sometimes takes “shortcuts” via the corners of the hex.







GridMovement.gd
extends Node2D

@onready var tilemap : TileMap = $TileMap
@onready var player : Node2D = $Player
@onready var path_preview : PathPreview = $PathPreview
@onready var debug_label : Label = $CanvasLayer/DebugLabel

const MAX_MOVE_POINTS := 10
var move_points := MAX_MOVE_POINTS
var move_speed := 100.0

var selected_tile : Vector2i = Vector2i(-999, -999)
var preview_path : Array =
var movement_path : Array =
var movement_index := 0

Instance of our A* helper class
var hex_astar : AStarHexGrid2D = AStarHexGrid2D.new()

func _ready() → void:

Wait a frame so TileMap is fully ready
await get_tree().process_frame
hex_astar.setup_hex_grid(tilemap, 0)

Place player on the “start tile”

var start_tile = Vector2i(-1, -1)
player.global_position = tilemap.to_global(tilemap.map_to_local(start_tile))
debug_label.text = " Debug initialized"
func _unhandled_input(event: InputEvent) → void:

If still moving along a path, don’t accept new clicks
if movement_index < movement_path.size():
return

if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
_on_click()
func _on_click() → void:
var clicked = tilemap.local_to_map(tilemap.get_local_mouse_position())
var current = tilemap.local_to_map(tilemap.to_local(player.global_position))

var info = " Clicked: %s\n Player on: %s" % [clicked, current]

var path = _find_path(current, clicked)
if path.empty():
info += “\n No valid path”
_clear_preview()
preview_path.clear()
selected_tile = Vector2i(-999, -999)
else:
var cost = _calculate_cost(path)
info += “\n Steps: %d Cost: %d/%d” % [path.size(), cost, move_points]

if cost > move_points:
    info += "\n Not enough move points!"
elif clicked == selected_tile and preview_path.size() > 0:
    info += "\n Path confirmed, moving"
    _start_movement(preview_path.duplicate())
    move_points -= cost
    _clear_preview()
    preview_path.clear()
    selected_tile = Vector2i(-999, -999)
else:
    info += "\n Previewing path, click again to move"
    _clear_preview()
    preview_path   = path
    selected_tile  = clicked
    path_preview.update_path(path, tilemap)

debug_label.text = info
func _process(delta: float) → void:
if movement_index >= movement_path.size():
return

var target = movement_path[movement_index]
var direction = target - player.global_position

if direction.length() < 2.0:
player.global_position = target
movement_index += 1
if movement_index >= movement_path.size():
player.play_idle_animation()
else:
var step = direction.normalized() * move_speed * delta
player.global_position += step
player.play_walk_animation(step)
func _find_path(start: Vector2i, goal: Vector2i) → Array:
var raw : PackedVector2Array = hex_astar.get_path(start, goal)
var out : Array =
for local_pos in raw:
out.append(tilemap.local_to_map(local_pos))
return out

func _calculate_cost(path: Array) → int:
var total = 0

skip first tile (no cost to stand still)
for i in range(1, path.size()):
var tile_id = tilemap.get_cell_source_id(0, path)
var cost = _get_tile_cost(tile_id)
if cost < 0:
return INF
total += cost
return total

func _get_tile_cost(tile_id: int) → int:
match tile_id:
2: return 1 # grass
3: return -1 # mountain (blocked)
4: return 2 # forest
5: return -1 # water (blocked)
6: return 3 # swamp
7: return 2 # sand
8: return 1 # hills
9: return 1 # road
10: return 2 # snow
11: return -1 # river (blocked)
_: return 1 # default

func _start_movement(path: Array) → void:
movement_path.clear()
for tile in path:
movement_path.append(tilemap.to_global(tilemap.map_to_local(tile)))
movement_index = 0

func _clear_preview() → void:
for child in get_children():
if child.name.begins_with(“preview_marker”) or child.name.begins_with(“step_label”):
child.queue_free()


a_star_hex_grid_2d.gd
extends AStar2D
class_name AStarHexGrid2D

var tile_map : TileMap
var solid_data_name := “solid”
var coords_to_id_map := {}

func setup_hex_grid(passed_tile_map: TileMap, layer: int) → void:
tile_map = passed_tile_map
coords_to_id_map.clear()
clear() # remove existing points/edges

Add each non‑solid cell as a node

for cell in tile_map.get_used_cells(layer):
var data = tile_map.get_cell_tile_data(layer, cell)
if data.get_custom_data(solid_data_name):
print(" Blocked tile:", cell)
continue

var cost     = _get_tile_cost(cell)
var local_pos = tile_map.map_to_local(cell)
var id       = coords_to_id_map.size()
coords_to_id_map[cell] = id
add_point(id, local_pos, cost)
print(" Added tile:", cell)

Connect each node to its hex neighbors

for cell in coords_to_id_map.keys():
var cid = coords_to_id_map[cell]
for nbr in _get_hex_neighbors(cell):
if coords_to_id_map.has(nbr):
var nid = coords_to_id_map[nbr]
if not are_points_connected(cid, nid):
connect_points(cid, nid, false)
func _get_hex_neighbors(cell: Vector2i) → Array:
var dirs_even = [
Vector2i( +1, 0), Vector2i( 0, -1), Vector2i(-1, -1),
Vector2i(-1, 0), Vector2i(-1, +1), Vector2i( 0, +1),
]
var dirs_odd = [
Vector2i(+1, 0), Vector2i(+1, -1), Vector2i( 0, -1),
Vector2i(-1, 0), Vector2i( 0, +1), Vector2i(+1, +1),
]
var directions = (cell.x % 2 == 0) ? dirs_even : dirs_odd
var nbrs :=
for d in directions:
nbrs.append(cell + d)
return nbrs

func _get_tile_cost(cell: Vector2i) → float:
var tid = tile_map.get_cell_source_id(0, cell)
match tid:
2: return 1.0 # grass
3,5,11: return INF # mountain / water / river blocked
4: return 2.0 # forest
6: return 3.0 # swamp
7,10: return 2.0 # sand / snow
8,9: return 1.0 # hills / roads
_: return 1.0 # default

func coords_to_id(coords: Vector2i) → int:
return coords_to_id_map.get(coords, -1)

func get_path(from_point: Vector2i, to_point: Vector2i) → PackedVector2Array:
var from_id = coords_to_id(from_point)
var to_id = coords_to_id(to_point)
if from_id < 0 or to_id < 0:
return PackedVector2Array()
return get_point_path(from_id, to_id)


PathPreview.gd
extends Node2D
class_name PathPreview

var path_points : Array =
var tilemap : TileMap

func _draw() → void:
if path_points.size() < 2 or tilemap == null:
return
for i in range(path_points.size() - 1):
var a_tile = tilemap.local_to_map(tilemap.to_local(path_points))
var b_tile = tilemap.local_to_map(tilemap.to_local(path_points[i+1]))
var a_pos = tilemap.to_global(tilemap.map_to_local(a_tile))
var b_pos = tilemap.to_global(tilemap.map_to_local(b_tile))
draw_line(a_pos, b_pos, Color.red, 2)

func update_path(path: Array, tm: TileMap) → void:
path_points = path
tilemap = tm
queue_redraw()


Player.gd
extends Node2D

@onready var anim : AnimatedSprite2D = $AnimatedSprite2D
var last_direction : Vector2 = Vector2.ZERO

func _ready() → void:
anim.play(“front_idle”)

func play_walk_animation(direction: Vector2) → void:
if direction == Vector2.ZERO:
play_idle_animation()
return

last_direction = direction
if abs(direction.x) > abs(direction.y):
# Horizontal
anim.flip_h = direction.x < 0
anim.play(“side_walk”)
else:
# Vertical
anim.flip_h = false
if direction.y > 0:
anim.play(“front_walk”)
else:
anim.play(“back_walk”)
func play_idle_animation() → void:
if abs(last_direction.x) > abs(last_direction.y):
anim.play(“side_idle”)
else:
if last_direction.y > 0:
anim.play(“front_idle”)
else:
anim.play(“back_idle”)


The story is longer here.
You have to use the Godot engine more like a calculating machine.
It’s best to use the N factoring here.
If not familiar with calculators. You can use a simpler method.
Calling the bilateral functions in the numbers to your advantage.
Each of the short problem paths you showed are different in values. The normal distance is let’s say 60 pixels. The incorrect distance is 70 pixels. You will probably have to limit your movement rays to segments that meet you median requirements. I don’t see a problem, Godot can do it.

Thanks for quick responce, i will try.

I think this is beyond my current skill level to make this work.