General Performance Question

SemaphorGames

New member
Hello, I seem to be getting a decent performance hit from my Behavior Trees. I have a strong feeling that I'm doing something/multiple things very wrong to get this performance drop.

The profiler shows ~35% total in BehaviorManager.Tick(), expanding the hierarchy is a big mess of BehaviorManager.RunTask(), RunParentTask(), PushTask(), PopTask() etc which leads me to believe I've structured my trees rather badly.

On each NPC I have 3 Behavior Trees running simultaneously, En_Detect, En_Move and En_Attack:

En_DetectScreenshot.pngEn_MoveScreenshot.pngEn_AttackScreenshot.png

These Behaviors need to run every tick as movement handles the enemies aiming and turning around and such. These Behaviors are on my NPC Prefab, NPCs are spawned by instantiating the prefab.

I'm not using external trees or anything. I'm using version 1.5.11.

Is there anything glaringly bad that sticks out that would cause this performance drop?
 
As the tree grows in size it gets harder and harder to point out optimizations without seeing it in action. With that said, there are a couple of things that you can first try without restructuring your tree:

- Update to the latest version. I don't remember any dramatic runtime boosts, but there have been a good amount of improvements which should help make your tree more efficient.
- Tick at a different rate. Does your tree need to run every frame? If not you could tick it at a different rate and get a good speed boost.
- Double check your task switches. For example, I've noticed that you use a Both conditional abort in a lot of locations. Do you need to reevaluate the current branch when it is active as well as when the lower priority branches are active? I use Both conditional aborts a lot less than I use Lower Priority conditional aborts.
- Check your tasks switching. The less that your tree has to switch tasks the better, but this one could require a lot of restructuring and depends greatly on the context of your game, but could lead to a huge improvement. This is especially the case if you started when your game first started and you've spent a lot of time iterating on it.
 
After going through my Behavior Trees a bit more, from what I can tell all the "Both" Conditional Aborts I'm using are necessary.

For example, in this section of my Attacking Behavior Tree:
en_attack snippet.png

1. Checks if the Player is detected by the Enemy
2. Is a infinite repeat loop to continually attack the Player
3. Runs if the Player isn't detected, in which case it idles

So here "Lower" is needed because if the Enemy isn't detecting the Player (3), but then detects the Player, (1) will need to run to switch into the attacking loop.

And "Self" is needed because if the Tree is stuck in the attack infinite repeat loop (2) and the Enemy stops detecting the Player, (1) will need to be checked and fail so the tree can switch to the idle branch.

Would it be better performance wise to make the Conditional Abort "Lower" and Put another check (1) inside the repeat loop? It'd be doing the same thing pretty much (checking (1)) every tick.
Also, does the conditional abort check both the "Compare Shared Detect Type" Task and "Wait" Task? In which case the conditional abort would have to wait before switching branches?

Also, what do you mean by checking my task switching exactly?
 
Would it be better performance wise to make the Conditional Abort "Lower" and Put another check (1) inside the repeat loop? It'd be doing the same thing pretty much (checking (1)) every tick.
No, that likely wouldn't make a difference.

Also, does the conditional abort check both the "Compare Shared Detect Type" Task and "Wait" Task? In which case the conditional abort would have to wait before switching branches?
No, it only checks the Compare Shared Detect Type task since it is a conditional task. Action tasks are not reevaluted.

Also, what do you mean by checking my task switching exactly?
By task switching I mean traversing the tasks. Anytime the tree has to traverse to a new task it has to update the internal tree state. In general this doesn't cause a performance problem though because once everything has been allocated and pooled it does not need to allocate any more memory.
 
Hmm ok, so just to double check the Conditional Aborts only check Conditionals that are direct children of the Conditional Abort Sequence?
If so then I don't think there's anything I can do about my conditional aborts.

With regards to Task Switching, I want to traverse tasks as little as possible? Will task switching be a decent CPU drain?
So I should restructure the tree to switch tasks less, and could combining my Tasks/built in tasks that usually run after each other into a single task help with that?

For example in my En_Detect Tree in the repeater bit,
I could combine "Store Current Detect Type" and "Set Alert Animation" into one task
And there's probably a lot of combining I could do in the Sequences that are children of the Selector
 
Also do you have any advice on navigating the Profiler to help identifying the problem areas of the trees?

I can find lots of BehaviorManager.RunTask() and RunParentTask()s that use a lot of CPU, but I don't know how to identify which tasks the profiler is referring to.
 
As you mentioned I think focusing on the profiler will allow you to figure out more what to restructure. If you deep profile the project you should be able to drill down to the individual task level. From here you should have a better idea of the bottlenecks.
 
I can find lots of BehaviorManager.RunTask() and RunParentTask()s that use a lot of CPU, but I don't know how to identify which tasks the profiler is referring to.
Maybe you could add Profiler.BeginSample("name of your custom node") within the Execute block and dummy nodes to tag for profiling.
But I agree that BD would be easier to use if it had its own profiling tools. A simple heat map overlay on top of the graph would help a lot.
 
Top