AStar2D

A* 的一种实现,用于查找 2D 空间中连通图上两个顶点之间的最短路径。

AStarGrid2D

A* 的一种实现,用于寻找疏松 2D 网格中两点之间的最短路径。

navigation_astar 项目解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
extends TileMap
# 定义了一个枚举。这是一种给数字起名字的方式(0,1,2),让代码更具可读性。
enum Tile { OBSTACLE, START_POINT, END_POINT }

# 定义瓦片尺寸(宽和高)
const CELL_SIZE = Vector2i(64, 64)
# 定义用于绘制路径的线条
const BASE_LINE_WIDTH = 3.0 # 宽度
const DRAW_COLOR = Color.WHITE * Color(1, 1, 1, 0.5) # 颜色

# 创建 AStarGrid2D 实例
# Godot 内置专门用于2D网格上进行路径寻找的工具,底层使用 A* 算法。
var _astar = AStarGrid2D.new()

# 用于存储路径计算的起点和终点在瓦片坐标系(Grid-based)位置
var _start_point = Vector2i()
var _end_point = Vector2i()
# 用于存储计算的路径上所有点的局部坐标系(Pixel-based)位置
var _path = PackedVector2Array()

func _ready():
# 定义 A* 算法工作的网格范围
# Rect2i(x, y, width, height)
# 注意:这里的尺寸是硬编码的(18x10)如果地图大小会改变需要动态计算,例如通过 get_used_rect()
_astar.region = Rect2i(0, 0, 18, 10)
# 将 A* 网格的单元格大小设置为与瓦片大小一致
_astar.cell_size = CELL_SIZE
# 将 A* 节点的位置设置在瓦片中心
_astar.offset = CELL_SIZE * 0.5
# 设置 A* 启发式函数为曼哈顿距离
_astar.default_compute_heuristic = AStarGrid2D.HEURISTIC_MANHATTAN
_astar.default_estimate_heuristic = AStarGrid2D.HEURISTIC_MANHATTAN
_astar.diagonal_mode = AStarGrid2D.DIAGONAL_MODE_NEVER # 禁止对角线移动
_astar.update() # 应用以上所有设置

# 遍历 _astar.region 定义的整个网格范围
for i in range(_astar.region.position.x, _astar.region.end.x):
for j in range(_astar.region.position.y, _astar.region.end.y):
var pos = Vector2i(i, j)
if get_cell_source_id(0, pos) == Tile.OBSTACLE:
_astar.set_point_solid(pos)


func _draw():
if _path.is_empty():
return

var last_point = _path[0] # 获取路径的第一个点
for index in range(1, len(_path)): # 从路径的第二个点开始遍历
var current_point = _path[index]
draw_line(last_point, current_point, DRAW_COLOR, BASE_LINE_WIDTH, true) # 绘制线段
draw_circle(current_point, BASE_LINE_WIDTH * 2.0, DRAW_COLOR) # 绘制圆形
last_point = current_point # 更新 last_point,为下一次循环做准备

# 接收一个像素坐标,先将其转换为瓦片坐标 (local_to_map),然后再转换回像素坐标 (map_to_local)。
# 这样做的效果是,无论你传入哪个像素点,它都会返回该像素点所在瓦片的左上角像素坐标。
# 这常用于将一个自由移动的物体 “吸附” 到网格上。
func round_local_position(local_position):
return map_to_local(local_to_map(local_position))

# 判断一个像素点所在的瓦片是否可以通过
func is_point_walkable(local_position):
var map_position = local_to_map(local_position)
if _astar.is_in_boundsv(map_position):
return not _astar.is_point_solid(map_position)
return false

# 用于清除上一次计算的路径
func clear_path():
if not _path.is_empty():
_path.clear()
erase_cell(0, _start_point)
erase_cell(0, _end_point)
# 触发 _draw() 函数,清除屏幕上已绘制的路径线条和圆点
queue_redraw()

# 这是外部调用者最常用的函数,用于计算新路径
func find_path(local_start_point, local_end_point):
clear_path()
# 将传入的像素坐标转换为瓦片坐标
_start_point = local_to_map(local_start_point)
_end_point = local_to_map(local_end_point)

# 调用 A* 算法,返回一个包含路径上所有点(像素坐标,且是瓦片中心)的 PackedVector2Array。如果找不到路径,该数组将为空。
_path = _astar.get_point_path(_start_point, _end_point)

if not _path.is_empty():
# 放置瓦片
set_cell(0, _start_point, 0, Vector2i(Tile.START_POINT, 0))
set_cell(0, _end_point, 0, Vector2i(Tile.END_POINT, 0))

# 触发 _draw() 函数,在屏幕上绘制出新的路径。
queue_redraw()

return _path.duplicate()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
extends Node2D

# 定义了角色两种状态
enum State { IDLE, FOLLOW }
# 用于 “转向行为” 的一个物理参数。可以理解为“惯性”。
const MASS = 10.0
# 到达距离。当角色与目标点的距离小于这个值时,就认为它已经到达了该点。
const ARRIVE_DISTANCE = 10.0

@export var speed: float = 200.0

var _state = State.IDLE # 角色当前状态
var _velocity = Vector2() # 角色当前速度向量

@onready var _tile_map = $"../TileMap"

var _click_position = Vector2() # 存储玩家最后一次点击鼠标的全局位置
var _path = PackedVector2Array() # 存储从 TileMap 获取的路径点数组
var _next_point = Vector2() #存储路径中下一个要移动到的目标点

func _ready():
_change_state(State.IDLE) # 初始化为 IDLE 状态


func _process(_delta):
if _state != State.FOLLOW:
return
# 调用 _move_to 函数,让角色向 _next_point 移动。该函数会返回一个布尔值,表示是否到达了目标点。
var arrived_to_next_point = _move_to(_next_point)
if arrived_to_next_point:
_path.remove_at(0)
if _path.is_empty():
_change_state(State.IDLE)
return
_next_point = _path[0]

# Godot 的输入处理函数之一,接收所有未被其他 UI 元素(如按钮)消耗掉的输入事件。
func _unhandled_input(event):
_click_position = get_global_mouse_position() # 无论点击什么,都先更新 _click_position 为当前鼠标的全局坐标。
if _tile_map.is_point_walkable(_click_position): # 调用 TileMap 脚本中的 is_point_walkable 函数,判断鼠标点击的位置是否是可通过瓦片
if event.is_action_pressed(&"teleport_to", false, true): # 判断是否按下了 teleport_to 动作(鼠标右键)
_change_state(State.IDLE)
global_position = _tile_map.round_local_position(_click_position) # 直接设置位置
elif event.is_action_pressed(&"move_to"): # 判断是否按下了 move_to 动作(鼠标左键)
_change_state(State.FOLLOW)

# 接收一个目标点(_next_point)作为参数
func _move_to(local_position):
var desired_velocity = (local_position - position).normalized() * speed # 得到速度向量
var steering = desired_velocity - _velocity # 获取“转向行为”向量
_velocity += steering / MASS # 将转向力除以质量,得到加速度。
position += _velocity * get_process_delta_time() # 根据更新后的速度和帧时间(delta)来移动角色的位置
rotation = _velocity.angle() # 让角色的旋转角度始终朝向其移动的方向
return position.distance_to(local_position) < ARRIVE_DISTANCE # 判断是否到达目标点

# 这是一个状态机的核心,负责处理状态切换时的逻辑。
func _change_state(new_state):
if new_state == State.IDLE:
_tile_map.clear_path() # 调用 TileMap 的 clear_path() 函数
elif new_state == State.FOLLOW:
# 调用 TileMap 的 find_path 函数,传入角色当前的位置和之前记录的鼠标点击位置,获取一条路径。
_path = _tile_map.find_path(position, _click_position)
if _path.size() < 2: # 路径数组的大小小于 2,意味着 TileMap 没有找到有效路径
_change_state(State.IDLE)
return
_next_point = _path[1] # 设置下一个目标点
_state = new_state # 更新状态