第十一部分:NavigationAgent2D 和地图寻路:让怪物绕过墙追玩家
上一部分的怪物 AI 已经能做到:
巡逻
发现玩家
直线追击
攻击玩家
受伤
死亡
脱战返回
但它有一个明显问题:
怪物只会直线追玩家
中间有墙、树、石头时,它会直接撞上去
这就需要寻路。
这一部分我们讲:
Navigation 是什么
NavigationRegion2D 是什么
NavigationAgent2D 是什么
地图导航区域怎么做
怪物怎么沿路径追玩家
为什么 NavigationAgent2D 不会自动移动怪物
TileMapLayer 导航怎么理解
常见寻路坑
1. 寻路要解决什么问题
先看直线追击:
var direction = global_position.direction_to(player.global_position)
velocity = direction * chase_speed
move_and_slide()
这段代码的意思是:
怪物永远朝玩家所在方向走
如果中间没有障碍,很好用。
但如果地图是这样:
怪物 墙墙墙墙墙 玩家
怪物会直接撞墙。
因为它根本不知道:
墙不能走
应该绕路
从上面绕还是下面绕
寻路系统就是告诉怪物:
哪些地方能走
哪些地方不能走
从当前位置到目标位置应该经过哪些点
2. Godot 2D Navigation 的核心组件
Godot 2D 寻路常见组件有:
NavigationRegion2D
NavigationPolygon
NavigationAgent2D
NavigationServer2D
TileMapLayer 的 navigation layer
AStar2D
你现在先重点掌握两个:
NavigationRegion2D
NavigationAgent2D
官方文档里说,Godot 提供了多种 2D 导航和寻路相关对象,包括基于导航区域、导航多边形、导航代理等的方案。(docs.godotengine.org)
你现在可以先这样理解:
NavigationRegion2D:
地图上哪些区域可以走
NavigationAgent2D:
某个角色想从 A 点走到 B 点时,帮它算下一步往哪走
3. NavigationRegion2D 是什么
NavigationRegion2D 表示一块可以通行的 2D 导航区域。
它里面有一个 NavigationPolygon,用来描述“这片地方可以走”。
官方文档对 NavigationRegion2D 的描述是:它是基于 NavigationPolygon 的可通行 2D 区域,NavigationAgent2D 可以使用这些区域来进行寻路;两个 region 想自动连接,需要共享相似边缘,仅仅重叠并不够。(docs.godotengine.org)
你可以把它理解成:
给 AI 看的可走地图
注意,不是给玩家看的图块地图。
比如画面上你看到:
草地
石路
树
墙
河流
导航区域则关心:
草地能走
石路能走
树不能走
墙不能走
河流不能走
4. NavigationAgent2D 是什么
NavigationAgent2D 是挂在角色身上的寻路助手。
官方文档说,NavigationAgent2D 是一个 2D agent,用来向某个位置寻路,并能避开静态和动态障碍;它需要导航数据才能正常工作。(docs.godotengine.org)
结构通常是:
Slime CharacterBody2D
├── AnimatedSprite2D
├── CollisionShape2D
├── NavigationAgent2D
├── DetectionArea Area2D
├── AttackArea Area2D
└── Hurtbox Area2D
但一定要记住:
NavigationAgent2D 不会自动移动怪物
它只是告诉你:
下一步应该往哪个路径点走
真正移动还是你自己写:
velocity = direction * speed
move_and_slide()
官方文档也明确说明,导航系统不会移动 NavigationAgent 的父节点,移动完全由用户自己的脚本负责。(docs.godotengine.org)
这个点非常重要。
很多人以为加了 NavigationAgent2D,怪物就会自动走路。
不会。
Godot 不是滴滴司机,它只给路线,方向盘还在你手里。
5. NavigationAgent2D 的基本使用流程
寻路流程是:
1. 地图上有可走导航区域
2. 怪物身上有 NavigationAgent2D
3. 给 agent 设置 target_position
4. 每个物理帧读取 get_next_path_position()
5. 怪物朝这个 next_path_position 移动
6. 到达后继续取下一个路径点
7. 最终抵达目标
代码大概是:
navigation_agent.target_position = player.global_position
var next_path_position = navigation_agent.get_next_path_position()
var direction = global_position.direction_to(next_path_position)
velocity = direction * chase_speed
move_and_slide()
官方文档也说明,设置 target_position 后,可以用 get_next_path_position() 获取路径中的下一个位置,然后用自己的移动代码让父节点朝这个位置移动。(docs.godotengine.org)
6. 第一种方式:手动画 NavigationRegion2D
这是最容易理解的方式。
结构:
World Node2D
├── Ground TileMapLayer
├── Obstacles TileMapLayer
├── NavigationRegion2D
├── Enemies Node2D
│ └── Slime
└── Player
NavigationRegion2D 里画一块可走区域。
比如你的地图是一块草地,中间有一个房子不能走。
导航区域大概就是:
整张草地区域
减去房子区域
减去墙区域
减去水区域
手动画的优点:
直观
适合小地图
容易理解
便于调试
缺点:
地图大了很累
地图改了导航区域也要跟着改
和 TileMap 不自动同步
第一阶段我建议你先手动画一块简单导航区域,理解 NavigationAgent2D 怎么跑。
7. NavigationRegion2D 的简单搭建步骤
大概流程:
1. 在 World 下添加 NavigationRegion2D
2. 选中 NavigationRegion2D
3. 在 Inspector 里新建 NavigationPolygon
4. 使用编辑器里的多边形工具画可走区域
5. 保存
6. 给 Slime 添加 NavigationAgent2D
7. 让 Slime 通过 agent 寻路追玩家
先别画复杂地图。
先画一个简单区域:
一块矩形可走区域
中间有一个障碍物空洞
用它测试怪物能不能绕路。
8. 第二种方式:TileMapLayer 导航
如果你用 TileMapLayer 做地图,也可以在 TileSet 里给 tile 设置导航形状。
TileMapLayer 本身使用 TileSet 创建网格地图;Godot 4 中旧 TileMap 节点已经 deprecated,新项目更推荐用多个 TileMapLayer。(docs.godotengine.org)
大致思路是:
TileSet 里:
给可走 tile 设置 Navigation Polygon
TileMapLayer 里:
你画了哪些可走 tile
导航系统就能知道哪些格子可走
比如:
草地 tile:有导航区域
石路 tile:有导航区域
墙 tile:没有导航区域
水 tile:没有导航区域
树 tile:没有导航区域
这样怪物就会沿着有导航的 tile 走。
9. TileMapLayer 导航适合什么情况
适合:
网格地图
大部分可走区域由 tile 决定
地图经常用 TileMap 编辑
每个 tile 的可走性比较固定
比如你的 RPG:
草地能走
路能走
墙不能走
河不能走
树干不能走
很适合用 TileSet 的导航层。
不过第一阶段,我还是建议:
先手动 NavigationRegion2D
再研究 TileSet navigation layer
因为 TileSet 面板已经有:
Physics Layer
Terrain
Pattern
Navigation Layer
Occlusion Layer
Custom Data Layer
一口气全学,脑子会像浏览器开了 80 个 tab。
先掌握 NavigationAgent2D 的运动逻辑,再回头接 TileMap 导航,会稳很多。
10. 给 Slime 添加 NavigationAgent2D
结构:
Slime CharacterBody2D
├── AnimatedSprite2D
├── CollisionShape2D
├── NavigationAgent2D
├── DetectionArea Area2D
├── AttackArea Area2D
└── Hurtbox Area2D
脚本里拿到:
@onready var navigation_agent: NavigationAgent2D = $NavigationAgent2D
在 _ready() 里可以设置一些参数:
func _ready():
navigation_agent.path_desired_distance = 4.0
navigation_agent.target_desired_distance = 8.0
简单理解:
path_desired_distance:
离当前路径点多近时,认为这个路径点已经到达
target_desired_distance:
离最终目标多近时,认为已经到达目标
这些参数太小,怪物可能会在路径点附近抖动。
太大,怪物可能离目标还远就停。
11. 用 NavigationAgent2D 改造 CHASE 状态
原来的直线追击:
func physics_chase(delta):
if player == null:
change_state(EnemyState.RETURN)
return
var distance = global_position.distance_to(player.global_position)
if distance <= attack_distance and can_attack:
change_state(EnemyState.ATTACK)
return
var direction = global_position.direction_to(player.global_position)
last_direction = direction
velocity = direction * chase_speed
move_and_slide()
play_animation("walk")
update_flip(direction)
改成寻路追击:
func physics_chase(delta):
if player == null:
change_state(EnemyState.RETURN)
return
var distance = global_position.distance_to(player.global_position)
if distance <= attack_distance and can_attack:
change_state(EnemyState.ATTACK)
return
navigation_agent.target_position = player.global_position
if navigation_agent.is_navigation_finished():
velocity = Vector2.ZERO
move_and_slide()
return
var next_path_position = navigation_agent.get_next_path_position()
var direction = global_position.direction_to(next_path_position)
last_direction = direction
velocity = direction * chase_speed
move_and_slide()
play_animation("walk")
update_flip(direction)
现在怪物不再直接朝玩家走,而是:
先问 NavigationAgent2D:
我下一步该往哪个路径点走?
然后朝那个点移动
12. target_position 要不要每帧设置
玩家一直在移动,所以追击时可以每帧更新:
navigation_agent.target_position = player.global_position
但如果目标不动,比如返回出生点,就不需要每帧都设置。
可以进入 RETURN 状态时设置一次:
func enter_state(state: EnemyState):
match state:
EnemyState.RETURN:
navigation_agent.target_position = spawn_position
然后在 physics_return() 里读取路径点。
不过第一版为了简单,每帧设置也能跑。
优化以后再说。
13. get_next_path_position() 很关键
get_next_path_position() 不是单纯“拿一个坐标”。
它还会更新 agent 内部路径状态。
官方文档也强调,设置 target 后,通过 get_next_path_position() 获取下一个路径点,然后用自己的移动代码向它移动。(docs.godotengine.org)
所以不要只设置:
navigation_agent.target_position = player.global_position
然后什么都不做。
你必须:
var next_path_position = navigation_agent.get_next_path_position()
再移动怪物。
14. RETURN 状态也可以用寻路
原来的返回出生点是直线:
var direction = global_position.direction_to(spawn_position)
velocity = direction * return_speed
move_and_slide()
如果中间有墙,也会卡。
改成:
func physics_return(delta):
navigation_agent.target_position = spawn_position
if navigation_agent.is_navigation_finished():
global_position = spawn_position
change_state(EnemyState.PATROL)
return
var next_path_position = navigation_agent.get_next_path_position()
var direction = global_position.direction_to(next_path_position)
last_direction = direction
velocity = direction * return_speed
move_and_slide()
play_animation("walk")
update_flip(direction)
这样怪物脱战后也会绕路回出生点。
15. 巡逻也可以用寻路
如果巡逻点之间可能有障碍,就也用 agent。
func physics_patrol(delta):
if patrol_points.is_empty():
change_state(EnemyState.IDLE)
return
var target = patrol_points[patrol_index]
navigation_agent.target_position = target
if navigation_agent.is_navigation_finished():
patrol_index = (patrol_index + 1) % patrol_points.size()
return
var next_path_position = navigation_agent.get_next_path_position()
var direction = global_position.direction_to(next_path_position)
last_direction = direction
velocity = direction * patrol_speed
move_and_slide()
play_animation("walk")
update_flip(direction)
不过有一个小问题:
刚切换巡逻点时,NavigationAgent2D 可能还没立即更新路径
如果遇到路径怪怪的,可以在切状态或切巡逻点后稍等一帧再取路径。
第一版通常先不用管。
16. 重要:NavigationAgent2D 的路径不等于碰撞
这是个大坑。
导航区域告诉怪物:
哪里理论上能走
碰撞系统告诉怪物:
实际移动时会不会撞到东西
这两个系统不是同一个东西。
也就是说:
导航区域画得能走
但地图碰撞挡住了
怪物还是可能卡住
地图看起来能走
但导航区域没覆盖
怪物就不会走那里
所以你要保证:
可走区域的导航
和
地图实际碰撞
大致一致
比如:
墙 tile 有碰撞
墙区域就不应该有导航
草地 tile 没碰撞
草地区域应该有导航
否则 AI 就会出现“脑子说能走,身体说不行”的精神分裂式卡墙。
17. Agent 半径和贴墙问题
寻路系统经常会出现一个问题:
路径贴着墙边走
怪物身体有宽度
结果怪物撞墙卡住
原因是:
导航区域太贴近障碍物边缘
没有给怪物身体留安全距离
解决思路:
导航区域离墙远一点
NavigationPolygon 不要贴着障碍边画
TileMap 导航 tile 不要覆盖太靠近墙的地方
怪物碰撞体不要太大
对你现在的 RPG 来说:
怪物 CollisionShape2D 建议只覆盖脚底附近
导航区域也留一点边距
不要让怪物用一个巨大的全身碰撞体去走贴墙路径。
18. NavigationAgent2D 的 avoidance 是什么
NavigationAgent2D 也支持避障相关能力。
官方文档提到,它可以避开静态和动态障碍,并且动态障碍避让使用 RVO;避让计算发生在物理步骤之前。(docs.godotengine.org)
但你现在先不要急着开复杂 avoidance。
第一阶段你只需要:
沿导航路径追玩家
绕过静态墙体
动态避让、怪物之间互相绕开,以后再说。
现在多个史莱姆挤成团不丢人,它们本来就像果冻开会。
19. 用 NavigationAgent2D 改造 Enemy.gd 的关键片段
你不需要一下子重写整个 Enemy.gd。
只要先加:
@onready var navigation_agent: NavigationAgent2D = $NavigationAgent2D
然后把 physics_chase() 改掉。
func physics_chase(delta):
if player == null:
change_state(EnemyState.RETURN)
return
var distance = global_position.distance_to(player.global_position)
if distance <= attack_distance and can_attack:
change_state(EnemyState.ATTACK)
return
navigation_agent.target_position = player.global_position
if navigation_agent.is_navigation_finished():
velocity = Vector2.ZERO
move_and_slide()
return
var next_path_position = navigation_agent.get_next_path_position()
var direction = global_position.direction_to(next_path_position)
last_direction = direction
velocity = direction * chase_speed
move_and_slide()
play_animation("walk")
update_flip(direction)
先只改追击。
等追击没问题,再改:
RETURN
PATROL
这样分步调试,不会一炸炸一片。
20. 更干净的 move_to_position() 封装
你会发现 CHASE、RETURN、PATROL 都在做同样的事:
设置目标
获取下一个路径点
计算方向
移动
播放动画
可以封装:
func move_to_position(target_position: Vector2, speed: float) -> bool:
navigation_agent.target_position = target_position
if navigation_agent.is_navigation_finished():
velocity = Vector2.ZERO
move_and_slide()
return true
var next_path_position = navigation_agent.get_next_path_position()
var direction = global_position.direction_to(next_path_position)
last_direction = direction
velocity = direction * speed
move_and_slide()
play_animation("walk")
update_flip(direction)
return false
返回值:
true:已经到达
false:还在路上
CHASE:
func physics_chase(delta):
if player == null:
change_state(EnemyState.RETURN)
return
var distance = global_position.distance_to(player.global_position)
if distance <= attack_distance and can_attack:
change_state(EnemyState.ATTACK)
return
move_to_position(player.global_position, chase_speed)
RETURN:
func physics_return(delta):
var arrived = move_to_position(spawn_position, return_speed)
if arrived:
global_position = spawn_position
change_state(EnemyState.PATROL)
PATROL:
func physics_patrol(delta):
if patrol_points.is_empty():
change_state(EnemyState.IDLE)
return
var target = patrol_points[patrol_index]
var arrived = move_to_position(target, patrol_speed)
if arrived:
patrol_index = (patrol_index + 1) % patrol_points.size()
这样 Enemy.gd 会清爽很多。
21. 但是 move_to_position() 也有一个坑
如果你每帧都设置:
navigation_agent.target_position = target_position
对于移动目标,比如玩家,是合理的。
但对于固定目标,比如 patrol point / spawn position,也可以优化成进入状态时设置一次。
不过第一版不用纠结。
你现在最重要的是:
能绕墙追
能绕墙回去
优化等卡了再说。
22. 寻路目标点不在导航区域上怎么办
如果玩家站在一个没有导航的地方,比如墙里、河里、房子里,怪物可能无法找到正确路径。
表现可能是:
怪物不动
怪物路径很怪
get_next_path_position 返回异常位置
解决:
确保玩家能走的区域都有导航
确保玩家不会站到导航区域外
门、室内外切换时出生点放在导航区域内
你后面用 Marker2D 做出生点时,也要注意:
PlayerSpawn 最好放在导航区域内
否则怪物发现玩家后寻路目标可能不稳定。
23. 导航区域要和地图边界一致
如果你的地图是 30x20 tile。
导航区域不要超出地图太多。
否则怪物可能会走到画面外。
也不要太小。
否则玩家走到边缘,怪物追不到。
推荐:
导航区域覆盖所有可走地面
障碍物区域留空
地图外区域不画导航
这是最朴素也最可靠的做法。
24. NavigationRegion2D 和 TileMapLayer 导航能不能混用
可以,但初学阶段不建议混。
比如你一部分地图手动画 NavigationRegion2D,另一部分用 TileMapLayer navigation layer,理论上可以成为同一导航地图的一部分。
但调试时会比较麻烦:
到底是手画区域出错
还是 TileSet 导航出错
还是连接边缘出错
所以建议你先选一种。
入门路线:
小测试地图:NavigationRegion2D 手画
正式 TileMap 地图:TileSet navigation layer
25. TileSet 里怎么理解 Navigation Layer
TileSet 可以给 tile 添加不同类型的数据,比如:
碰撞
导航
遮挡
自定义数据
导航层的意思是:
这个 tile 上哪些区域对 AI 来说是可走的
比如一个 32x32 草地 tile:
整格都可走
它的 navigation polygon 就可以是整个 32x32。
一个墙 tile:
不可走
它就不设置 navigation polygon。
一个桥 tile:
桥面可走
桥外不可走
它就只给桥面区域设置 navigation polygon。
26. TileMapLayer 导航和碰撞都在 TileSet 里配置
之前我们讲地图碰撞时,做的是:
给墙 tile 设置 Physics Collision
现在导航类似:
给可走 tile 设置 Navigation Polygon
也就是说:
Physics Layer:
决定角色会不会被挡住
Navigation Layer:
决定 AI 认为哪里能走
这两者要配合。
比如:
墙 tile:
有 Physics Collision
没有 Navigation
草地 tile:
没有 Physics Collision
有 Navigation
27. 为什么不要给所有 tile 都加 Navigation
如果你给墙、树、水都加了 Navigation,AI 会认为这些地方都能走。
结果:
怪物路径会穿墙
然后实际移动时被碰撞卡住
所以:
能走的 tile 才加 Navigation
不能走的 tile 不加 Navigation
这个和碰撞刚好反着:
能走:通常没碰撞,有导航
不能走:通常有碰撞,没导航
28. TileMap 导航的一个现实问题
TileMap 导航很方便,但也有一个常见问题:
导航区域贴着障碍 tile 的边缘
怪物身体有宽度
所以会贴墙走,然后卡住
尤其是:
怪物 CollisionShape2D 比较大
导航 polygon 覆盖整格草地
墙旁边草地也有导航
路径贴墙边
怪物撞墙
解决办法:
怪物碰撞体做小一点
导航区域离墙留一点余量
某些靠墙 tile 不设置导航
或者以后使用更细致的导航区域
所以第一版别要求完美。
RPG 像素地图里,怪物碰撞体小一点通常最省事。
29. 调试 Navigation 的思路
如果怪物不动,先按这个顺序查:
地图上有没有 NavigationRegion2D 或 TileMap 导航数据
Slime 是否有 NavigationAgent2D
NavigationAgent2D 是否在导航地图范围内
目标 player.global_position 是否在导航区域内
是否调用了 target_position
是否调用了 get_next_path_position()
是否自己写了 velocity 和 move_and_slide()
特别注意最后一点:
NavigationAgent2D 不会自动移动
你必须自己移动。
30. 可视化调试
Godot 可以显示调试信息。
你可以在运行时开启可见碰撞和导航调试。
常见操作是:
Debug
Visible Collision Shapes
导航调试也可以在相关 debug 设置中打开。
你至少要经常开:
Visible Collision Shapes
这样你能看到:
Player 碰撞范围
Enemy 碰撞范围
DetectionArea 范围
AttackArea 范围
Hurtbox 范围
如果你不看这些形状,很多 bug 就像黑夜里摸猫,还不确定猫在不在房间里。
31. NavigationAgent2D 常用属性
先记这些:
target_position:
目标位置
path_desired_distance:
离路径点多近算到达
target_desired_distance:
离最终目标多近算到达
avoidance_enabled:
是否启用避障
radius:
agent 半径,和避障相关
第一阶段建议只调:
path_desired_distance
target_desired_distance
比如:
func _ready():
navigation_agent.path_desired_distance = 4.0
navigation_agent.target_desired_distance = 8.0
如果怪物在目标附近抖动,可以把数值调大一点。
32. 攻击距离要大于目标到达距离
假设:
attack_distance = 28
target_desired_distance = 32
那可能出现:
NavigationAgent 觉得已经到达玩家附近
怪物停了
但 attack_distance 又不满足
怪物不攻击
所以通常建议:
attack_distance >= target_desired_distance
或者更简单:
攻击判断用真实距离
移动判断用导航路径
我们现在的代码就是:
var distance = global_position.distance_to(player.global_position)
if distance <= attack_distance and can_attack:
change_state(EnemyState.ATTACK)
return
这比完全依赖 is_navigation_finished() 更直观。
33. 玩家快速移动时,怪物路径更新频率
如果每帧设置:
navigation_agent.target_position = player.global_position
怪物会持续更新追击路径。
这对少量怪物没问题。
但如果你有几十上百只怪物,每帧全部重新寻路可能会贵。
以后可以优化成:
每 0.2 秒更新一次 target_position
玩家移动超过一定距离才更新路径
离玩家很远的怪物降低更新频率
屏幕外怪物暂停 AI
现在别管。
你当前阶段怪物数量少,先能跑。
34. 一个更完整的寻路版 Enemy 片段
这里只给关键变化,不重复整个 Enemy.gd。
@onready var navigation_agent: NavigationAgent2D = $NavigationAgent2D
func _ready():
navigation_agent.path_desired_distance = 4.0
navigation_agent.target_desired_distance = 8.0
封装移动:
func move_to_position(target_position: Vector2, speed: float) -> bool:
navigation_agent.target_position = target_position
if navigation_agent.is_navigation_finished():
velocity = Vector2.ZERO
move_and_slide()
return true
var next_path_position = navigation_agent.get_next_path_position()
var direction = global_position.direction_to(next_path_position)
if direction == Vector2.ZERO:
velocity = Vector2.ZERO
move_and_slide()
return true
last_direction = direction
velocity = direction * speed
move_and_slide()
play_animation("walk")
update_flip(direction)
return false
CHASE:
func physics_chase(delta):
if player == null:
change_state(EnemyState.RETURN)
return
var distance = global_position.distance_to(player.global_position)
if distance <= attack_distance and can_attack:
change_state(EnemyState.ATTACK)
return
move_to_position(player.global_position, chase_speed)
RETURN:
func physics_return(delta):
var arrived = move_to_position(spawn_position, return_speed)
if arrived:
global_position = spawn_position
change_state(EnemyState.PATROL)
PATROL:
func physics_patrol(delta):
if patrol_points.is_empty():
change_state(EnemyState.IDLE)
return
var target = patrol_points[patrol_index]
var arrived = move_to_position(target, patrol_speed)
if arrived:
patrol_index = (patrol_index + 1) % patrol_points.size()
这就是把直线移动替换成导航移动的核心。
35. 如果没有导航数据,get_next_path_position 会怎样
如果 agent 没有可用路径,表现可能是:
返回当前位置
怪物不动
路径异常
这种情况通常不是代码追击错了,而是导航地图没有正确建立。
排查:
NavigationRegion2D 是否真的画了 NavigationPolygon
Slime 是否在 NavigationPolygon 内
Player 是否在 NavigationPolygon 内
NavigationRegion2D 是否启用
NavigationAgent2D 是否在正确场景树里
36. 室内地图也需要导航
你以后切换到室内:
HouseInterior
如果室内也有怪物或 NPC 自动走路,也需要导航区域。
每张地图都应该有自己的导航数据:
ForestMap:森林导航区域
TownMap:村庄导航区域
HouseInterior:室内导航区域
DungeonMap:地牢导航区域
不要指望一张导航区域通吃所有地图。
37. NPC 移动也可以用 NavigationAgent2D
不只是怪物。
以后 NPC 也可能需要:
走到玩家面前
按时间表去商店
晚上回家
剧情中移动到指定位置
这些也可以用 NavigationAgent2D。
所以你现在学这个,不只是为了怪物。
以后很多“会自己走路的角色”都会用它。
38. NavigationAgent2D 和 AStar2D 的区别
你现在可能会看到另一个类:
AStar2D
AStar2D 更偏底层,需要你自己管理点、边、权重。
适合:
棋盘格寻路
战棋游戏
自己生成路径图
特殊规则路径
NavigationAgent2D 更适合:
普通 2D 地图角色寻路
RPG 怪物追玩家
NPC 自动走路
你现在做 RPG,优先学:
NavigationRegion2D + NavigationAgent2D
别先跳去 AStar2D。
39. 什么时候需要 NavigationObstacle2D
如果你有动态障碍,比如:
移动的箱子
临时挡路的 NPC
其他怪物
玩家放下的障碍物
以后可以用 NavigationObstacle2D。
但第一阶段不要加。
先让静态地图寻路跑通。
动态避障是进阶内容,不要现在就把自己扔进怪物堆。
40. 常见错误 1:加了 NavigationAgent2D,怪物还是不动
原因通常是:
你以为 agent 会自动移动
实际上必须写:
var next_path_position = navigation_agent.get_next_path_position()
var direction = global_position.direction_to(next_path_position)
velocity = direction * chase_speed
move_and_slide()
只设置 target 不够。
41. 常见错误 2:怪物绕路了,但还是卡墙
原因可能是:
导航区域太贴墙
怪物碰撞体太大
路径点贴着障碍边缘
地图碰撞和导航区域不一致
解决:
缩小怪物脚底碰撞
导航区域离墙留边距
不要给靠墙不可走区域加导航
调整 NavigationAgent2D 的距离参数
42. 常见错误 3:怪物在目标点附近抖动
尝试调整:
navigation_agent.path_desired_distance = 6.0
navigation_agent.target_desired_distance = 10.0
或者:
攻击距离稍微调大
不要让怪物必须贴到玩家中心点才算到达
RPG 里怪物攻击距离一般应该比身体半径大一点。
43. 常见错误 4:玩家能走的地方,怪物不走
原因:
玩家靠 move_and_slide 在地图碰撞里移动
怪物靠 NavigationAgent2D 寻路
但导航区域没覆盖那块地方
解决:
给那块可走地面加导航
检查 TileSet navigation polygon
检查 NavigationRegion2D 是否覆盖
44. 常见错误 5:怪物试图穿过水或墙
原因:
水或墙 tile 也被设置了 navigation
解决:
不可走 tile 不要设置 navigation polygon
NavigationRegion2D 里把不可走区域挖掉
45. 常见错误 6:切地图后怪物寻路失效
原因可能是:
新地图没有 NavigationRegion2D
新地图导航区域没有进入 SceneTree
怪物出生点不在导航区域内
玩家出生点不在导航区域内
每张地图都要检查:
导航区域
出生点
怪物位置
玩家位置
46. 这一部分最重要的记忆点
NavigationRegion2D 定义哪里能走
NavigationAgent2D 帮角色计算下一步路径点
NavigationAgent2D 不会自动移动角色
移动仍然要自己写 velocity + move_and_slide()
target_position 是目标位置
get_next_path_position() 是下一个路径点
地图碰撞和导航区域必须一致
可走区域应该有导航
不可走区域不应该有导航
导航区域不要太贴墙
怪物碰撞体不要太大
先用手画 NavigationRegion2D 理解,再接 TileMapLayer 导航
47. 你现在的小练习
先做一个最小寻路测试:
1. 创建一个小地图,中间放一堵墙
2. 添加 NavigationRegion2D
3. 画出墙外可走区域,并绕开墙
4. 给 Slime 添加 NavigationAgent2D
5. 修改 physics_chase()
6. 玩家进入 DetectionArea 后,Slime 追玩家
7. 玩家站到墙另一边
8. 检查 Slime 是否能绕墙追过来
9. 玩家跑远后,Slime 用导航返回出生点
完成这一步之后,你的怪物就从“直线撞墙型 AI”进化成“知道绕路的 AI”。
如果您觉得这篇文章有帮助,请点个赞吧~
评论
请登录后发表评论
去登录