Hi, I am currently trying to make a very basic enemy AI starting with it picking a random point inside of NavigationRegion2D that I gave it but I am heavily struggling with it.
I was wating fot Godot 4.3 because it should have new methods specifically made for this usage but I am not sure if I am even using them correctly.
My setup: I am trying to create a basic class that other enemy types will inherit from, so I have everything at one place.
The enemy scene looks like this:
Sprite2D
CollisionShape2D
NavigationAgent2D
TexturedProgressBar
NavigationRegion2D that is assigned to this enemy is in root place of the whole world scene.
and the code in question looks like this:
(I left there my comments from previous attempts)
extends CharacterBody2D
class_name Enemy
@export_category("Stats")
@export var health := 100 ## base enemy HP
@export var speed := 200.0 ## base enemy speed
@export var strength := 10 ## how much base damage enemy deals to player
@export var auto_agro : bool ## check if enemy should automatically attack player when they get near
@export var walk := true ## check if enemy should automatically move on its own
@export_category("Nodes")
@export var region : NavigationRegion2D ## reference to NavigationRegion2D enemy should walk by default
@export var agent : NavigationAgent2D ## reference to NavigationAgent2D
@export_category("UI")
@export var hp_bar : TextureProgressBar ## reference to TextureProgressBar for visual HP display
const MIN_WAIT := 1.0 ## minimum wait time for movement delay
const MAX_WAIT := 3.0 ## maximum wait time for movement delay
var agro := false
var moving := false
func _ready() -> void:
hp_bar.max_value = health
hp_bar.set_value_no_signal(health)
agent.set_navigation_map(region.get_rid()) #makes sure that the correct navigation region is set to navigation agent
agent.target_reached.connect(_on_target_reached)
if agent.target_reached.is_connected(_on_target_reached):
print("Target reached has been connected") # connection should work, did not tested yet
# if enemy should be agresive towards player, make it
if auto_agro:
agro = true
# if enemy can walk, start moving
if walk:
get_move_location()
func _physics_process(delta: float) -> void:
print("Current location: " + str(global_position))
print("Target location: " + str(agent.target_position))
#if not agro: # check if player attacked the enemy, I am not handling this one yet so it's always false
#if not moving: # moving is a bool variable to check if the enemy is currently moving or waiting between moves
#moving = true
#get_move_location()
#
#if global_position == agent.target_position:
#moving = false
#await get_tree().create_timer(randf_range(MIN_WAIT, MAX_WAIT))
#get_move_location()
#
move_and_slide()
func get_move_location() -> void:
moving = true
agent.target_position = NavigationServer2D.region_get_random_point(agent.get_navigation_map(), 1, false)
velocity = agent.target_position * speed
func _on_target_reached():
print("Target reached")
moving = false
await get_tree().create_timer(randf_range(MIN_WAIT, MAX_WAIT)).timeout
get_move_location()
What it should be doing is:
Enemy load in and decide if it can move
If yes, pick a random point in NavigationRegion2D and start moving to it
When it reach the point, wait random time between 1 sec to 3 sec
Pick a new point and repeat
What it is doing:
Enemy load in and decide if it can move
It does not pick anything and “Target location” print stays as (0, 0)
You are assigning an invalid RID to the agent. A region RID is not a navigation map RID.
An agent and region will both join the default navigation map of the World2D/3D of the Viewport automatically. So you dont need to assign a map if you dont plan to use a completely different one.
Instead of getting the navigation map RID , you need to get the region RID
I’ve bolded what you should change. NavigationServer2D.region_get_random_point(**agent.get_navigation_map()**, 1, false)
That is getting the Navigation Map, but you want to get the Navigation Region.
I did a quick test and this should work. I’m assuming you only have 1 Nav region. I’ve never used more than one at least, but you’d replace the index with whatever you want if it’s not 0.
var mapRID : RID = navAgent.get_navigation_map()
var regionRID : RID = NavigationServer3D.map_get_regions(mapRID)[0]
The user is using the right function already and does not need to change that line. The NavigationServer2D.region_get_random_point() is a dedicated function to query just a region instead of the entire map.
All the user needs to change is to not confuse region RID with map RID. Also not override the agent default map with an invalid / empty map RID, because that makes the other agent queries fail with the empty map.
The line
agent.set_navigation_map(region.get_rid()) #makes sure that the correct navigation region is set to navigation agent
needs to be removed, the agent uses the default map by default already. If a map change would be intentional at least this would need to be changed to an actual navigation map RID instead of a region RID.
It also needs to be waited for the NavigationServer sync that happens on the next physics process. Calling inside a ready() function on the first frame will query an empty map that returns an empty path or just a position at the origin.
I think I updated everything how you described it, but I am still not getting any random point from the NavigationServer2D.
For reference, in the code above I have Print() functions that are printing current position of the TestEnemy and the desired target location. The output in console looks like this:
The Current position print works, when I manually move the enemy, it is changing accordingly.
It looks like there is still something wrong with this line I would guess
I way playing a bit with different setups of the code. Like doing everything in _physics_process() but in the end reverted everything to the original state with the suggested modifications.
Out of curiosity, I tried to set the agent.target_position to a specific point manually and when I run the project, I got just a never ending slide into the direction of the point.
This should give you a random position everytime you call it and that is really all that there is to it.
If in your main scene you just get a zero vector the reason is that the navigation map is either empty, not synchronised, or the used RID is wrong/invalid.
The obvious issues that may cause this I already mentioned but there might be more going on with your custom setup and you need to narrow that down by removing stuff.
Ok, thank you. This was indeed correct, there was just another problem of me also applying velocity not correctly.
I am leaving this mess of a test code as a reference if someone will also struggle with this. I know it is not how a correct scene should look like but as a test code and reference it should work.
extends Node2D
var destination : Vector2
var moving := false
var speed := 20000.0
func _physics_process(delta: float) -> void:
if not moving: # if enemy is not moving, pick a new destination
destination = (NavigationServer2D.region_get_random_point($NavigationRegion2D.get_rid(), 1, false)) # get a random point from NavigationRegion2D
moving = true
%NavigationAgent2D.target_position = destination # sets the destination as a target position for NavigationAgent2D
if moving: # if enemy has target position set and is ready to start moving
var direction = (%TestEnemy.global_position - destination).normalized() # calculates the direction vector towards the destination
%TestEnemy.velocity = -direction * speed * delta # sets the velocity
%TestEnemy.move_and_slide()
if %NavigationAgent2D.distance_to_target() < 50.0: # checks if NavigationAgent2D distance towards the target position is less then 50 pixels
moving = false # sets the moving variable to false, so it can pick a new destination