我终于想起还有“这回事”了,然后其实这些东西很久以前就写了七七八八。但是这个主题单纯写怎么用的话,要写的不太多,但是详细写一些东西半天又写不完。所以这篇内容不是特别多。但是想了想还是发出来。
另外在上一篇文章到这篇文章发布期间,Godot也更新了一个小版本和一个维护性更新。感兴趣的朋友可以去了解一下。一些新功能确实好用。
之前我们已经为敌人加入了简单的追踪玩家的功能。但是这个功能目前非常简陋。
当然你永远都可以编写更复杂的代码来处理这些问题,但是我们用游戏引擎的目的就是不要去重新发明这些轮子(只要现在的轮子够用的话)。Godot同样内置了让虚拟物件自动前往目的地的各种基础设施。
导航区域和导航网格
复杂的场景对于处理这个问题来说太复杂了,很多东西纯粹就是噪音。导航系统首先要求你构建一个NavigationRegion3D来表示可以进行寻路的区域。
为了便于学习和实验,建议构造一个较为复杂的场景,毕竟只有一个简单的平面的话,那也没必要用这个功能了。
首先需要在你的场景中加入一个NavigationRegion3D节点。此时有警告提示此节点需要一个NavigationMesh资源。但是,不要被这个资源的名字迷惑到,因为这个资源本身并不是某种具体的网格,它主要是起配置作用。新建此资源即可。
接下来,你需要把你的具体的场景作为这个region的子节点。NavigationRegion3D会根据子节点的状况(以及
NavigationMesh的配置)来构造导航用的数据。
配置好后,选中你的NavigationRegion3D节点。此时在3D视口中会出现两个按钮,点击Bake Navigation Data即可烘焙导航数据(“烘焙”的意思其实就是事先计算好)。另一个按钮是清除数据。现在你可以在视口中看到计算出来的导航区域。这些亮起来的区域就表示导航代理可以去到的地方:

需要知道的类型
Godot导航系统的API其实并不是那么简单,要实现简单的自动寻路也得用到好几个部分。我们首先简单了解一下要用到的一些类型。下述类型都有2D和3D版本,故省略类名最后的3D和2D。
- NavigationServer:和PhysicsServer类似,它是真正负责计算导航相关数据的类。要和它交互,我们可以发起请求,也可以通过其他类来操作。实际上你完全可以通过它的API来实现寻路。
- NavigationMap:抽象的地图,一个地图可以有很多NavigationRegion。
- NavigationRegion:就是上面用到的NavigationRegion节点。它拥有一个NavigationMesh资源,定义一个可以导航的区域,并通过各种参数对其进行配置。
- NavigationAgent:即将在下面介绍,它是一种便于实现可在地图中寻路的的对象的辅助节点。正如前面所说,你其实也可以不用它而是直接向server发起请求。
导航代理
光有导航网格实际上还没法让一个节点自动寻路——其实说实话,Godot的内置功能(意思是不自己写脚本的话)其实根本没法实现“给它一个点,它自己走过去”。我们需要给受导航系统控制的节点加上一个导航代理(NavigationAgent)。这是一个抽象的节点,表示参与导航相关运算的可以移动的东西。
这里我们给之前的敌人加上一个导航代理节点,然后快速地测试一下这一系列系统是如何运作的。注意这里你可能需要把之前跟踪玩家的相关代码先注释掉。
导航代理首先要了解的属性是target_position,顾名思义就是设置代理的目标位置。设置好此属性之后,就可以通过代理向导航系统(NavigationServer3D)请求一条到目标点的路径。
要获得到目标点的路径上的下一个点,需要调用代理的get_next_path_position方法。但是在调用此方法之前,首先要保证导航系统的地图已经完成处理。如果你一上来就设置目标然后调用此方法,极大概率会报错。
参考错误提示和文档,要确保在地图准备好后再请求路径,有两种方法。其一是连接map_changed信号,其二是通过map_get_iteration_id
来确定地图数据是否完成同步。当然,你也可以尽可能地延迟get_next_path_position等方法的调用来避免这个问题。
导航系统的很多操作是和物理帧同步的,因此相关方法也应当在_physics_process中调用。首先要写上:
if not NavigationServer3D.map_get_iteration_id(navigation_agent.get_navigation_map()): return # 或根据实际情况处理
导航代理的get_navigation_map方法返回其所在的地图(的ID),然后通过map_get_iteration_id获得其迭代ID。具体的我们这里不用管,只用知道如果返回0就代表此地图还没有完成同步,我们无法正常调用相关方法。
Godot的文档里一开始用的是call_deferred来延迟进行目标点的相关设置,故这里介绍另一种方法。
接下来,使用导航代理的is_navigation_finished确认当前到target_position的导航工作是否完成。如果没有完成,我们就调用get_next_path_position获得下一路径点的位置以设置速度:
if not navigation_agent.is_navigation_finished(): var next_position = navigation_agent.get_next_path_position() var new_velocity = global_position.direction_to(next_position) * SPEED new_velocity.y = velocity.y velocity = new_velocity else: velocity.x = 0 velocity.z = 0 if not is_on_floor(): velocity += get_gravity() * delta
通过get_next_path_position获得当前路径的下一个点之后,我们用Vector3的direction_to方法求得一个指向next_position的方向向量。正如我们之前了解到甚至已经写过的,此方法在这里等价于(next_position - global_position).normalized()。考虑到我们还有处理重力加速度的代码,所以这里没有直接设置velocity,而是在保留竖直方向上的速度分量的同时设置水平方向上的分量。如果导航已经完成,我们设置水平方向上的速度分量让它停下来。当然,你应该这里加上自己需要的逻辑来处理。

现在你的敌人应当可以移动到指定的位置然后停下来。
转向
在移动过程中,我们只是设置了速度,它并不会自动转向目标位置。当然,借用我们之前学习过的look_at方法可以很快地修正这一点:
var look_at_position = next_position look_at_position.y = position.y if not look_at_position.is_equal_approx(position): look_at(look_at_position, Vector3.UP, true)
这里补充上了之前让敌人看向玩家时忽略了的一个问题。之所以把look_at_position的y设为和敌人自身同样的值是为了避免让它绕水平方向旋转(抬头低头)。不信你可以把look_at_position换成next_position。
look_at函数有一个坑是如果目标位置和当前位置相同,它会报错。所以这里用is_equal_approx来检查两个位置是否“大致相等”(浮点数精度问题,你懂的)以避免此问题。