As I was building out my latest game, I began to add dozens of enemies that needed to gradually move towards the player via a simple 2D pathfinding algorithm. When I play-tested the level, my framerate dropped quickly from 60fps down to 10-20fps. I knew exactly what it was, and it was something I was hoping to fix eventually: all the enemies on the level calculate a 2D path to the player every ~400ms or so. And all those calculations are happening in the main game thread, which is handling rendering, physics, etc. So each time an enemy needed to update their path to the player, the rest of the game had to pause for a fraction of a second. Obviously, this is terrible. Way too much happening in that single thread.
The solution: Add another thread!
I needed to move the calculation of each enemy’s path to a thread, and better yet, if we could move all of the calculations to a single thread, and update all enemies’ paths that needed updating, that’d be ideal.
I found an example of what I wanted to do on Godot engine’s forum. User Skipperro
posted a code sample of how one may move the pathfinding code to a thread:
As you can see, Skipperro
makes a new thread (called pathfinder
) to do the work for all enemies in a scene. The thread is responsible for
- checking if the thread is currently doing work. If so, do not attempt to do more work.
- finding all enemies that need a path:
- the enemy is not dead
- the enemy’s last update was > 1.0 second ago
Then, the code pushes details about the enemies that need updating onto an array, enemies_to_command
- the enemy node’s name
- the enemy node’s global position
- the player’s global position
Once the code has collected all the details about enemies needing updates, it pushes those details to the thread. The thread simply runs a function:
Then, _async_pathfinder
, loops through the incoming data, and for each entry:
- it looks up the enemy node by name in the current scene
- calls
nav.get_simple_path(start, nav.get_closest_point(end), true)
to calculate the 2D path from start (the enemy) to end (the player) - sends the path as an array of Vectors to the enemy node
When it’s done, it lets the Godot engine know the thread can be put to sleep and wait for further instructions by calling pathfinder.wait_to_finish()
.
I made some slight modifications to the script, mostly to fit my own use case.
- I based the update time per enemy to every 400ms
- I removed the first returned point in the calculated path, since the first point is the starting point of the enemy at the time of calculating the new path.
With that change, my frames per second was back up to 60fps (or better).