第十一部分:NavigationAgent2D 和地图寻路:让怪物绕过墙追玩家

2026-05-19
499517 分钟
...

上一部分的怪物 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. 玩家进入 DetectionAreaSlime 追玩家
7. 玩家站到墙另一边
8. 检查 Slime 是否能绕墙追过来
9. 玩家跑远后Slime 用导航返回出生点

完成这一步之后,你的怪物就从“直线撞墙型 AI”进化成“知道绕路的 AI”。

如果您觉得这篇文章有帮助,请点个赞吧~

分享文章

相关文章

更多文章 →
godot2026-07-27
Godot 4 常用 UI 节点详解
Godot 4 常用 UI 节点详解 在 Godot 4 中,UI 系统基于 Control(控件) 节点构建。所有 UI 节点都继承自 Control,形成一棵完整的 UI 树。与游戏引擎中常见的"Canvas + DOM"模式不同,Godot 的 UI 系统是声明式的——你在场景中搭好节点树,引擎自动完成布局计算。 一、布局系统:Container 家族 Container 是 Godot UI 的 骨架 。它决定了子节点的大小和位...
学习
godot2026-07-09
Godot 4 自动地形系统(AutoTileSet / Terrains)完全指南
前言 在 2D 游戏开发中,地形瓦片(tile)的拼接是一个绕不开的问题。想象一下:你有一片草地、一条河流、一段平台——如果每一块边缘、角落、过渡区域都要手动选择对应的瓦片图,工作量将是巨大的。 自动地形系统 就是为了解决这个问题而生的。 一、什么是自动地形(Autotiling)? 自动地形的核心思想很简单: 你只管画,引擎帮你选对瓦片 。 当你在 TileMap 上绘制地形时,引擎会自动检测每个瓦片的上下左右邻居,然后根据预设的规则...
学习
godot2026-06-25
Godot 中 zindex 和 ysort 的区别总结
在 Godot 2D 游戏开发中,角色、树木、怪物、地面、技能特效、UI 都需要正确的显示顺序。比如角色走到树前面时,角色应该挡住树;角色走到树后面时,树又应该挡住角色。 这种显示顺序主要和两个概念有关: 和 。 其中 用来手动控制图层顺序, 用来根据物体的 Y 坐标自动排序。 一句话理解 是手动分层。 是根据 Y 坐标自动排序。 简单来说: | 属性 | 作用 | 适合场景 | | | | | | | 数值越大,显示越靠前 | 地面、...
学习
godot2026-06-11
Tileset 资源图的标准和规范
一、什么是 Tileset 资源图 Tileset,中文通常叫“图块资源图”或“瓦片图”,是 2D 游戏中非常常见的一种地图资源组织方式。 简单来说,Tileset 就是把很多小图块按照固定尺寸排列在一张图片里。游戏引擎会按照固定的格子大小去切割这张图片,然后把每个小格子当成一个独立的地图块使用。 比如一个 32×32 像素的 Tileset 中,每一个 tile 都是 32×32 像素。地图编辑器或游戏引擎会按照 32×32 的网格,...
学习
godot2026-05-29
用户角色精灵图制作角色的完整流程
在 2D RPG 游戏里,角色通常不是用一张单独图片完成的,而是用一张“角色精灵图”来做。 所谓角色精灵图,通常是一张包含多个动作帧的大图。比如角色向下走有 4 帧,向左走有 4 帧,向右走有 4 帧,向上走有 4 帧。Godot 会根据这些帧不断切换图片,看起来角色就动起来了。 这篇文章主要介绍:拿到一张角色精灵图之后,如何在 Godot 中把它做成一个可以正常移动、播放动画、和地图产生遮挡关系的角色。 一、先理解角色精灵图是什么 角...
学习
godot2026-05-26
Godot 节点系统详细介绍
Godot 里最核心的东西不是“类”,也不是“组件”,而是 节点 Node 。 你可以把 Godot 的节点理解成: 在前端里,一个页面是由很多 DOM 元素组成的; 在 Godot 里,一个游戏场景是由很多 Node 节点组成的。 比如一个玩家角色,可能不是一个单独对象,而是这样的结构: 这里的 是根节点,下面挂着显示图片、播放动画、碰撞检测、摄像机、音效等子节点。 Godot 官方文档也把节点和场景放在一起讲:多个节点组成树状结构后...
学习

评论

请登录后发表评论

去登录
加载评论中...