---
name: godot-pathfinding
description: TileMap BFS寻路实现、六边形网格寻路、路径追溯与流式寻路功能。用于需要网格寻路、动态障碍物避让、游戏AI导航等场景。
---

# Godot TileMap 寻路系统

TileMap网格寻路实现，包含BFS扩散算法、六边形网格支持、路径追溯以及流场寻路功能。

## 何时使用此技能

- 需要为格子地图实现寻路功能
- 实现RTS、塔防、回合制策略等游戏
- 需要动态障碍物避让
- 需要流场寻路支持多单位导航

## 1. TileMap BFS 寻路实现

### 基础 BFS 扩散算法

```gdscript
# tile_map_bfs.gd
# TileMap网格BFS寻路实现
class_name TileMapBFS
extends Node2D

signal path_found(path: Array[Vector2i])
signal path_not_found

@export var tile_map: TileMap
@export var obstacles_layer: int = 0  # 障碍物所在的层

var _grid_size: Vector2i
var _walkable: Dictionary = {}  # {Vector2i: bool}

func _ready() -> void:
    if tile_map:
        _initialize_grid()

func _initialize_grid() -> void:
    _grid_size = tile_map.get_used_rect().size
    var origin := tile_map.get_used_rect().position

    for x in range(_grid_size.x):
        for y in range(_grid_size.y):
            var cell := Vector2i(origin.x + x, origin.y + y)
            var tile_data := tile_map.get_cell_tile_data(obstacles_layer, cell)
            _walkable[cell] = tile_data == null or not tile_data.get_custom_data("obstacle")

func find_path(start: Vector2i, end: Vector2i) -> Array[Vector2i]:
    if not _walkable.has(start) or not _walkable.has(end):
        return []

    if not _walkable.get(end, false):
        return []

    if start == end:
        return [start]

    var open_set: Array[Vector2i] = [start]
    var came_from: Dictionary = {}
    var visited: Dictionary = {start: true}

    while not open_set.is_empty():
        var current := open_set.pop_front()

        if current == end:
            return _reconstruct_path(came_from, current)

        for neighbor in _get_neighbors(current):
            if not visited.get(neighbor, false) and _walkable.get(neighbor, false):
                visited[neighbor] = true
                came_from[neighbor] = current
                open_set.append(neighbor)

    return []

func _get_neighbors(cell: Vector2i) -> Array[Vector2i]:
    return [
        cell + Vector2i(0, -1),  # 上
        cell + Vector2i(1, 0),   # 右
        cell + Vector2i(0, 1),   # 下
        cell + Vector2i(-1, 0),  # 左
    ]

func _reconstruct_path(came_from: Dictionary, current: Vector2i) -> Array[Vector2i]:
    var path: Array[Vector2i] = [current]

    while came_from.has(current):
        current = came_from[current]
        path.push_front(current)

    return path

func is_walkable(cell: Vector2i) -> bool:
    return _walkable.get(cell, false)

func set_obstacle(cell: Vector2i, obstacle: bool) -> void:
    _walkable[cell] = not obstacle
```

### 带权重的 BFS（用于斜向移动）

```gdscript
# weighted_bfs.gd
class_name WeightedBFS
extends Node

@export var tile_map: TileMap
@export var obstacles_layer: int = 0

var _cell_cost: Dictionary = {}  # 每个格子的移动成本

func _ready() -> void:
    _initialize_costs()

func _initialize_costs() -> void:
    var rect := tile_map.get_used_rect()
    for cell in tile_map.get_used_cells(obstacles_layer):
        var tile_data := tile_map.get_cell_tile_data(obstacles_layer, cell)
        if tile_data:
            _cell_cost[cell] = tile_data.get_custom_data("cost")
        else:
            _cell_cost[cell] = 1.0

func find_path(start: Vector2i, end: Vector2i) -> Array[Vector2i]:
    if start == end:
        return [start]

    var open_set: Array[Vector2i] = [start]
    var came_from: Dictionary = {}
    var cost_so_far: Dictionary = {start: 0.0}
    var visited: Dictionary = {}

    while not open_set.is_empty():
        open_set.sort_custom(func(a, b): return cost_so_far[a] < cost_so_far[b])
        var current := open_set.pop_front()

        if current == end:
            return _reconstruct_path(came_from, current)

        visited[current] = true

        for neighbor in _get_neighbors(current):
            if visited.get(neighbor, false):
                continue

            var move_cost := _get_move_cost(current, neighbor)
            var new_cost := cost_so_far[current] + move_cost

            if not cost_so_far.has(neighbor) or new_cost < cost_so_far[neighbor]:
                cost_so_far[neighbor] = new_cost
                came_from[neighbor] = current

                if not open_set.has(neighbor):
                    open_set.append(neighbor)

    return []

func _get_neighbors(cell: Vector2i) -> Array[Vector2i]:
    return [
        cell + Vector2i(0, -1),
        cell + Vector2i(1, 0),
        cell + Vector2i(0, 1),
        cell + Vector2i(-1, 0),
        cell + Vector2i(1, -1),  # 斜向
        cell + Vector2i(1, 1),
        cell + Vector2i(-1, 1),
        cell + Vector2i(-1, -1),
    ]

func _get_move_cost(from: Vector2i, to: Vector2i) -> float:
    var base_cost := _cell_cost.get(to, 1.0)

    # 斜向移动成本更高
    if from.x != to.x and from.y != to.y:
        return base_cost * 1.414  # sqrt(2)

    return base_cost

func _reconstruct_path(came_from: Dictionary, current: Vector2i) -> Array[Vector2i]:
    var path: Array[Vector2i] = [current]

    while came_from.has(current):
        current = came_from[current]
        path.push_front(current)

    return path
```

## 2. 六边形网格寻路

### 六边形网格坐标系统

```gdscript
# hexagon_pathfinding.gd
class_name HexagonPathfinding
extends Node

# 六边形网格偏移系统
enum OffsetSystem { POINTY_TOP, FLAT_TOP }
enum CoordinateSystem { CUBE, AXIAL, OFFSET }

@export var offset_system: OffsetSystem = OffsetSystem.POINTY_TOP
@export var tile_map: TileMap
@export var obstacles_layer: int = 0

var _cube_directions := [
    Vector3i(1, -1, 0), Vector3i(1, 0, -1), Vector3i(0, 1, -1),
    Vector3i(-1, 1, 0), Vector3i(-1, 0, 1), Vector3i(0, -1, 1)
]

func axial_to_cube(hex: Vector2i) -> Vector3i:
    return Vector3i(hex.x, hex.y, -hex.x - hex.y)

func cube_to_axial(cube: Vector3i) -> Vector2i:
    return Vector2i(cube.x, cube.y)

func offset_to_axial(offset: Vector2i) -> Vector2i:
    if offset_system == OffsetSystem.POINTY_TOP:
        return Vector2i(offset.x, offset.y - (offset.x - (offset.x & 1)) / 2)
    else:
        return Vector2i(offset.x - (offset.y - (offset.y & 1)) / 2, offset.y)

func axial_to_offset(axial: Vector2i) -> Vector2i:
    if offset_system == OffsetSystem.POINTY_TOP:
        return Vector2i(axial.x, axial.y + (axial.x - (axial.x & 1)) / 2)
    else:
        return Vector2i(axial.x + (axial.y - (axial.y & 1)) / 2, axial.y)

func get_neighbors(hex: Vector2i) -> Array[Vector2i]:
    var axial := offset_to_axial(hex)
    var cube := axial_to_cube(axial)
    var neighbors: Array[Vector2i] = []

    for direction in _cube_directions:
        var neighbor_cube := cube + direction
        var neighbor_axial := cube_to_axial(neighbor_cube)
        var neighbor_offset := axial_to_offset(neighbor_axial)
        neighbors.append(neighbor_offset)

    return neighbors

func is_walkable(hex: Vector2i) -> bool:
    var tile_data := tile_map.get_cell_tile_data(obstacles_layer, hex)
    return tile_data == null or not tile_data.get_custom_data("obstacle")

func find_path(start: Vector2i, end: Vector2i) -> Array[Vector2i]:
    if start == end:
        return [start]

    if not is_walkable(end):
        return []

    var open_set: Array[Vector2i] = [start]
    var came_from: Dictionary = {}
    var visited: Dictionary = {start: true}

    while not open_set.is_empty():
        var current := open_set.pop_front()

        if current == end:
            return _reconstruct_path(came_from, current)

        for neighbor in get_neighbors(current):
            if not visited.get(neighbor, false) and is_walkable(neighbor):
                visited[neighbor] = true
                came_from[neighbor] = current
                open_set.append(neighbor)

    return []

func _reconstruct_path(came_from: Dictionary, current: Vector2i) -> Array[Vector2i]:
    var path: Array[Vector2i] = [current]

    while came_from.has(current):
        current = came_from[current]
        path.push_front(current)

    return path

# 计算两个六边形之间的距离
func distance(a: Vector2i, b: Vector2i) -> int:
    var cube_a := axial_to_cube(offset_to_axial(a))
    var cube_b := axial_to_cube(offset_to_axial(b))
    return maxi(
        maxi(abs(cube_a.x - cube_b.x), abs(cube_a.y - cube_b.y)),
        abs(cube_a.z - cube_b.z)
    )
```

## 3. 路径追溯与平滑

### 路径可视化与平滑

```gdscript
# path_follower.gd
class_name PathFollower
extends Node2D

@export var path_line: Line2D
@export var move_speed: float = 200.0

var current_path: Array[Vector2i] = []
var current_index: int = 0
var is_moving: bool = false

var target_position: Vector2

signal path_completed
signal position_changed(new_pos: Vector2)

func set_path(path: Array[Vector2i], grid_to_world: Callable) -> void:
    current_path = path
    current_index = 0
    is_moving = false

    if path.is_empty():
        return

    target_position = grid_to_world.call(path[0])
    position = target_position

func _physics_process(delta: float) -> void:
    if not is_moving or current_path.is_empty():
        return

    var world_pos := grid_to_world(current_path[current_index])
    var direction := (world_pos - position).normalized()
    var distance := (world_pos - position).length()

    if distance < 5.0:
        current_index += 1
        position_changed.emit(position)

        if current_index >= current_path.size():
            is_moving = false
            path_completed.emit()
    else:
        position += direction * move_speed * delta

func start_moving() -> void:
    is_moving = true

func stop_moving() -> void:
    is_moving = false

func grid_to_world(grid_pos: Vector2i) -> Vector2:
    return tile_map.map_to_local(grid_pos)

# 平滑路径（去除多余拐点）
func smooth_path(path: Array[Vector2i], line_of_sight: Callable) -> Array[Vector2i]:
    if path.size() <= 2:
        return path

    var smoothed: Array[Vector2i] = [path[0]]
    var current := 0

    while current < path.size() - 1:
        var furthest := current + 1

        for i in range(path.size() - 1, current, -1):
            if line_of_sight.call(path[current], path[i]):
                furthest = i
                break

        smoothed.append(path[furthest])
        current = furthest

    return smoothed
```

## 4. 流式寻路（Flow Field）

流场寻路是RTS游戏中常用的技术，所有单位共享同一个流场，实现自然的群体避让。

### 流场构建原理

```gdscript
# flow_field.gd
# 流场寻路实现
# 原理：
# 1. 从目标点向外BFS扩散，构建距离场
# 2. 每个格子记录到目标的最短距离
# 3. 流场方向指向距离递减最快的方向
# 4. 单位沿流场方向移动即可到达目标
class_name FlowField
extends Node2D

@export var tile_map: TileMap
@export var obstacles_layer: int = 0
@export var destination_layer: int = 1

var _grid_size: Vector2i
var _cell_size: Vector2i
var _distance_field: Dictionary = {}  # {Vector2i: float}
var _flow_field: Dictionary = {}       # {Vector2i: Vector2}
var _walkable: Dictionary = {}

var _use_diagonals: bool = true

signal field_ready

func _ready() -> void:
    if tile_map:
        _initialize()

func _initialize() -> void:
    var used_rect := tile_map.get_used_rect()
    _grid_size = used_rect.size
    _cell_size = tile_map.tile_set.tile_size

    _build_walkable_map(used_rect)

func _build_walkable_map(rect: Rect2i) -> void:
    for x in range(rect.size.x):
        for y in range(rect.size.y):
            var cell := Vector2i(rect.position.x + x, rect.position.y + y)
            var tile_data := tile_map.get_cell_tile_data(obstacles_layer, cell)
            _walkable[cell] = tile_data == null or not tile_data.get_custom_data("obstacle")

func build_field(destination: Vector2i) -> void:
    _distance_field.clear()
    _flow_field.clear()

    # 如果目标不可行走，从最近的可行走格子开始
    if not _walkable.get(destination, false):
        destination = _find_nearest_walkable(destination)
        if destination == Vector2i(-1, -1):
            return

    # BFS 扩散构建距离场
    var queue: Array[Vector2i] = [destination]
    _distance_field[destination] = 0.0

    while not queue.is_empty():
        var current := queue.pop_front()
        var current_dist := _distance_field[current]

        for neighbor in _get_neighbors(current):
            if not _walkable.get(neighbor, false):
                continue

            var new_dist := current_dist + _get_move_cost(current, neighbor)

            if not _distance_field.has(neighbor) or new_dist < _distance_field[neighbor]:
                _distance_field[neighbor] = new_dist
                queue.append(neighbor)

    # 构建流场
    for cell in _distance_field.keys():
        _flow_field[cell] = _calculate_flow_direction(cell)

    field_ready.emit()

func _find_nearest_walkable(target: Vector2i) -> Vector2i:
    var closest: Vector2i = Vector2i(-1, -1)
    var min_dist := INF

    for cell in _walkable.keys():
        if _walkable[cell]:
            var dist := (cell - target).length()
            if dist < min_dist:
                min_dist = dist
                closest = cell

    return closest

func _get_neighbors(cell: Vector2i) -> Array[Vector2i]:
    var neighbors: Array[Vector2i] = [
        cell + Vector2i(0, -1),
        cell + Vector2i(1, 0),
        cell + Vector2i(0, 1),
        cell + Vector2i(-1, 0),
    ]

    if _use_diagonals:
        neighbors.append_array([
            cell + Vector2i(1, -1),
            cell + Vector2i(1, 1),
            cell + Vector2i(-1, 1),
            cell + Vector2i(-1, -1),
        ])

    return neighbors

func _get_move_cost(from: Vector2i, to: Vector2i) -> float:
    # 斜向移动成本
    if from.x != to.x and from.y != to.y:
        return 1.414
    return 1.0

func _calculate_flow_direction(cell: Vector2i) -> Vector2:
    var neighbors := _get_neighbors(cell)
    var best_direction := Vector2.ZERO
    var lowest_distance := INF

    for neighbor in neighbors:
        if _distance_field.has(neighbor):
            var dist := _distance_field[neighbor]
            if dist < lowest_distance:
                lowest_distance = dist
                var dir := Vector2(neighbor - cell).normalized()
                best_direction = dir

    return best_direction

func get_flow_direction(cell: Vector2i) -> Vector2:
    if _flow_field.has(cell):
        return _flow_field[cell]
    return Vector2.ZERO

func get_distance(cell: Vector2i) -> float:
    return _distance_field.get(cell, INF)

# 移动单位沿流场方向
func move_along_flow(unit_position: Vector2, speed: float, delta: float) -> Vector2:
    var cell := tile_map.local_to_map(unit_position)
    var flow := get_flow_direction(cell)

    if flow.length() > 0.01:
        return unit_position + flow * speed * delta
    else:
        # 如果没有流向（不在流场中），尝试随机移动
        return unit_position + Vector2.RIGHT * speed * delta * 0.5
```

### 多个目标的流场

```gdscript
# multi_target_flow_field.gd
class_name MultiTargetFlowField
extends FlowField

var _target_cells: Array[Vector2i] = []

func add_target(cell: Vector2i) -> void:
    if not cell in _target_cells:
        _target_cells.append(cell)

func remove_target(cell: Vector2i) -> void:
    _target_cells.erase(cell)

func clear_targets() -> void:
    _target_cells.clear()

func build_field() -> void:
    if _target_cells.is_empty():
        return

    _distance_field.clear()
    _flow_field.clear()

    # 多目标BFS
    var queue: Array[Vector2i] = _target_cells.duplicate()
    var visited: Dictionary = {}

    for target in _target_cells:
        if _walkable.get(target, false):
            _distance_field[target] = 0.0
            visited[target] = true

    while not queue.is_empty():
        var current := queue.pop_front()
        var current_dist := _distance_field[current]

        for neighbor in _get_neighbors(current):
            if not _walkable.get(neighbor, false):
                continue

            if visited.get(neighbor, false):
                continue

            visited[neighbor] = true
            _distance_field[neighbor] = current_dist + _get_move_cost(current, neighbor)
            queue.append(neighbor)

    # 构建流场
    for cell in _distance_field.keys():
        _flow_field[cell] = _calculate_flow_direction(cell)

    field_ready.emit()
```

## 5. 完整示例：RTS 单位寻路系统

```gdscript
# rts_unit_pathfinding.gd
# 完整的RTS单位寻路系统
class_name RTSUnitPathfinding
extends CharacterBody2D

@export var move_speed: float = 150.0
@export var flow_field: FlowField
@export var unit_radius: float = 16.0

var _current_target: Vector2i = Vector2i(-1, -1)
var _is_selected: bool = false
var _formation_offset: Vector2 = Vector2.ZERO

@onready var selection_indicator: Sprite2D = $SelectionIndicator
@onready var unit_sprite: Sprite2D = $Sprite2D

func _ready() -> void:
    selection_indicator.visible = false

func _physics_process(delta: float) -> void:
    if _current_target != Vector2i(-1, -1):
        _move_along_flow(delta)

func _move_along_flow(delta: float) -> void:
    var flow_dir := flow_field.get_flow_direction(global_position)

    if flow_dir.length() > 0.01:
        # 应用编队偏移
        var target_pos := global_position + flow_dir * move_speed * delta + _formation_offset * 0.1

        # 简单避让
        var avoidance := _calculate_avoidance()
        target_pos += avoidance * 50.0 * delta

        global_position = target_pos

        # 旋转朝向移动方向
        rotation = flow_dir.angle()
    else:
        # 到达目标
        _current_target = Vector2i(-1, -1)

func _calculate_avoidance() -> Vector2:
    var avoidance := Vector2.ZERO
    var nearby_units := get_tree().get_nodes_in_group("rts_units")

    for unit in nearby_units:
        if unit == self:
            continue

        var dist := global_position.distance_to(unit.global_position)
        if dist < unit_radius * 3:
            var push_dir := (global_position - unit.global_position).normalized()
            avoidance += push_dir * (1.0 - dist / (unit_radius * 3))

    return avoidance

func set_target(world_position: Vector2) -> void:
    _current_target = flow_field.tile_map.local_to_map(world_position)

func set_selected(selected: bool) -> void:
    _is_selected = selected
    selection_indicator.visible = selected

func set_formation_offset(offset: Vector2) -> void:
    _formation_offset = offset
```

## 性能优化建议

1. **缓存寻路结果**：对于相同起点的查询，直接返回缓存路径
2. **批量更新流场**：多个单位共享同一流场，避免重复计算
3. **分层寻路**：远距离使用粗糙网格，近距离使用精细网格
4. **异步计算**：复杂寻路在后台线程计算，避免阻塞主线程

## 最佳实践

- TileMap 障碍物使用自定义数据 `obstacle: true` 标记
- 流场适合大量单位同时寻路的场景
- 六边形网格适合策略游戏
- 路径平滑使用视线检测去除多余拐点
