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