我终于想起还有“这回事”了,然后其实这些东西很久以前就写了七七八八。但是这个主题单纯写怎么用的话,要写的不太多,但是详细写一些东西半天又写不完。所以这篇内容不是特别多。但是想了想还是发出来。
另外在上一篇文章到这篇文章发布期间,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 来检查两个位置是否“大致相等”(浮点数精度问题,你懂的)以避免此问题。
评论区
共 条评论热门最新