Enemy movement based on calculating player distance

Godot Version

Godot v4.2


Hi everyone,

I’m super new to Godot and programming and am trying to make my first game. I’m making a top down shooter and want to program the enemies to move towards the player until a specified distance and then stop and start shooting the player.

The way I thought to do this was to create a variable which is equal to the distance between the enemy and the player. Then in the movement part of the enemy’s script make an ‘if’ statement that when the player enters its detection area (area2d node) it will move towards the player ‘until’ the player_distance variable is less than or equal to 20 (or some number) then the enemy will stop moving and change an ‘player_in_range’ variable to true which will then trigger the shooting function.

I’m getting stuck at the very first step of calculating the player_distance variable.

I tried using the code in this post: Enemy State Machine: calculating distance between player-enemy [2D Platformer]

Which is:
@onready var player = preload(“res://scenes/player.tscn”)
var player_position = player.global_position
var player_distance = (global_position - player_position).length()

But I just get the error: Invalid get index ‘global_position’ (on base ‘Nil’).

I’ve been trying to work what this means and how to fix it for like 2 hours now, please help

Where did you write the this code ? Inside _ready() ? Need more context.

I put them above the _ready() function with the other variables I define at the beginning (like speed)

I’m also new to Godot but have experience with other languages and frameworks.

The error you saw is probably because player is null. So when you do player.global_position, it fails and gives an error to tell you that player is null, or ‘Nil’. So you need a way to get a reference to your player.
edit: I’m not sure why player is null though. I would expect it to be a reference to your player.tscn scene. I still don’t think this would work though, as you would want a reference to a specific instance of that scene in your scene tree. If that came from the tutorial then perhaps I don’t understand something.

I think the way I will be handling this will be by tracking player objects in either a group or autoload singleton. You can read about these and learn how to use them here:

If you want, you can just read the singleton page, add a variable to your singleton to track the player, and assign to it in your player’s _init method. Then you can reference it anywhere with WhateverYouCallYourAutoloadSingleton.whatever_you_call_your_player_reference. Sometimes the simplest solution is best! If you think you are likely to have more than one player at some point though (perhaps for local multiplayer), I’d do it slightly differently.

For the group, I would simply create a group called something like “active_player_objects”, and then add that group the player Node’s Groups property, as shown in the Groups documentation above. If you need to add or remove them from a group at a time other than when their Nodes are created or destroyed, you can use Node.add_to_group and Node.remove_from_group methods (also in the documentation) instead.

For the Singleton, I would store an array of active player objects (typed to match your player class, or its base if you have multiple possible player classes inheriting from a shared base) inside it. In the player class (or its base) I would array.append a reference to the player class to the array in the player whenever it is added the world (for example in its _init method) and remove it whenever it is removed from the world (for example in its _destroy method).

Groups seem to be built for exactly this kind of referencing problem and can be assigned to right in the editor, so I will probably use them. The singleton would work as well if you prefer it. If groups have poor type hinting (I have not yet tried them!) then I will use singletons instead.

For your enemy, I would add an export variable called something like “target”:
@export var target: Node; This way, you can have the enemy follow anything, not just a player. And you can assign it from the editor if you want to set an initial behaviour. It could be handy if, for example, your enemies can accidentally hit each other and you want this to make them fight each other.

Wherever I wanted to find a target, I would iterate over the array to find the nearest player. Something like (I don’t have the editor open, but I think this is right):

# use whichever of these 2 lines is appropriate
var active_players = get_tree().get_nodes_in_group("whatever_i_called_my_group");
var active_players = WhateverICalledMyAutoload.whatever_i_called_my_player_array;
var nearest_player: MyPlayerClass = null;
var nearest_player_distance: float = null;
for player in active_players:
	var distance: float = global_position - player.global_position;
	if (nearest_player_distance == null or nearest_player_distance > distance):
		nearest_player = player;
		nearest_player_distance = distance;

Then I would check if the nearest player is too far away, or null. If they are, I’d set the target to null and switch to a state that isn’t following the target. Otherwise, I’d set the target to that player and switch to a state that follows the target.

If you do all this, your enemy is ready for the inclusion of multiple players.

Some other notes:

  • I wouldn’t call this every _physics_process because it could be computationally expensive. I’d call it, for example, only once every 15 _physics_process calls and only while in a searching state. Exactly what you want to do depends on the behaviour you want for your enemy.

  • When checking for closest or greater than or less than a set distance it’s quicker to use length_squared than length. This is because these functions use Pythagoras’ theorem to get the length of the hypotenuse. Getting this length involves a square root right at the end, which is computationally expensive. The length_squared method doesn’t do this, you just have to compare it to square of the distance you care about. Eg.
    if (my_vector2.length_squared() < distance_to_check ** 2):
    is the same as, and faster than:
    if (me_vector2.length() < distance_to_check):

  • I would be sure to add a null check in my follow behaviour before trying to follow, otherwise you may get a ‘Nil’ error there as well (for example when trying to follow a player that has been killed and removed from the map). You could have your enemy do something else if the null check fails, such as return to a wandering state.

  • I would use a signal to indicate when the player has died. I would have my follower connect to that signal so that it can stop following when this happens. Otherwise, when a player dies, the enemy might start running off to the players new spawn point half way across the map!

  • I would probably make a Node or class specifically for the follow behaviour, so that lots of enemies can re-use it. Or possibly put it on a base class common to all enemies. How you choose to do that is up to you, I’m not yet sure how I’m going to architecture for this myself.

  • Wherever possible, I split my code up into small functions. So rather than having a function which:

    • Gets the array of players as described
    • Iterates over them to find the nearest
    • Assigns that result to target and switches state

    I would instead have a function for each of those things, and another function for the overarching behaviour that uses them. That functions calls the first function to get the array, then passes it to a call of the second function to get the nearest of them, then passes that to the third function to change state. This way, if I later have another behaviour that uses some or all of the same steps, I don’t have to repeat the code. This is a good practice to follow.

1 Like

I put them above the _ready() function with the other variables I define at the beginning (like speed)

Sorry to double post but I missed this message. I might have misinterpreted it, but if you are only updating the player_position and player_distance variables in _ready() then your enemy will just run to wherever the player was when the enemy spawned and jiggle about, not chase the player. You will need to update these occasionally, like every some number of _physics_process calls.

In my above answer I’ve addressed the root cause of the issue, but you will need to fix that too. A simpler fix to the main issue by the way is to replace the var player = preload(“res://scenes/player.tscn”) line with var player = get_tree().get_node("path to your player node in the scene tree"). This gets the node for the root of the scene tree, and then finds a node inside it. Sometimes simpler is better, though I prefer the method I outlined above as:

  • it is much more flexible
  • it doesn’t break if the structure of your scene tree changes or if you change the name of your Player node

I will personally be avoiding get_tree().get_node().

1 Like

You should write like this:

@onready var player = preload(“res://scenes/player.tscn”)

func _read() -> void:
    var player_position = player.global_position
    var player_distance = (global_position - player_position).length()

Why (From offical docs):

Mark the following property as assigned when the Node is ready. Values for these properties are not assigned immediately when the node is initialized (Object._init()), and instead are computed and stored right before Node._ready().

Two things to note here:

  1. “Values for these properties are not assigned immediately”
  2. " computed and stored right before Node._ready()"
@onready var player = preload(“res://scenes/player.tscn”)
var player_position = player.global_position # <---- player still not loaded yet
                                              #      it will load right before _ready()
var player_distance = (global_position - player_position).length()

Although, accessing player positions like this will not gonna work. You are creating a new player (inside your enemy) instead of accessing original player data. You should do more learning on how to do such things. I suggest following some tutorials on YT.

1 Like