我終於想起還有“這回事”了,然後其實這些東西很久以前就寫了七七八八。但是這個主題單純寫怎麼用的話,要寫的不太多,但是詳細寫一些東西半天又寫不完。所以這篇內容不是特別多。但是想了想還是發出來。
另外在上一篇文章到這篇文章發佈期間,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來檢查兩個位置是否“大致相等”(浮點數精度問題,你懂的)以避免此問題。