跳转至

第十一章:路径规划与导航系统

本章导言

在上一章中,我们通过射线检测实现了敌人单位的简单避障行为。这种基于局部感知的方式虽然实现成本低,但也存在明显的局限。其一是路径死板,巡逻路线只能设置为简单的矩形,缺乏转弯与动态调整的能力;其二是容易卡死,避障只关注前方是否有障碍,而不具备全局路径规划能力,敌人经常被困在障碍物之间。

为了让敌方单位拥有更智能、更灵活的移动行为,本章将引入 Godot 引擎中的自动导航系统(Navigation System),通过导航图层(Navigation Layer)与导航代理(NavigationAgent2D),帮助敌人实现从当前位置到任意目标点的自动路径规划,并在移动过程中避开静态和动态障碍物。

在本章中,你将学习以下关键内容:

  • 如何在 TileMap 地图中设置可通行区域,并构建多图层的导航信息;
  • 如何为敌人单位添加 NavigationAgent2D 节点,并设置其目标与避障半径;
  • 如何在敌人的状态机中集成路径导航逻辑,实现“巡逻”“索敌”“追击”等状态下的智能移动;
  • 如何通过调试工具可视化导航路径与避障行为,评估路径规划的效果。

完成本章后,你将能够构建出具有真实路径规划能力的敌方单位,它们不仅能在地图上灵活巡逻,还能根据玩家的位置发起追击、自动寻找路线并躲避同伴,显著提升游戏的智能感与挑战性。

1 导航系统初识

在开发敌人AI时,最常见的任务之一就是“让某个敌人单位从当前位置移动到目标位置”,这看似简单的问题,背后却蕴含着复杂的路径计算逻辑。要实现“绕开障碍,智能地移动”,我们需要为敌人单位构建一套路径规划系统。而Godot正好为我们提供了功能强大的导航系统(Navigation System),能够自动完成路径计算与避障处理。

在正式动手实现前,我们先来认识一下“路径规划”与“局部避障”这两种不同的移动方式,以及Godot引擎在这方面的技术架构。

1.1 路径规划 vs 局部避障

在前一章中,我们使用“射线检测”的方式让敌人单位在行进过程中检测前方是否有障碍物,如果有,则调整方向避开。这种方式属于局部避障策略,其特点是:

  • 无需全图路径规划,只关注单位当前周围的环境;
  • 计算成本低,适合快速实现;
  • 行为偏向反应式,依赖局部感知,不一定能避开“死胡同”;
  • 容易卡住,无法绕开大型障碍或复杂区域。

路径规划是通过提前了解地图上的“可通行区域”,然后计算出从当前位置到目标位置的一条最优路径,其特点是:

  • 具备全局视野,考虑整个地图结构;
  • 可绕开多层障碍,适用于复杂地图;
  • 路径平滑且可控,更符合真实游戏行为;
  • 支持动态避障,可避免移动单位之间的碰撞。

在真实游戏中,路径规划通常作为“AI行动”的基础,而局部避障则是“AI移动修正”的补充。

1.2 Godot中的导航体系结构

Godot提供了功能强大的导航系统,可以让开发者轻松实现复杂地图中的路径规划与动态避障。其核心组件包括:

  • NavigationServer2D:它是导航系统的底层引擎,它负责管理导航地图、区域(regions)、代理(agents)等信息,集中调度路径计算和动态避障。导航地图由多个可通行区域(region)和边连接组成,可以支持多个 agent 的并发路径查询和避障请求。所有对导航地图的修改(如区块变更、添加代理)会在下一帧物理同步执行,避免计算冲突。
  • NavigationRegion2D:它用于构建导航地图中的可通行区域,代理可以在这些区域内进行路径计算和避障。导航区域可以通过多边形资源定义为单个区域,也可以是多个多边形组成的多个区域,通过共享边缘自动连接成一个导航地图。可为不同区域设置“进入代价”和“路径行进代价”,实现区域偏好或路径优化,如优先走道路而非草地。TileMap 配合 Navigation Layer 可自动生成 NavigationPolygon,方便编辑器操作。
  • NavigationAgent2D:它通常挂载到移动单位身上,负责路径计算和局部避障。使用 target_position 提出路径请求,调用 get_next_path_position() 获取当前要移动到的下一个路径点,并用物理引擎驱动单位移动。提供动态避障功能(依赖 RVO 算法),可在 agent 周围检测其他 agent 或 obstacle,通过 velocity_computed 信号获取“安全速度”,实现单位躲避碰撞。支持路径优化:可设置不同路径算法、平滑选项、简化参数等,提高路径质量。

在 Godot 中,除了手动创建导航区域 NavigationRegion2D以外,还可以通过 TileMap 配置 NavigationLayer,让每个 Tile 自带导航形状,由引擎自动生成导航区域,更适合格子类地图。本章将重点讲解第二种方式的工作流程。它的核心步骤如下:

  1. 在 TileSet 中添加并设置导航图层 NavigationLayer。导航图层可以有多个,每个图层相当于一个逻辑区域,例如:敌人平时巡逻只能走“道路”图层,战斗追击时可进入“草地”图层。
  2. 使用TileSet中的绘图工具为特定 Tile 设置可通行区域的形状(如八角形或圆角矩形,避免碰撞边缘卡顿)。
  3. 当绘制完地图后,Godot 会根据每个 Tile 的导航图层信息自动创建隐藏的 NavigationRegion2D 区域,并注册到 NavigationServer 中。
  4. 在移动单位上挂载 NavigationAgent2D节点,它就可以根据地图上的可通行区域进行路径规划。还可以根据情况指定当前可通行的图层。如巡逻时走道路,追击时走草地。

2 设置导航图层

在本项目中,我们将通过 TileMap 中的 TileSet 配置导航信息,让地图中不同类型的地形具备不同的通行规则。比如,敌方单位在“巡逻”状态下只能走道路,而在“战斗”状态下可以穿过草地。这一切都是通过为 Tile 设置不同的导航图层(Navigation Layer)实现的。

2.1 添加导航图层

导航图层是导航系统识别地图通行区域的基础。我们将在 TileSet 中为地图定义两个导航图层:

  • patrol 图层:用于敌人巡逻路径,对应“道路”区域;
  • wander 图层:用于敌人索敌状态,对应“草地”区域。

操作步骤如下:

  1. 打开 map.tscn 场景,因为子节点使用的TileSet的资源是相同,所以选择任一个子节点即可。
  2. 在右侧检查器面板中找到 Tile Set 属性,点击资源文件map.tres展开编辑界面。
  3. 编辑界面中找到Navigation Layers,点击Add Element,会新建一个导航图层。
  4. 点击 Add Element 两次,分别创建两个导航图层,不同的图层使用不同的数字编码进行区别。

为了区别这种数字编码,可以类似于碰撞层那样,给每个数字编码一个名字,以方便后续引用。打开菜单 Project > Project Settings,找到 Layer Names > 2D Navigation,在 Layer 1 填入 wander,Layer 2 填入 patrol。这样系统中使用的图层编号就有了更清晰的语义。

设置完成后如下图所示。

alt text

至此,我们已经完成了导航图层的创建与命名,接下来就要告诉系统:哪些 Tile 属于哪些图层。

2.2 绘制可通行区域

导航图层创建完成后,我们需要定义哪些网格是wander,哪些网络是patrol,还要为每个Tile 指定可通行区域的形状,这些区域最终会合并生成导航网格,供导航系统使用。

操作步骤如下:

  • 在底部操作区中,点选TileSet标签进入设置,切换到 Paint标签页。
  • 在 Paint Properties 下拉框中,选择 Navigation Layer 0,即我们定义的 wander 导航图层。
  • 使用形状编辑工具来编辑导航形状。工具使用上和编辑物理图层类似,我们可以使用多个点来定义一个几何形状,默认的形状是一个正方形。操作编辑工具,让正方形变成一个八角形,然后再点击左上角的草地图块,这样就可以将这个草地网格设置为可用于wander的导航图层了。设置完成如下图所示。

alt text

  • 同样地,在Paint Properties中切换到 Navigation Layer 1(即 patrol层),为所有的“道路”图块设置八角形的导航区域形状。这样就可以将这些道路网格设置为可用于patrol的导航图层了。设置完成后如下图所示。

alt text

最后注意保存 TileSet 文件,返回场景。

这些导航区域并不会直接显示在编辑器中。但在运行游戏时,Godot 会自动读取这些信息,并构建可供 NavigationAgent2D 使用的导航网格。若想预览导航网格,可以在编辑器中点击上方菜单栏的Debug > Visible Navigation选项,打开Game场景运行游戏,即可在地图上看到两种颜色的导航区域网格:草地区域,对应wander图层;道路区域: 对应patrol图层。此外,所有障碍物区域不会显示导航网格,这意味着这些区域是不可通行的。主场景运行游戏后如下图所示。 alt text

通过以上设置,我们已经成功在地图上建立了多图层的导航通行信息。在接下来的章节中,我们将为敌方单位配置导航代理(NavigationAgent2D),并使用这些图层进行路径规划与行为切换。

3 理解NavigationAgent2D

完成了导航图层的设置后,我们还需要为每个需要移动的敌人单位添加路径规划和避障功能。这就需要用到 NavigationAgent2D 节点。它能够根据目标位置、地图上的导航图层和其他动态单位的位置,为角色规划一条合理的移动路径,并在遇到障碍或其他单位时自动调整速度以避免碰撞。

3.1 路径规划流程

NavigationAgent2D 并不会自己移动单位,它的职责是“规划路径”并输出“移动建议”,开发者需要在脚本中读取这些建议并执行移动。

路径规划的一般流程如下:

  1. 设置目标位置。使用 target_position 属性设置目标点。这一步会告诉导航系统:“我想去那里”,然后由系统开始计算路径;
  2. 读取下一个路径点。路径规划完成后,调用get_next_path_position函数,它会返回单位当前应前往的下一个路径点,这个路径点不是终点,而是路径中的一个节点,你可以据此控制单位朝这个方向移动。
  3. 移动单位。如果单位是CharacterBody2D类型,在更新函数中,可以根据路径规划的结果来修改velocity,再使用 move_and_slide 函数移动单位到下一个路径点。
  4. 判断是否到达终点。使用 is_navigation_finished 函数判断是否到达目标点,如果到达,就可以停止移动了。

3.2 动态避障原理

Godot 中的导航系统将“避障”分为两种层次:静态避障和动态避障。理解这两者的区别,有助于我们正确设置游戏单位的行为。

静态避障通过导航网格本身实现。也就是说,地图上的不可通行区域已经在 NavigationLayer 中被排除,单位在路径规划阶段就会自动绕开这些区域。

在我们游戏中,像石块障碍物所在的 tile 没有被标记为导航区域,因此它们不会出现在导航网格中。这样敌人单位在路径规划时就不会考虑穿越这些区域,路径压根不会经过这里。这就是静态避障,特点是:

  • 在路径规划时就处理,不需要单位在运行时“发现”障碍;
  • 由 TileMap 和 NavigationLayer 决定;
  • 无需脚本干预,只要导航层设置正确即可生效;
  • 适合处理地图地形类障碍物。

与静态避障不同,动态避障用于解决“移动单位之间”的相互干扰问题。例如,当两辆敌方坦克同时在一条道路上前进,它们都规划出同一条路径时,可能会发生“相撞”、“卡住”或“原地打转”等问题。因为这些单位在路径规划时只考虑了静态地图,没有意识到“另一辆车也在这条路上”。

这时就需要 NavigationAgent2D 的动态避障功能,它能在单位之间相互协调,通过速度调整主动避让。Godot 的避障机制基于RVO(Reciprocal Velocity Obstacles)算法,RVO 就像“大家一起让一让”的行走规则。在 Godot 里,每个角色先告诉系统:“我想往这个方向、用这个速度走。”系统再看看附近其他正在移动的角色,预测如果大家都这样走,几秒后会不会撞在一起。如果有可能撞上,系统不会让某一个角色硬拐弯,而是让双方都稍微改一点点速度,最后算出一个“既尽量朝目标走、又不会撞到别人”的安全速度,交还给角色使用。这样就可以实现类似“人群走动避让”的效果。

在Godot中使用动态避障功能的操作流程如下:

  • 启用避障功能。在 NavigationAgent2D 的属性面板中,勾选 Avoidance Enabled,设置合理的半径、邻近距离等。
  • 设置期望速度。在脚本中计算当前路径方向上移动单位的期望速度,并调用set_velocity函数,将速度提交给导航系统,导航系统会计算出安全速度。
  • 接收导航系统调整后的“安全速度”。导航系统会根据周围的动态障碍计算出“安全速度”,移动单位会通过 velocity_computed 信号获取推荐的安全速度:
  • 执行移动。最终使用 move_and_slide() 让单位沿“安全速度”方向平滑移动,避开队友、障碍等。

4 导弹攻击车的导航改造

在本节中,我们将对敌方单位中的导弹攻击车(EnemyMisVehicle)进行导航系统的全面升级。之前的导弹攻击车依靠射线检测进行局部避障,存在路径僵化和卡顿的问题。现在,我们将为它添加 NavigationAgent2D 节点,实现全局路径规划和动态避障,从而让它能够在地图上自主巡逻、搜索并攻击玩家。

4.1 添加 NavigationAgent2D 节点与设置

打开导弹攻击车场景文件enemy_mis_vehicle.tscn,在根节点下增加NavigationAgent2D节点,在节点中进行如下设置:

  • Path PostProcessing:设置为Edge centered,这样路径规划的结果就会更适配网格地图,移动更加平滑。
  • Avoidance Enable:设置为true,启用动态避障功能,确保与其他敌人单位不会互相阻挡。
  • Radius:设置为40。这是移动单位避障范围的半径,值不能过小(容易撞车),也不能过大(路径躲得太远)。可以根据实际情况进行调整。
  • Degub:设置中打开Enabled,启用可视化调试,方便我们运行游戏时观察坦克的路径规划结果。

将原有的 AvoidComponent 节点从节点树中删除,以免和新的导航系统产生冲突。我们现在将完全依赖导航系统进行路径调整和避障。

完成以上操作后,保存场景并准备进入脚本逻辑改造。

4.2 修改脚本逻辑

导航系统提供路径和安全速度,但实际的移动逻辑仍需我们在脚本中实现。接下来,我们将修改导弹攻击车的主控制脚本 enemy_mis_vehicle.gd,使其能够正确使用 NavigationAgent2D 进行路径规划与动态避障。

extends CharacterBody2D

@export var points: Node2D
@export var speed: float = 100
@onready var hurt_box_component: Area2D = $HurtBoxComponent
@onready var health_component: Node = $HealthComponent
@onready var animation_player: AnimationPlayer = $AnimationPlayer
@onready var trail_compoment: Node2D = $TrailCompoment
# 用于访问导航节点
@onready var navigation_agent_2d: NavigationAgent2D = $NavigationAgent2D
var cur_pos: Vector2  #保存坦克的当前位置

func _ready() -> void:
    hurt_box_component.get_damage.connect(health_component.get_damage)
    health_component.died.connect(on_died)
    hurt_box_component.get_damage.connect(on_get_damage)
    set_cur_pos()
    navigation_agent_2d.velocity_computed.connect(on_velocity_computed)

#用于保存坦克的位置
func set_cur_pos():
    cur_pos = global_position

func _physics_process(delta: float) -> void:
    trail_compoment.start()
代码解释:_ready函数基本不变,注意此处需要将velocity_computed信号连接到on_velocity_computed函数。每当导航系统计算出一个安全速度时,这个信号会触发,我们会在回调函数中中接收并执行移动。

func on_died():
    Gamemanager.entity_died.emit(global_position,get_groups())
    queue_free()

func on_get_damage(damage):
    animation_player.play("flash")

func update_direction(target_pos: Vector2):
    var direction = global_position.direction_to(target_pos)
    var angle_rad = direction.angle()
    var delta = get_physics_process_delta_time()
    rotation = rotate_toward(rotation,angle_rad,2*PI*delta )
代码解释:

  • on_died函数负责敌人死亡的响应
  • on_get_damage函数负责敌人受到伤害的响应,
  • update_direction函数负责根据坐标参数来更新导弹攻击车的移动方向。

func set_nav_to_target(target: Vector2):
    navigation_agent_2d.target_position = target

func on_velocity_computed(safe_velocity: Vector2):
    velocity = safe_velocity
代码解释:

  • set_nav_to_target函数负责设置导弹攻击车的导航目标,
  • on_velocity_computed函数负责响应velocity_computed信号,更新导弹攻击车的移动速度为安全速度。

# 负责最重要的导航移动功能
func update_nav():
    if not navigation_agent_2d.is_navigation_finished():
        var next_pos = navigation_agent_2d.get_next_path_position()
        update_direction(next_pos)
        var new_velocity = transform.x * speed
        if navigation_agent_2d.avoidance_enabled:
            navigation_agent_2d.set_velocity(new_velocity)
        else:
            velocity = new_velocity
        move_and_slide()

# 负责停止导弹攻击车的移动
func stop():
    if navigation_agent_2d.avoidance_enabled:
        navigation_agent_2d.set_velocity(Vector2.ZERO)
    else:
        velocity = Vector2.ZERO
代码解释:

  • update_nav首先判断是否到达导航目标点,如果没有到达,就计算下一个导航点,之后更新导弹攻击车的移动方向,由此得到需要的移动速度向量
  • 使用if条件判断,如果需要避让动态障碍,就将移动速度输入给导航系统
  • 最后调用move_and_slide进行移动。

至此,我们已经完成了导弹攻击车的导航系统升级。它现在具备:自主路径规划能力;实时避开其他单位的能力;可通过状态机动态切换导航目标与路径行为。

在后续章节中,我们将进一步将导航逻辑整合进状态机的“巡逻”、“索敌”、“攻击”三个状态,实现更完整的 AI 行为模式。

5 导航系统集成状态机

完成了 NavigationAgent2D 的添加与配置后,我们需要将导航逻辑整合进敌人单位的行为系统中。在本项目中,敌人使用的是基于状态模式的行为系统,包含“巡逻”、“索敌”、“攻击”三种状态。

每种状态都拥有独立的脚本,并在状态切换时调用进入函数enter()和更新函数physics_update()。通过将路径导航逻辑嵌入这些状态,我们可以让敌人单位具备以下能力:

  • 在巡逻状态中:沿着道路图层自动在巡逻点之间移动;
  • 在索敌状态中:在草地图层上进行随机探索;
  • 在攻击状态中:靠近玩家,发射武器,同时调整位置。

5.1 等待导航系统准备

Godot 的 NavigationServer2D 需要在场景开始运行后的一帧中完成地图构建。如果在 _ready() 函数中立即设置导航目标,有时会因为导航图还未准备好而导致路径无法生成。

为避免这个问题,我们可以在状态机初始化时稍作延迟处理。打开state_machine.gd代码文件,修改如下:

func _ready() -> void:
    for child in get_children():
        if child is State:
            states[child.name] = child 
            child.transitioned.connect(on_child_transition)

    if initial_state:
        initial_state.enter()
        current_state = initial_state

    # waiting navigationserver ready
    set_physics_process(false)
    await get_tree().create_timer(0.1).timeout
    set_physics_process(true)
代码解释:在_ready函数中,增加了最后三行代码,先将physics_process设置为false,等0.1秒后再设置为true,目的是等待导航系统初始化完成后再开始处理逻辑,确保路径规划能够正确运行。

5.2 巡逻状态集成导航

在巡逻状态中,敌人单位会在预设的几个巡逻点之间移动。使用导航系统后,巡逻路径不再受限于矩形路径,可以在道路图层中任意布置巡逻点。

打开enemy_patrol.tscn,修改对应的代码如下:

extends State

@export var enemy: Node2D   # 引用敌方单位根节点
@export var nav_agent: NavigationAgent2D   # 用于引用导航节点
@export var search_component :Node2D   # 引用搜索组件
var patrol_points :Array   # 保存巡逻点数组
var patrol_target : Vector2  # 保存巡逻目标

func _ready() -> void:
    get_patrol_point()

func get_patrol_point():
    for point in enemy.points.get_children():
        patrol_points.append(point.global_position)
代码解释:get_patrol_point函数负责获取巡逻点,遍历enemy的points节点下的所有子节点,将子节点的全局位置添加到巡逻点数组中。

func enter():
    get_next_patrol_point()
    set_nav_layer()

func set_nav_layer():
    nav_agent.set_navigation_layer_value(2,true)
    nav_agent.set_navigation_layer_value(1,false)

func get_next_patrol_point():
    patrol_target = patrol_points.pick_random()
代码解释:

  • enter函数是状态进入的调用的函数,它会执行下面两个操作
  • get_next_patrol_point是获取下一个巡逻点
  • set_nav_layer函数负责设置导航系统的导航层,我们之前在地图中设置了两个导航层,编号2是对应巡逻时的道路,编号1是对应追击时的草地。此时,我们处于巡逻状态,所以所以将wander导航层设置为true,其它导航层设置为false。

func physics_update(delta: float) -> void:
    enemy.set_nav_to_target(patrol_target)
    enemy.update_nav()
    if enemy.navigation_agent_2d.is_target_reached():
        enemy.set_cur_pos()
        get_next_patrol_point()
    if search_component.can_see_player():
        transitioned.emit(self,"EnemyAttack")
代码解释:physics_update负责更新状态,它会执行四个操作,更新导航目标,更新导航系统,如果到达导航目标,就保存一下,然后获取下一个巡逻点,如果能看见玩家,就转换到攻击状态。

5.3 索敌状态集成导航

在索敌状态中,敌人会在草地区域内进行随机移动,模拟“搜索”玩家的行为。打开enemy_wander.tscn场景文件,修改代码如下:

extends State

@export var enemy: Node2D
@export  var distance: float = 200
@export  var search_component :Node2D
@onready var timer: Timer = $Timer

var target_pos : Vector2
var noise: float = 50
var end_wander: bool
此处的变量定义部分代码和之前完全一致。

func _ready() -> void:
    timer.timeout.connect(on_time_out)

func enter():
    set_nav_layer()
    end_wander = false
    find_next_point()
    timer.start(5)

func set_nav_layer():
    enemy.navigation_agent_2d.set_navigation_layer_value(1,true)
    enemy.navigation_agent_2d.set_navigation_layer_value(2,false)
代码解释:_ready函数保持不变。enter函数中增加了set_nav_layer函数调用,它的目的是设置导航层,此时,我们处于索敌状态,所以将wander导航层设置为true,其它导航层设置为false。

func physics_update(delta: float) -> void:
    if search_component.can_see_player():
        transitioned.emit(self,"EnemyAttack")
    else:
        if not end_wander:
            enemy.set_nav_to_target(target_pos)
            enemy.update_nav()
            if enemy.navigation_agent_2d.is_target_reached():
                find_next_point()
        else:
            target_pos = enemy.cur_pos
            enemy.set_nav_to_target(target_pos)
            enemy.update_nav()
            if enemy.navigation_agent_2d.is_target_reached():
                transitioned.emit(self,"EnemyPatrol")
代码解释:

  • physics_update负责状态更新,如果发现了玩家,就转换到攻击状态,如果没有发现玩家,而且还没有结束索敌状态,就继续使用导航系统进行移动
  • 移动的目标坐标是使用find_next_point函数获取的一个随机位置。
  • 如果结束了索敌状态,就将移动目标设置为之前保存的位置,然后使用导航系统进行移动,
  • 如果到达了移动目标,就转换到巡逻状态。

func find_next_point():
    var random_x = randf_range(-noise, noise)
    var random_y = randf_range(-noise, noise)
    var random_vec = Vector2(random_x,random_y)
    var pos = distance * enemy.transform.x + random_vec
    target_pos = enemy.global_position + pos

func on_time_out():
    end_wander = true   
代码解释:find_next_point函数是用于获取下一个随机位置,on_time_out函数是超时触发的函数,它的作用是设置end_wandertrue,这样就可以退出索敌状态了。

5.4 攻击状态集成导航

在攻击状态中,敌人会靠近玩家,若距离合适则停止移动并发射武器。打开enemy_attack.tscn,修改代码如下:

extends State

@export var enemy: Node2D
@export  var search_component :Node2D
@export  var weapon_component  :Node2D

var target_pos : Vector2    # 保存目标位置
var min_dist: = 150   # 用于保存敌方单位和玩家的最小距离


func enter():
    set_nav_layer()

func set_nav_layer():
    enemy.navigation_agent_2d.set_navigation_layer_value(1,true)
    enemy.navigation_agent_2d.set_navigation_layer_value(2,false)
代码解释:

  • enter函数是状态进入的函数,在此处设置导航层
  • 这里和索敌状态一样,将草地对应的导航层的值是设置为true。

# 负责状态更新
func physics_update(delta: float) -> void:
    if search_component.can_see_player():
        target_pos = search_component.player_ref.global_position
        if enemy.global_position.distance_to(target_pos)>min_dist:
            enemy.set_nav_to_target(target_pos)
            enemy.update_nav()
        else:
            enemy.stop()
        weapon_component.target(target_pos)
        weapon_component.shoot(target_pos)
    else:
        transitioned.emit(self,"EnemyWander")
代码解释:

  • 首先判断是否探测到玩家
  • 再判断与其距离是否大于最小距离,如果还比较远,则使用导航系统进行移动
  • 如果到达了最小距离,则停止移动,然后设置武器组件的目标为玩家位置,然后发射子弹。
  • 如果没有看到玩家,则转换到索敌状态。

完成以上状态代码修改后,记得要在enemy_mis_vehicle场景中确认已经设置好对应的节点属性,才能让节点引用变量正常工作。

5.5 测试导弹攻击车的导航系统

完成了导航组件的添加与状态机的行为整合之后,我们就可以在游戏主场景中测试导弹攻击车的导航效果了。

回到主场景,打开game.tscn文件。首先修改points节点下的四个marker2D节点,这些节点定义了敌方单位的巡逻点,使用移动工具,将它们移动到地图中道路的交叉点上。因为导航系统可以自动规划出两个点之间的移动路径,所以这些巡逻点可以放在道路的任一个地方。大大增强了灵活性。

在Enemies节点下,放置几辆敌方的导弹攻击车,用移动工具将其放到道路上合适的位置。需要手动设置好每个单位的points属性,定义为game节点树中的points节点。完成设置后场景如下: alt text

运行游戏后,你将看到如下效果:

  • 草地和道路上分别显示不同颜色的八角形蒙版,表示可通行区域;
  • 敌方单位会自动沿着路径点巡逻,并根据状态切换行为;
  • 当多个单位靠近时,彼此会主动避让,路径会发生轻微调整,有时会出现倒车和侧移等不自然的情况。
  • 一旦玩家靠近,导弹攻击车会追击并发射武器。

因为我们打开了导航系统的debug功能,你可以在观察到敌方单位的规划移动路径。 alt text

本节我们将状态机与导航系统的融合,让敌人单位真正具备了“智能移动+情境行为”能力,是构建高级游戏AI的重要步骤。

6 敌方坦克导航改造

在上一节中,我们已经成功地为导弹攻击车添加了路径导航和动态避障功能,使其具备了全地图自主移动与智能响应的能力。接下来,我们将对敌方坦克(EnemyTank)进行同样的改造。

由于敌方坦克的基本行为与导弹攻击车类似,也采用了状态机控制,因此我们可以复用大量已有的导航逻辑,大幅提高开发效率。

6.1 替换组件与脚本

打开敌方坦克场景 enemy_tank.tscn,在根节点下添加一个 NavigationAgent2D 节点。设置该节点的属性与导弹攻击车相同。你也可以直接从导弹攻击车的节点树中将这个节点拷贝过来,这样不用再次进行节点的设置。如果敌方坦克中还包含旧的 AvoidComponent 或自定义障碍检测脚本,务必将其删除,以免逻辑冲突。

然后将导弹攻击车的代码也复制到enemy_tank.gd中,尽管坦克使用的武器组件与感知组件和导弹攻击车不同,但由于我们在项目中统一设计了这些组件的接口函数。你无需修改任何函数调用,只要节点指向正确的组件即可正常运行。

6.2 修改状态机

敌方坦克的状态机通常也包括巡逻、索敌和攻击状态,结构与导弹攻击车类似。注意需要设置好状态机有关的四个节点上的变量,确保让这些变量指向正确的节点。

在状态机节点代码中,我们使用 @export 关键字来暴露变量,使其在编辑器中可以直接关联对应的组件节点:

只要你在场景中正确绑定每个状态子节点的变量,这些脚本就无需更改,可直接工作。变量设置如下:

  • enemy → 指向根节点(敌人本体);
  • nav_agent → 指向新添加的 NavigationAgent2D;
  • search_component → 指向坦克所用的探测节点;
  • weapon_component → 指向坦克的炮弹发射组件;

这种基于接口一致性的组件设计,有两个重要优势:

  • 提升可复用性:状态逻辑与导航控制代码不依赖组件具体类型,只要接口一致,就能兼容不同敌人;
  • 增强模块解耦性:每个组件可独立开发与替换,不影响其他系统逻辑。

这是构建可扩展游戏 AI 架构的关键设计理念。在后续开发更多敌人类型时,也可以延续这种方式,大幅简化逻辑结构与维护成本。

通过本节的改造,我们成功地将导航系统应用到了另一个敌人单位上,验证了模块化逻辑的可扩展性。后续如需添加更多敌人类型,只需继承相同的导航与状态框架,即可快速实现具有自主行为的 AI 单位,极大提升开发效率与系统稳定性。

7 测试与观察导航行为

导航系统的添加和状态逻辑的整合只是第一步。要确保路径规划、状态切换、动态避障等功能如预期运作,我们还需要在主场景中搭建测试环境,并借助 Godot 提供的可视化工具,对导航过程进行观察与分析。

7.1 主场景中布置测试环境

第一步:添加多个敌人单位

打开主场景game.tscn,在 Enemies节点下:

  • 拖入两个导弹攻击车实例;
  • 拖入两个敌方坦克实例;
  • 使用移动工具将它们摆放在地图中合理位置,推荐起始点位于道路上,确保能连通导航网格。

第二步:设置巡逻路径引用

所有敌人单位都需要访问场景中的 points 节点来获取巡逻目标点。请逐个选中敌人节点,在检查器面板中将它们的 points 属性设置为主场景中points节点。

第三步:运行游戏

点击运行,观察以下行为是否正常:

  • 敌人单位按设定路径巡逻;
  • 玩家进入探测范围后,敌人进入攻击状态并移动到玩家附近;
  • 多个敌人单位不会互相阻挡或卡死;
  • 各状态之间切换流畅,路径规划合理。

主场景设置完成后,运行游戏后的效果如下: alt text

7.2 路径规划调试分析

Godot 提供了强大的导航调试功能,可以帮助我们分析敌人的路径决策过程、目标位置、避障半径等信息。

开启调试工具

运行游戏前,依次点击:菜单栏Debug > Visible Navigation,这个选项会在主场景中显示导航网格的可视化信息。

在每个移动单位的NavigationAgent2D节点上,在右侧属性中,点击Debug > Enable选项,可以启用导航调试工具。会在主场景运行游戏后显示路径规划的可视化信息,包括路径点、目标点等信息。

你可以通过这些信息来判断地图中的导航区域是否定义正确,以及调试敌人不同状态下的行为逻辑,了解路径规划的决策过程。

由于路径规划涉及多个系统协同运作,在开发初期可能会遇到一些问题。以下是常见的调试技巧和排查建议。

问题 可能原因 解决建议
敌人原地不动 没有设置 target_position 或路径未规划成功 检查状态逻辑是否调用了 set_nav_to_target();检查是否启用了导航层
路径点错乱 调用顺序不正确 确保在设置目标前,导航系统已初始化完成(使用 await 延迟)
敌人之间重叠 动态避障未开启或半径过小 检查 Avoidance Enabled 是否勾选,并适当增大 Radius
可通行区域缺失 TileSet 没有绑定正确的 Navigation Layer 返回 TileSet,确认所有草地/道路图块是否正确绘制导航形状

本章小结

在本章中,我们完成了敌人单位导航系统的全面升级,从最初基于射线的局部避障,转向基于地图结构的全局路径规划与动态避障系统。通过合理利用 Godot 的 NavigationAgent2D 组件和 NavigationLayer 设置,我们赋予了敌人单位真正意义上的“地图理解能力”与“行为适应能力”。

本章的核心收获包括:

  • 掌握了两种导航方式的区别:局部避障虽然简单,但容易卡死;全局导航具备完整的路径规划能力,适用于复杂地图;
  • 学习了通过 TileMap 配置 NavigationLayer 的工作流程,实现地图级别的通行区域定义,并支持多图层切换(如 patrol / wander);
  • 理解并使用了 NavigationAgent2D 的路径与避障系统,包括路径点获取、目标设置、速度控制与安全速度计算;
  • 将导航逻辑整合进状态机行为,使敌人单位在巡逻、索敌、攻击等不同状态下能自动切换目标、控制移动;
  • 通过调试工具观察并验证导航行为是否符合预期,并掌握了排查常见问题的方法;

通过这些内容,我们将进一步拓展 AI 敌人的行为复杂度,让他们不仅能“知道怎么走”,还能“知道该往哪里走”,实现更丰富的战术行为与游戏深度。