1. 项目概述当寻路遇上“贪吃蛇”AstarGrid2D的对角线模式成了胜负手最近在Godot社区里看到不少朋友在做“贪吃蛇寻路”这类项目时遇到了一个挺有意思的难题蛇身越来越长AI控制的蛇需要自动寻找食物但路径规划总是不尽如人意。要么是蛇头走起来“卡卡的”像在跳格子要么是明明有更近的路AI却偏偏绕了个大弯。如果你也卡在这个点上那问题很可能出在AstarGrid2D的“对角线模式”设置上。AstarGrid2D是Godot 4.2中一个专门为网格化2D地图设计的寻路工具它比通用的AStar2D更高效因为它直接利用了网格的规整性。但很多开发者包括我自己刚开始用的时候都容易忽略它的一个关键参数diagonal_mode。这个参数直接决定了寻路算法如何看待“斜向移动”是让你的AI蛇走“曼哈顿距离”的直线还是能像国际象棋里的“皇后”一样斜着走全看它怎么设。我花了大量时间在几个不同的项目里测试这四种模式从简单的贪吃蛇AI到复杂的策略游戏单位移动踩了不少坑也总结出一些非常实用的经验。今天这篇实战避坑指南就带你彻底搞懂AstarGrid2D的四种对角线模式到底该怎么选以及在不同游戏场景下的最佳实践。2. AstarGrid2D对角线模式深度解析四种模式的本质区别在Godot 4.2的官方文档里AStarGrid2D.diagonal_mode是一个枚举类型有四个选项DIAGONAL_MODE_ALWAYS、DIAGONAL_MODE_NEVER、DIAGONAL_MODE_AT_LEAST_ONE_WALKABLE、DIAGONAL_MODE_ONLY_IF_NO_OBSTACLES。光看名字可能有点抽象我们得结合寻路算法的底层逻辑和实际网格环境来理解。2.1 模式一DIAGONAL_MODE_NEVER (永远不允许对角线移动)这是最严格、也最“安全”的模式。在这种模式下寻路算法只考虑上下左右四个正交方向的邻居格子。从算法角度看它把每个移动的代价通常称为g_cost视为相等的比如每次移动花费1点。启发式函数h_cost用于估算到终点的距离通常使用曼哈顿距离即abs(dx) abs(dy)。核心影响与计算过程假设起点在(0,0)终点在(3,3)。在DIAGONAL_MODE_NEVER模式下最短路径需要先向右走3步再向下走3步或者先下后右总步数为6路径总代价也是6。算法会探索所有可能的正交路径组合。适用场景经典的棋盘类或网格策略游戏比如战棋游戏单位只能前后左右移动一格。需要严格网格对齐的移动某些解谜游戏或平台游戏角色移动必须严格对齐网格线斜向移动会破坏游戏规则或视觉表现。性能要求极高且地图简单的情况由于只计算4个方向邻居节点检查最少理论上性能开销最小。实战避坑点在贪吃蛇寻路中如果你的蛇身已经很长地图上空白格子被分割得很零碎使用这个模式可能会导致AI找不到路径。因为蛇头可能被“困”在一个区域需要斜向移动一步才能到达食物但此模式禁止斜向算法就会返回“无路径”。此时即使视觉上看起来有路算法也认为不可达。2.2 模式二DIAGONAL_MODE_ALWAYS (总是允许对角线移动)这是最“宽松”的模式。算法将上下左右以及四个斜角方向左上、右上、左下、右下都视为可移动的邻居。这就意味着移动方向从4个增加到了8个。核心影响与计算过程这里有个关键点斜向移动的代价是多少在AstarGrid2D中默认的default_estimate_heuristic是欧几里得距离HEURISTIC_EUCLIDEAN。如果移动代价default_compute_cost也使用默认的欧几里得距离计算那么斜向移动一格的代价大约是1.414即√2。算法在计算总代价f_cost g_cost h_cost时会累加这些非整数的代价。继续用(0,0)到(3,3)的例子。最优路径是走一条斜线理想情况下需要走3个斜步因为3*1.414 ≈ 4.242但实际上由于网格离散性路径可能是混合的。算法会计算出总代价最小的那条路径。适用场景追求自然、平滑移动的游戏如RTS即时战略游戏单位可以朝任意方向移动移动轨迹更接近真实。俯视角动作或角色扮演游戏ARPG玩家或AI需要向任意方向自由移动。地图障碍物较少的开放环境能充分发挥斜向移动缩短距离的优势。实战避坑点这是最容易出问题的地方很多开发者直接选用这个模式因为它看起来最“自由”。但在网格环境下斜向移动会带来一个经典问题“切角”Cutting Corners。想象一下两个障碍物紧挨着放在对角位置形成一个“L”形的墙角。如果允许AI从它们中间斜穿过去视觉上AI会“挤”过那个实际上不存在的缝隙这通常是不符合游戏逻辑的除非你的单位是幽灵。在贪吃蛇游戏中如果蛇身构成了这样的直角墙角使用此模式可能导致蛇头错误地判定可以穿过从而撞上自己。2.3 模式三DIAGONAL_MODE_AT_LEAST_ONE_WALKABLE (至少有一个可通行邻格)这个模式是“有条件允许对角线移动”。它允许斜向移动但有一个前提条件在斜向移动的目标格子的两个正交邻格相对于移动方向中至少有一个是可通行的walkable。规则详解假设要从格子A(0,0)斜向移动到格子B(1,1)。算法会检查格子(1,0)和(0,1)这两个格子。只有当(1,0)和(0,1)中至少有一个是可通行的才允许从A直接斜向移动到B。核心影响与计算过程这个规则有效防止了“切角”问题。因为如果(1,0)和(0,1)都是障碍物比如都是蛇身那么从(0,0)到(1,1)的斜向移动就会被禁止。这样AI就不会从两个紧挨着的障碍物中间“挤”过去。它计算路径时会先判断每个潜在的斜向移动是否满足此条件只有满足的才会被加入开放集OpenSet进行后续评估。适用场景需要防止单位穿过墙壁夹角的游戏这是绝大多数2D网格游戏如Roguelike、策略游戏的首选模式。它在提供斜向移动便利性的同时保证了移动的合理性。贪吃蛇AI寻路的黄金标准它能有效防止蛇头试图穿过由自己身体构成的“墙角”是平衡智能性与安全性的最佳选择。任何有密集障碍物的网格地图在障碍物复杂的环境中它能提供比ALWAYS更合理、比NEVER更灵活的路径。实战避坑点这个模式的计算开销会比NEVER和ALWAYS稍大因为每次评估斜向邻居时都需要额外检查两个正交格子。但在现代硬件上这点开销对于大多数游戏来说微乎其微。你需要确保你的“可通行”判断即set_point_solid方法是准确且实时更新的。在贪吃蛇游戏中蛇身每一帧都在变化必须及时调用update_point或重新设置相关格子的固体状态。2.4 模式四DIAGONAL_MODE_ONLY_IF_NO_OBSTACLES (仅当没有障碍物时允许)这是条件最苛刻的斜向移动模式。它要求在斜向移动的目标格子的两个正交邻格相对于移动方向都必须是可通行的。规则详解继续上面的例子从A(0,0)到B(1,1)。只有当(1,0)和(0,1)两者都是可通行的才允许斜向移动。核心影响与计算过程这个规则比AT_LEAST_ONE_WALKABLE严格得多。它基本上只允许在完全开阔的“空地”上进行斜向移动。一旦斜角附近有任何障碍物斜向移动就会被禁止。这会导致路径更加“保守”AI会更倾向于走正交路径即使斜向路径看起来是安全的比如只有一个角被挡住。适用场景对移动安全性要求极高的游戏例如某些策略游戏中单位体积较大需要绝对避免任何可能的碰撞风险即使这个风险在视觉上很小。模拟有“体积”或需要转向空间的单位感觉单位不是一个点而是一个有宽度的物体斜向移动时需要更多空间。特定美学或游戏性需求开发者希望单位移动更加“方正”减少斜向移动带来的锯齿感。实战避坑点在贪吃蛇这类地图空间随着游戏进行会越来越狭窄、障碍物蛇身越来越多的游戏中这个模式会过早地禁止斜向移动。可能导致AI在游戏中后期变得极其笨拙因为几乎找不到同时满足两个正交邻格都空白的斜向移动机会。它通常不是贪吃蛇AI的最佳选择除非你刻意想增加游戏难度。3. 贪吃蛇寻路实战四种模式对比与代码实现理论说完了我们直接上代码看看在Godot 4.2的GDScript中如何为你的贪吃蛇AI配置AstarGrid2D并直观感受四种模式的区别。3.1 基础AstarGrid2D设置与初始化首先我们创建一个SnakeAIController节点并为其添加脚本。extends Node2D class_name SnakeAIController onready var astar_grid: AStarGrid2D AStarGrid2D.new() var grid_size: Vector2i Vector2i(20, 20) # 假设你的游戏区域是20x20的网格 var cell_size: int 32 # 每个网格的像素大小 func _ready(): setup_astar_grid() func setup_astar_grid(diagonal_mode: AStarGrid2D.DiagonalMode AStarGrid2D.DIAGONAL_MODE_AT_LEAST_ONE_WALKABLE): # 1. 初始化网格区域 astar_grid.region Rect2i(0, 0, grid_size.x, grid_size.y) # 2. 设置网格大小本例中与region大小一致表示每个格子都是可寻路单元 astar_grid.cell_size Vector2(cell_size, cell_size) # 3. 设置默认的代价计算和启发式函数欧几里得距离是常见选择 astar_grid.default_compute_cost AStarGrid2D.HEURISTIC_EUCLIDEAN astar_grid.default_estimate_heuristic AStarGrid2D.HEURISTIC_EUCLIDEAN # 4. 关键步骤设置对角线模式 astar_grid.diagonal_mode diagonal_mode # 5. 默认将所有点设为可通行 astar_grid.update() # 6. 标记所有点为可通行非固体 for x in range(grid_size.x): for y in range(grid_size.y): astar_grid.set_point_solid(Vector2i(x, y), false) func update_snake_body(snake_body_cells: Array[Vector2i]): 更新A*网格将蛇身所在格子标记为障碍物 # 首先重置所有点为可通行如果地图有其他固定障碍需要另外处理 # 这里为了简化假设只有蛇身是障碍 for x in range(grid_size.x): for y in range(grid_size.y): astar_grid.set_point_solid(Vector2i(x, y), false) # 然后将蛇身包括头部后的所有部分标记为固体 for cell in snake_body_cells: # 确保坐标在网格范围内 if astar_grid.is_in_boundsv(cell): astar_grid.set_point_solid(cell, true) func find_path_to_food(start_cell: Vector2i, food_cell: Vector2i) - PackedVector2Array: 寻找从蛇头到食物的路径 if not astar_grid.is_in_boundsv(start_cell) or not astar_grid.is_in_boundsv(food_cell): return PackedVector2Array() # 返回空路径 # 调用A*算法获取路径点格子坐标 var id_path: PackedVector2Array astar_grid.get_id_path(start_cell, food_cell) # 将格子坐标转换为世界坐标中心点 var world_path: PackedVector2Array PackedVector2Array() for cell in id_path: var world_pos: Vector2 Vector2(cell) * cell_size Vector2(cell_size, cell_size) * 0.5 world_path.append(world_pos) return world_path3.2 四种模式在贪吃蛇场景下的行为对比测试让我们设计一个简单的测试场景一个10x10的网格蛇身初始为[(5,5), (5,6), (6,6)]L形食物在(8,8)。蛇头在(5,5)。我们分别用四种模式计算路径并分析结果DIAGONAL_MODE_NEVER:路径可能会是(5,5) - (6,5) - (7,5) - (8,5) - (8,6) - (8,7) - (8,8)或类似的直角路径。分析路径完全由水平和垂直移动组成。由于不能斜向移动路径看起来会有些“绕远”。如果蛇身把路堵死可能完全找不到路径。DIAGONAL_MODE_ALWAYS:路径理想情况下是(5,5) - (6,6) - (7,7) - (8,8)。问题注意(6,6)是蛇身的一部分第二个身体段。如果我们的update_snake_body函数正确地将(6,6)标记为固体那么A*算法在计算(5,5)到(6,6)的代价时会发现(6,6)不可通行因此不会选择这条路径。它会尝试其他路线比如(5,5)-(6,5)-(7,6)-(8,7)-(8,8)。但关键在于它可能会尝试(5,5)-(6,6)这个斜向移动只是在评估目标点时被拒绝。更危险的是“切角”场景假设蛇身是[(5,5), (6,5), (6,6)]形成一个直角墙角。从(5,6)到(6,7)的斜向移动在ALWAYS模式下是允许的即使(6,6)是障碍物。这会导致蛇头“擦着”自己的身体过去风险极高。DIAGONAL_MODE_AT_LEAST_ONE_WALKABLE:路径这是最智能的选择。它会寻找类似(5,5) - (6,5) - (7,6) - (8,7) - (8,8)的路径。分析当考虑从(5,5)到(6,6)时算法检查(6,5)和(5,6)。(5,6)是蛇身固体但(6,5)是空地。满足“至少一个可通行”的条件因此这个斜向移动本身是允许被算法评估的。但是目标点(6,6)本身是固体所以最终这个移动选项还是会被排除。算法会安全地绕过蛇身形成的墙角。它能有效利用斜向移动缩短距离同时避免碰撞风险。DIAGONAL_MODE_ONLY_IF_NO_OBSTACLES:路径可能比NEVER模式更保守。因为斜向移动条件苛刻它可能更倾向于走纯粹的直角路径例如(5,5) - (5,4) - (6,4) - (7,5) - (8,6) - (8,7) - (8,8)。分析在游戏后期蛇身遍布地图时这个模式几乎禁止了所有斜向移动AI会表现得非常笨拙。3.3 性能与效果权衡如何做出选择根据上面的分析我们可以得出一个清晰的结论对于贪吃蛇AI寻路DIAGONAL_MODE_AT_LEAST_ONE_WALKABLE在绝大多数情况下是最佳选择。它在路径效率允许斜向和安全性防止切角之间取得了最佳平衡。DIAGONAL_MODE_ALWAYS风险太高容易导致蛇头撞上自己身体形成的“内角”。DIAGONAL_MODE_NEVER过于死板在复杂地形下可能导致寻路失败或路径冗长。DIAGONAL_MODE_ONLY_IF_NO_OBSTACLES过于保守限制了AI的灵活性。性能考量AT_LEAST_ONE_WALKABLE和ONLY_IF_NO_OBSTACLES因为需要额外的邻格检查理论上比前两者稍慢。但在一个20x20的网格上这种差异可以忽略不计。只有当你的网格非常大比如1000x1000并且每帧都需要进行大量寻路计算时才需要考虑使用NEVER或ALWAYS来换取极致的性能。对于贪吃蛇这种规模完全不需要担心。4. 高级技巧与常见问题排查掌握了模式选择你的AstarGrid2D寻路就成功了一大半。但在实际开发中还有一些细节和坑需要注意。4.1 动态更新障碍物的正确姿势在贪吃蛇游戏中蛇身每时每刻都在移动障碍物地图是动态变化的。很多人会犯的一个错误是每帧都完全重建整个A*网格。这是非常低效的。高效的做法是增量更新var previous_body_cells: Array[Vector2i] [] func update_obstacles_efficiently(new_body_cells: Array[Vector2i]): # 找出这一帧新变成障碍物的格子蛇身移动后新占据的位置 var new_obstacles _array_difference(new_body_cells, previous_body_cells) # 找出这一帧从障碍物变为空地的格子蛇尾离开的位置 var freed_cells _array_difference(previous_body_cells, new_body_cells) for cell in new_obstacles: if astar_grid.is_in_boundsv(cell): astar_grid.set_point_solid(cell, true) for cell in freed_cells: if astar_grid.is_in_boundsv(cell): astar_grid.set_point_solid(cell, false) previous_body_cells new_body_cells.duplicate() # 注意要复制数组而不是引用 func _array_difference(array_a: Array, array_b: Array) - Array: # 一个简单的辅助函数求数组差集 var dict_b {} for item in array_b: dict_b[item] true var result [] for item in array_a: if not dict_b.has(item): result.append(item) return result注意set_point_solid会立即改变该点的通行状态。如果你在寻路过程中间修改了网格可能会影响正在进行的寻路计算导致不可预测的结果。最佳实践是在一帧开始或结束时统一更新所有障碍物信息。4.2 启发式函数Heuristic与代价计算Cost的微调default_estimate_heuristic和default_compute_cost也影响着寻路行为。它们默认都是欧几里得距离HEURISTIC_EUCLIDEAN。曼哈顿距离 (HEURISTIC_MANHATTAN)计算abs(dx) abs(dy)。在DIAGONAL_MODE_NEVER模式下使用曼哈顿距离作为启发式函数是一致的admissible and consistent能保证找到最短路径。但在允许斜向移动的模式下它可能会高估实际代价导致A*算法探索更多节点降低效率。切比雪夫距离 (HEURISTIC_CHEBYSHEV)计算max(abs(dx), abs(dy))。在允许八方向移动的网格中如果你将斜向移动的代价设为1而不是1.414那么切比雪夫距离是更合适的启发式函数。你可以通过重写_compute_cost虚函数来自定义移动代价。自定义代价函数你可以继承AStarGrid2D并重写_compute_cost方法来实现更复杂的移动代价。例如给某些类型的格子如沼泽设置更高的移动代价。extends AStarGrid2D class_name CustomAStarGrid2D # 假设我们有一个地形代价字典 var terrain_cost_map: Dictionary {} func _compute_cost(from_id: Vector2i, to_id: Vector2i) - float: var base_cost super._compute_cost(from_id, to_id) # 获取目标格子的地形代价 var terrain_multiplier terrain_cost_map.get(to_id, 1.0) return base_cost * terrain_multiplier4.3 寻路失败与性能优化问题get_id_path返回空数组。检查1起点或终点是否被标记为固体确保在调用寻路前没有意外地将起点蛇头或终点食物设为set_point_solid(true)。检查2网格边界使用is_in_boundsv()确保坐标有效。检查3连通性A*只能找到连通的路径。如果障碍物将起点和终点完全隔开自然无路可走。对于贪吃蛇这意味着AI被困死了游戏可以结束。问题寻路卡顿尤其是在蛇很长时。优化1降低寻路频率。AI不需要每帧都重新寻路。可以每0.1秒或当食物被吃掉时才计算一次新路径。优化2使用get_point_path替代get_id_path。get_point_path直接返回世界坐标路径省去了手动转换的步骤但前提是你的网格原点与世界原点对齐且cell_size设置正确。优化3限制寻路范围。如果地图很大可以使用astar_grid.region限制寻路区域或者实现一个简单的距离检查如果食物离得太远先让AI朝大致方向移动接近后再精细寻路。4.4 一个完整的、健壮的贪吃蛇AI控制器示例结合以上所有要点这里是一个更完善的示例extends Node2D class_name RobustSnakeAI onready var astar_grid: AStarGrid2D AStarGrid2D.new() export var grid_size: Vector2i Vector2i(20, 20) export var cell_size: int 32 export var path_update_interval: float 0.15 # 每0.15秒更新一次路径 var current_path: PackedVector2Array [] var path_update_timer: float 0.0 var snake_body_cache: Array[Vector2i] [] func _ready(): _initialize_astar_grid() # 假设从游戏主逻辑获取初始蛇身 # update_snake_body(initial_body) func _initialize_astar_grid(): astar_grid.region Rect2i(0, 0, grid_size.x, grid_size.y) astar_grid.cell_size Vector2(cell_size, cell_size) astar_grid.default_compute_cost AStarGrid2D.HEURISTIC_EUCLIDEAN astar_grid.default_estimate_heuristic AStarGrid2D.HEURISTIC_EUCLIDEAN # 核心选择使用 AT_LEAST_ONE_WALKABLE astar_grid.diagonal_mode AStarGrid2D.DIAGONAL_MODE_AT_LEAST_ONE_WALKABLE astar_grid.update() # 初始化所有点为可通行 for x in range(grid_size.x): for y in range(grid_size.y): astar_grid.set_point_solid(Vector2i(x, y), false) func _process(delta): path_update_timer - delta if path_update_timer 0.0: path_update_timer path_update_interval # 假设这个函数会被外部调用传入最新的蛇身和食物位置 # recalculate_path(current_snake_head, current_food_position) func update_obstacles_incremental(new_snake_body: Array[Vector2i]): # 增量更新障碍物 var new_obstacles _get_array_difference(new_snake_body.slice(1), snake_body_cache.slice(1)) # 蛇头不算障碍 var freed_cells _get_array_difference(snake_body_cache.slice(1), new_snake_body.slice(1)) for cell in new_obstacles: if astar_grid.is_in_boundsv(cell): astar_grid.set_point_solid(cell, true) for cell in freed_cells: if astar_grid.is_in_boundsv(cell): astar_grid.set_point_solid(cell, false) snake_body_cache new_snake_body.duplicate() func recalculate_path(snake_head_cell: Vector2i, food_cell: Vector2i): if not astar_grid.is_in_boundsv(snake_head_cell) or not astar_grid.is_in_boundsv(food_cell): current_path.clear() return # 临时将蛇头设为可通行因为它是起点 var was_head_solid astar_grid.is_point_solid(snake_head_cell) if was_head_solid: astar_grid.set_point_solid(snake_head_cell, false) # 计算路径 current_path astar_grid.get_point_path(snake_head_cell, food_cell) # 恢复蛇头的固体状态如果是障碍物的一部分 if was_head_solid: astar_grid.set_point_solid(snake_head_cell, true) # 如果路径为空可能是无路可走游戏结束状态 if current_path.is_empty(): print(AI Warning: No path to food found!) # 这里可以触发游戏结束或让AI执行避险行为 func get_next_direction(current_world_pos: Vector2) - Vector2: 根据当前路径返回蛇头应该移动的方向向量 if current_path.size() 2: return Vector2.ZERO # 没有路径或已在终点 var next_target: Vector2 current_path[1] # current_path[0] 是当前位置 var direction: Vector2 (next_target - current_world_pos).normalized() # 可以加入一个容差如果非常接近下一个路径点就将其从路径中移除 if current_world_pos.distance_to(next_target) cell_size * 0.1: current_path.remove_at(0) return direction func _get_array_difference(minuend: Array, subtrahend: Array) - Array: var set_b {} for item in subtrahend: set_b[item] true var diff [] for item in minuend: if not set_b.has(item): diff.append(item) return diff这个控制器提供了增量更新障碍物、定期寻路、路径平滑跟随等功能并且安全地使用了DIAGONAL_MODE_AT_LEAST_ONE_WALKABLE。你可以将它挂载到你的蛇AI节点上并通过调用update_obstacles_incremental和recalculate_path来驱动它。最后关于“贪吃蛇寻路”这个热门话题我想说的是AstarGrid2D是一个强大的工具但选择合适的对角线模式是让它真正智能起来的关键。DIAGONAL_MODE_AT_LEAST_ONE_WALKABLE模式在绝大多数2D网格游戏寻路中都是那个“甜点”它用最小的性能代价换来了更自然、更安全的移动体验。下次当你的游戏单位又在墙上卡住或者做出诡异穿墙动作时先别急着怀疑自己的算法检查一下这个参数很可能就是它没设对。