Godot4 Navigationを使った経路探索
Navigation機能の準備
NPCを追加してプレイヤーを追いかけるようにします
経路探索が必要になるのでNavigation機能を使ってマップを作成します
Regionの作成
まずはノードを作成します
NavigationRegion3Dを作成し、床や障害物をその配下に置きます
NavigationLink3Dで段差の上と下をつなぎます
NavigationRegion3DはCellsとAgentsの項目を設定すればよいです
Cellsの項目はNaviMeshの計算を行うときの基準となる大きさです
AgentsはNPCの大きさです
Max Clibは乗り越えられる段差の高さ
Max Slopeは登れる坂の最大の角度です
ジャンプや降下が必要な場所にはNavigationLink3Dを配置します
Start Position と End Positionでつなぐ2点を設定します
Costを増やすとこの経路を取りづらくなります
BidirectionalをオンにするとEndからStartへの経路も有効になります
NavigationMeshのベイク
NavigationRegion3Dを選択すると画面上部に焼き込むボタンが出てくるのでそれを押します
成功すると半透明のNaviMeshが表示されます
薄いピンクの円と線で表示されているのがNavigationLink3Dです
床や障害物の配置を変えるたびにベイクをする必要があります
ゲーム中にベイクすることもできますが重い処理ですので複雑な地形ではやらない方が良いです
敵の作成
CharacterBody3Dをもとにenemyシーンを作成します
「シーン>新規シーン」から「その他のノード」でCharacterBody3Dを選びます
ノードの構成は次の通り
zombie以下はmixamoでダウンロードしたアニメ付きモデルです
歩き/走り/待機/ジャンプ/噛みつきのアニメをダウンロードしました
CollisionはNaviMeshのAgentの大きさを超えないようにします
Agentの設定
NavigationAgent3Dの設定は次の通り
Desired Distanceは目標に到着したとみなす距離です
Pathは経路の中継点Targetは最終地点での設定です
Navi Meshは床から浮いた場所に生成されるのでAgentも浮いた状態になります
それを補正するのがPath Height Offsetになります
Cells Heightと同じか少し大きい値にするといいと思います
Avoidanceをオンにすると複数のAgentがある場合に他のAgentを考慮して経路を探索します
そこそこ重い処理のようなので限定して使うのが良いようです
デバッグ用にDebug Enabledをオンにしておきます
AnimationTreeの設定は以下の通りです
今回はAnimationTreeBlendNodeを使っています
Transitionの項目はインスペクターから増やせます
スクリプトからNaviMeshを使う
enemyシーンのルートノードにスクリプトをアタッチします
enemy.gd
extends CharacterBody3D
@export var JUMP_VELOCITY = 5
@onready var navigation_agent_3d = $NavigationAgent3D
@onready var animation_tree = $AnimationTree
var gravity = ProjectSettings.get_setting("physics/3d/default_gravity")
var player:Node3D
var current_speed = 4
@export var is_attack = false
func _ready():
player = get_tree().get_nodes_in_group("player")[0]
func _physics_process(delta):
navigation_agent_3d.target_position = player.global_position
if not is_on_floor():
var new_velocity = velocity - Vector3(0,gravity * delta,0)
navigation_agent_3d.velocity = new_velocity
else:
var new_velocity = (navigation_agent_3d.get_next_path_position() - global_position).normalized()*current_speed
navigation_agent_3d.velocity = new_velocity
var direction = Vector3(velocity.x,0,velocity.z)
var speed = direction.length()
var loco = "idle"
if speed>0.1: loco = "walk"
var jump = "landed" if is_on_floor() else "falling"
var attack = "attack" if is_attack else "normal"
animation_tree.set("parameters/loco/transition_request",loco)
animation_tree.set("parameters/jump/transition_request",jump)
animation_tree.set("parameters/attack/transition_request",attack)
func _on_navigation_agent_3d_velocity_computed(safe_velocity):
velocity = velocity.move_toward(safe_velocity,0.3)
var direction = navigation_agent_3d.get_next_path_position() - global_position
direction.y=0
direction = direction.normalized()
if direction.length()>0:
look_at(global_position+direction)
move_and_slide()
func _on_navigation_agent_3d_link_reached(details):
var entry = details["link_entry_position"]
var exit = details["link_exit_position"]
if entry.y < exit.y+0.1 and is_on_floor() and absf(entry.y-global_position.y)<0.1:
velocity = exit - entry
velocity.y=0
velocity = velocity.normalized()*current_speed
velocity.y = JUMP_VELOCITY
func _on_navigation_agent_3d_target_reached():
if is_on_floor() and !is_attack:
is_attack=true
Navigationに関係する部分を解説します
target_positionに目標地点を設定します
get_next_path_position()で次に行くべき座標が取れるので
接地中はその座標に向かうように速度を計算します
落下中は重力の処理のみ行います
Avoidanceをオンにしている場合は
navigation_agent_3d.velocityに計算した速度を設定します
func _physics_process(delta):
navigation_agent_3d.target_position = player.global_position
if not is_on_floor():
var new_velocity = velocity - Vector3(0,gravity * delta,0)
navigation_agent_3d.velocity = new_velocity
else:
var new_velocity = (navigation_agent_3d.get_next_path_position() - global_position).normalized()*current_speed
navigation_agent_3d.velocity = new_velocity
Avoidanceをオンにしてvelocityを設定すると
その他のAgentを考慮した速度がvelocity_computedシグナルで通知されるので
それをもとに移動処理を行います
func _on_navigation_agent_3d_velocity_computed(safe_velocity):
velocity = velocity.move_toward(safe_velocity,0.3)
var direction = navigation_agent_3d.get_next_path_position() - global_position
direction.y=0
direction = direction.normalized()
if direction.length()>0:
look_at(global_position+direction)
move_and_slide()
シグナルはインスペクタの横のノードのタブから設定します
右クリック>接続からスクリプトを選択すると関数が生成されます
Navigationのリンクについては降下など自然に移動できるものについては移動できますが
ジャンプなどが必要なところは自分で処理を行う必要があります
リンクの端点に到着時にlink_reachedシグナルが来るのでそこでジャンプの処理を書きます
リンクの移動先が上部にあって接地していている場合にジャンプするようにします
中々うまくいかなかったので色々条件を試行錯誤してます
マップの形によっても変わってくると思います
func _on_navigation_agent_3d_link_reached(details):
var entry = details["link_entry_position"]
var exit = details["link_exit_position"]
if entry.y < exit.y+0.1 and is_on_floor() and absf(entry.y-global_position.y)<0.1:
velocity = exit - entry
velocity.y=0
velocity = velocity.normalized()*current_speed
velocity.y = JUMP_VELOCITY
目的地についた時のnavigation_finishedシグナルの設定です
接地中かつ攻撃中でない場合に攻撃に移行します
func _on_navigation_agent_3d_target_reached():
if is_on_floor() and !is_attack:
is_attack=true
アニメーションの設定はTransitionの項目で設定したものをリクエストするようにします
アニメーション終わりにis_attackにfalseを設定するように噛みつきのアニメーションにトラックを追加してます
var loco = "idle"
if speed>0.1: loco = "walk"
var jump = "landed" if is_on_floor() else "falling"
var attack = "attack" if is_attack else "normal"
animation_tree.set("parameters/loco/transition_request",loco)
animation_tree.set("parameters/jump/transition_request",jump)
animation_tree.set("parameters/attack/transition_request",attack)
障害物やリンク等を適当に配置して完成
Discussion