• Godot Help
  • Need help with a 2d Hexgrid movement with pathfinding.

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.

I dont know how to fix this.





Here is my code:

GridMovement.gd


extends Node2D

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

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

var selected_tile: Vector2i = Vector2i(-999, -999)
var preview_path: Array = []
var move_path: Array = []
var move_index := 0

var hex_astar = preload("res://Scripts/a_star_hex_grid_2d.gd").new()

func _ready():
await get_tree().process_frame
hex_astar.setup_hex_grid(tilemap, 0)
var start_tile = Vector2i(-1, -1)
player.global_position = tilemap.to_global(tilemap.map_to_local(start_tile))
debug_label.text = "✅ Debug init"

func _input(event):
if event.is_action_pressed("end_turn"):
start_new_turn()

func _unhandled_input(event):
if move_index < move_path.size():
return

if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
	var clicked_tile = tilemap.local_to_map(tilemap.get_local_mouse_position())
	var player_tile = tilemap.local_to_map(tilemap.to_local(player.global_position))

	var debug_text = "🖱️ Klickat på tile: " + str(clicked_tile)
	debug_text += "\n🧍 Spelare står i tile: " + str(player_tile)

	var path = find_path(player_tile, clicked_tile)

	if path.is_empty():
		debug_text += "\n❌ Ingen giltig path – blockerad eller ogiltig"
		clear_preview_visuals()
		preview_path.clear()
		selected_tile = Vector2i(-999, -999)
	else:
		debug_text += "\n📍 Path längd: " + str(path.size())
		var cost = calculate_path_cost(path)
		debug_text += "\n💰 Kostnad: " + str(cost) + " / " + str(player_move_points)

		if cost > player_move_points:
			debug_text += "\n❌ Inte tillräckligt med move points!"
		elif clicked_tile == selected_tile and preview_path.size() > 0:
			debug_text += "\n✅ Path bekräftad – startar rörelse"
			move_along_path(preview_path.duplicate())
			player_move_points -= cost
			clear_preview_visuals()
			preview_path.clear()
			selected_tile = Vector2i(-999, -999)
		else:
			debug_text += "\n👁️ Visar path – klicka igen för att gå"
			clear_preview_visuals()
			preview_path = path
			selected_tile = clicked_tile
			show_preview_visuals(preview_path)

	debug_label.text = debug_text

func _process(delta):
if move_index >= move_path.size():
return

var target = move_path[move_index]
var direction = target - player.global_position
var distance = direction.length()

if distance < 2.0:
	player.global_position = target
	move_index += 1
	if move_index >= move_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:
print("🔍 Söker path från ", start, " till ", goal)
var raw_path: PackedVector2Array = hex_astar.get_path(start, goal)
var from_id = hex_astar.coords_to_id(start)
var to_id = hex_astar.coords_to_id(goal)
print("🧠 Start ID:", from_id, ", Mål ID:", to_id)
if from_id == -1:
print("❌ Start-tile finns inte i AStar-nätet:", start)
if to_id == -1:
print("❌ Mål-tile finns inte i AStar-nätet:", goal)

var converted: Array = []
for pos in raw_path:
	converted.append(tilemap.local_to_map(pos)) # hex-grid tile-coords
print("📍 Path: ", converted)
return converted

func calculate_path_cost(path: Array) -> int:
var total_cost = 0
for tile in path:
var tile_id = tilemap.get_cell_source_id(0, tile)
print("Clicked tile ID:", tile_id, " at coords: ", tile)
var cost = get_tile_cost(tile_id)
if tile_id == -1 or cost == -1:
return 9999
total_cost += cost
return total_cost

func get_tile_cost(tile_id: int) -> int:
match tile_id:
2: return 1 # Gräs
3: return -1 # Berg
4: return 2 # Skog
5: return -1 # Vatten
6: return 3 # Träsk
7: return 2 # Sand
8: return 1 # Kullar
9: return 1 # Vägar
10: return 2 # Snö
11: return -1 # Floder
_: return 1

func move_along_path(path: Array):
move_path.clear()
for tile in path:
move_path.append(tilemap.to_global(tilemap.map_to_local(tile)))
move_index = 0

func start_new_turn():
player_move_points = MAX_MOVE_POINTS
debug_label.text += "\n🔄 Ny tur – move points återställda till " + str(player_move_points)

func clear_preview_visuals():
for child in get_children():
if child.name.begins_with("preview_marker") or child.name.begins_with("step_label"):
child.queue_free()

func show_preview_visuals(path: Array):
for i in path.size():
var tile = path
var pos = tilemap.to_global(tilemap.map_to_local(tile))

	var marker = ColorRect.new()
	marker.name = "preview_marker_%d" % i
	marker.color = Color(1, 1, 1, 0.4)
	marker.size = Vector2(10, 10)
	marker.position = pos - marker.size * 0.5
	add_child(marker)

	var label = Label.new()
	label.name = "step_label_%d" % i
	label.text = str(i)
	label.modulate = Color.BLACK
	label.position = pos - Vector2(5, 20)
	add_child(label)

path_preview.update_path(path, tilemap)

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 := {}

🔧 Setup the grid

func setup_hex_grid(passed_tile_map: TileMap, layer: int) -> void:
tile_map = passed_tile_map
coords_to_id_map.clear()
clear()

var used_cells = tile_map.get_used_cells(layer)

for cell in used_cells:
	var tile_data = tile_map.get_cell_tile_data(layer, cell)
	var is_tile_solid = tile_data.get_custom_data(solid_data_name)
	if not is_tile_solid:
		var local_pos = tile_map.map_to_local(cell)
		var cost = get_tile_cost_by_coords(cell)
		var id = coords_to_id_map.size()
		coords_to_id_map[cell] = id
		add_point(id, local_pos, cost)
		print("✅ Tillagd tile:", cell)
	else:
		print("⛔ Blockerad tile:", cell)

# Anslut alla gångbara noder till sina riktiga hexgrannar
for cell in coords_to_id_map.keys():
	var center_id = coords_to_id_map[cell]
	for neighbor in get_hex_neighbors(cell):
		if coords_to_id_map.has(neighbor):
			var neighbor_id = coords_to_id_map[neighbor]
			connect_points(center_id, neighbor_id)

func get_hex_neighbors(cell: Vector2i) -> Array:
var neighbors := []
var x = cell.x
var y = cell.y

var directions_even_x = [
	Vector2i(+1, 0), Vector2i(0, -1), Vector2i(-1, -1),
	Vector2i(-1, 0), Vector2i(-1, +1), Vector2i(0, +1)
]

var directions_odd_x = [
	Vector2i(+1, 0), Vector2i(+1, -1), Vector2i(0, -1),
	Vector2i(-1, 0), Vector2i(0, +1), Vector2i(+1, +1)
]

var directions = directions_even_x if x % 2 == 0 else directions_odd_x

for dir in directions:
	neighbors.append(cell + dir)

return neighbors

func get_tile_cost_by_coords(cell: Vector2i) -> float:
var tile_id = tile_map.get_cell_source_id(0, cell)
match tile_id:
2: return 1.0 # Gräs
3: return INF # Berg (blockerad)
4: return 2.0 # Skog
5: return INF # Vatten (blockerad)
6: return 3.0 # Träsk
7: return 2.0 # Sand
8: return 1.0 # Kullar
9: return 1.0 # Vägar
10: return 2.0 # Snö
11: return INF # Flod (blockerad)
_: return 1.0 # Standard

func coords_to_id(coords: Vector2i) -> int:
if coords_to_id_map.has(coords):
return coords_to_id_map[coords]
return -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 == -1 or to_id == -1:
	print("🚫 Invalid path request – from_id:", from_id, " to_id:", to_id)
	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():
if path_points.size() < 2 or tilemap == null:
return

for i in range(path_points.size() - 1):
	var from_tile = tilemap.local_to_map(tilemap.to_local(path_points[i]))
	var to_tile = tilemap.local_to_map(tilemap.to_local(path_points[i + 1]))
	var from_pos = tilemap.to_global(tilemap.map_to_local(from_tile))
	var to_pos = tilemap.to_global(tilemap.map_to_local(to_tile))
	draw_line(from_pos, to_pos, Color.RED, 2.0)

func update_path(path: Array, tilemap_ref: TileMap):
path_points = path
tilemap = tilemap_ref
queue_redraw()


player.gd


extends Node2D

@onready var anim = $AnimatedSprite2D

var last_direction: Vector2 = Vector2.ZERO

func _ready():
anim.play("front_idle")

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

last_direction = direction

if abs(direction.x) > abs(direction.y):
	# Horisontell rörelse
	anim.flip_h = direction.x < 0
	anim.play("side_walk")
else:
	# Vertikal rörelse
	anim.flip_h = false
	if direction.y > 0:
		anim.play("front_walk")
	else:
		anim.play("back_walk")

func play_idle_animation():
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")


Thanks for any help // Linus

  • xyz replied to this.

    Linken81 Format your code properly.

    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")

    7 days later

    I got it to work.