🎮

Godot4 Navigationを使った経路探索

2023/10/07に公開

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への経路も有効になります

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)

障害物やリンク等を適当に配置して完成
https://youtu.be/JWDXmHnk8UU

Discussion