Efficient ways for NPCs to reference nearby Players or Enemies?

Godot Version

4.5

Question

I’m working on implementing some basic AI to enemies in my first person sword fighting game and I’m having a hard time deicing how I want them to get a reference to players or npcs. There are a few options I can think of, but they all come with some downsides:

1) Player Reference Singleton or Export Variable

This is what I most commonly see being done and for a good reason. It’s super easy to just give the enemy reference to the player right away with an auto load PlayerRef script or by dropping the player into an export variable on the EnemyBase class.

However, I feel this method is limiting. I’m very early into development so don’t know where this game is going yet. Even though this would work fine for me now, down the line it’s probable that the enemy ai is going to need to target other NPC’s or potentially even multiple players. For that reason I’d opted for a different approach:

2) Groups

Currently I’m using groups for my enemy to get reference to the Player. On ready the Player adds itself the Players Group. From the Enemy script I have a target_player function that will get_tree().get_nodes_in_group("Player") and for each node in the Players group, I check distance, and then keep a reference to closest player as var target_player: Player.

As my game is now the target player will always just be the same, since right now there is only ever one node in the Player group. However, this feels very scalable. If ever implement multiplayer, my function can already choose it’s target_player when there are multiple players. Additionally, I can easily expand it to reference NPC’s too by just having it search another group like HostileCharacters or something and rename target_player to target_actor.

My issue with this set up is that it feels expensive. I could be wrong, but it seems excessive for the enemy to always know what the closest enemy is, and consistently be checking every node in the tree to find nodes in the Player group. So I’m considering switching it to:

3) Area3D

As you can probably guess, this option is just putting an Area3D in my EnemyBase scene. I can use the body_entered and body_exited signals to make an array of players (or hostile NPCs) that are inside the area. I can then use that Array in a function to set target as one of the bodies.

In my head this option makes the most sense, however I’m worried about the area3Ds causing performance issues if there are a lot of enemies at once. Especially since my EnemyBase scene already has an Area3D serving an unrelated function.

Is there another more efficient way to do this that I’m not thinking of? If not, would swapping my current system for the Area3D option be better or worse for performance?

It depends on the number of enemies/players. Best to profile your code to determine if that’s making any actual bottlenecks. If not, just keep doing it bruteforce.

Once/if the number of entities starts causing measurable bottlenecks, the best way is to employ some sort of space-partitioning scheme. This can potentially get involved.

Using areas would be the second best. Simple to implement and it will take advantage of fast native broadphase collision detection. It can possibly even beat space partitioning implemented in “slow” script code. Again, profile to determine bottlenecks.

So you can proceed in stages, moving to the next if the performance is not satisfactory: bruteforce aka don’t worry > areas > space partitioning > space partitioning implemented in native code (via gdextension).

1 Like

Thanks! I’m aware of space partitioning as a concept but have never tried implementing it before. If I feel like the areas start causing performance issues I’ll definitely take the time to learn and implement that. You are right about moving in stages, I think i got a bit ahead of myself wanting to make everything as performant as possible from the start without actually having any performance issues yet. I’m gonna switch my current system to use areas for now, definitely should have done that from the start instead of trying the group thing.

2 Likes