StateTree MoveTo vs BehaviorTree MoveTo performance overhead

Hello,

We have a dedicated server multiplayer game in which there will be a lot of npc’s. Most of them will be quite simple, just have some Patrol/Wandering, Following Player and attacking logic.

We are testing both BT’s and StateTrees to see which one will be more performant since we want to have many of AI’s.

Upon testing with Unreal Insights with 100 AI;s it seemed to me like the StateTree has some overhead when using the MoveTo Tasks compared to BT Move To Node.

StateTree move to Task seems to use Tick for checking the bTrackingGoal distance Threshold.

In BT Node it is done by OnBlackboardValueChange event which is more performant?

In UnrealInsights it looks like we have the ProcessUntilTaskComplete which takes quite some time . The Screesnhot below is taken from a range of 20s. There are like big gaps in that StateTree block Tick and I dont know if its normal compared to BT one. which takes less time. [Image Removed]

[Image Removed] [Image Removed]

Is this a common problem or maybe there is something wrong with my setup? I tried to make BT and StateTree similar one to one, and in StateTree use Event Driven transitions instead of Tick. it looks somehow like this. The StateTree MoveTo just have transition on succeed to go to next state

[Image Removed] [Image Removed]

How did you do it in Fortnite let say? Do you use StateTree / BT or maybe Hybrid? Do you seperate Locomotion for BT and COmbat Logic for StateTree? Is There a way to make MoveToRequest more performant like in BT without using the Tick and still maintain that bTrackMovingGoal logic?

[Attachment Removed]

Steps to Reproduce

  1. Create Basic AI and Behavior Tree with MoveTo Node - Player as Target. Set bTrackMovingGoal = true
  2. Create Basic AI and State Tree with MoveTo Task - Player As Target. Set bTrackingMovingGoal = true
  3. Use Unreal Insights to measure both Systems , by spawning let say 100 AI

[Attachment Removed]

This seems relevant to your request

https://github.com/EpicGames/UnrealEngine/commit/03c7e88cce81a876b1808a073e97812d00a33c26

The tick is only supposed to occur if tracking a positional move target(not an actor). If the move target is an actor, it doesn’t need to tick. Actor based move targets can be dynamically tracked independent of the state tree task. That is handled inside the UAITask_MoveTo, based on the FAIMoveRequest::SetGoalActor

[Attachment Removed]

Is the long StateTree tick from your image in the reply part of what you attached? Would it be possible to send us a VisLog recording and the trace file for when you see this large hiccup in your Move To test? I am very curious what is happening inside of that ST tick that is taking up the CPU.

I do know that there is a known issue of some of the ST tags for Insights not showing up in 5.7. Another post on EPS brought that to our attention as we were looking at their traces. There are a few things happening during the tick that should be shown in Insights but are not. Such things like preparing runtime data for the component, starting ST tree execution, transitions, and cleaning up are what is missing from the total tick time. I do know that BP tasks can cause some longer ticks for EnterState/ExitState. We largely use BP to prototype and iterate then pull everything out to C++ in our projects once they are nearly finished with tweaks.

-James

[Attachment Removed]

I believe something may have gotten mangled on my end. Your recent attachment shows as being no longer available. Would you still have it so it can be re-attached?

-James

[Attachment Removed]

Ahh… we had an issue with this previously. Let me talk with some colleagues to see if I can get it approved or set up something such as a Box link to send it. My apologies for the aggravation with this. I thought it had been addressed.

-James

[Attachment Removed]

So I have spoken with our StateTree feature owner about some of the StateTree perf concerns being seen in 5.7. It appears that some overhead for StateTree has been added over development with each one having a minor impact, but altogether, there is something like 20% more overhead. We are actively investigating how to alleviate that going forward.

-James

[Attachment Removed]

My apologies! I did not see that final question to your post as I got caught up looking for the attachment that got stripped from your comment.

There is not a direct budget for async path queries that you can adjust. You can use synchronous pathfinds to avoid this, but that can cause hitches on the main game thread. The default setup for the navigation system processes the async path queries on background threads, but when world actor’s have reached the end of their tick, the navigation system calls PostponeAsyncQueries which returns the finished queries and postpones the rest to be handled in the next tick. You can enable LogNavigation logs to see how many queries are processed and postponed each tick. The idea is that requests can be handled off the game thread and use the tick ending as a sync point. This allows for some queries to finish each frame rather than waiting for all of them to finish which could be quite a high number.

You may also look at using TaskGraph.TaskPriorities.NavTriggerAsyncQueries either in your DefaultEngine.ini or as a CVar to change the way that navigation query threads are handled. This could impact other gameplay logic as the standard priority is for background threads and normal task priority. You could attempt background and high priority (TaskGraph.TaskPriorities.NavTriggerAsyncQueries bhn), or moving to normal threads with high task priority (TaskGraph.TaskPriorities.NavTriggerAsyncQueries nhn). I don’t think I would advise great caution and profiling if you chose this option to ensure other systems do not have adverse impacts from changing priorities.

Our normal idea is to stagger the amount of queries being made at once, but if you have something where all enemies immediately begin pathing to the player, that may not be possible. You could attempt a hierarchical pathfind solution as the hierarchical queries should be quicker. You could also look at using a singular query for enemies in an area that can begin using that path while starting their own queries. This would need some massaging, but it may have agents behave as a group with fewer pathfind queries to handle at the start.

[Attachment Removed]

You can add some kind of LOD system that will disable the crowd simulation state for the agents which should remove them from the active agent list. The only issue is that the Detour agents will not avoid the non-Detour agents because Detour is not aware of them. If only happening close to the player, should not be an issue, but something to look out for if there is a large group of them with only a few enabled for Detour.

You can increase the number as the complexity is largely how they sample neighbors. It is likely using both approaches to get the feel you desire. If you are using Mover, you could even investigate using Mover and its Integrations plugin to do pathing and avoidance in Mass. Not something with full production ready support, but something we did for The Witcher 4 demo.

-James

[Attachment Removed]

[Image Removed]Sadly I tried with the change and still I have similar results.

I don’t understand why sometimes StateTree is taking longer as shown in the screenshot, its still processing some task If I understan correctly from the right panel Callers.

But looking at Calees then AITask MoveTo takes significantly less. So Im not sure where the resources are going. I also have an EQS running to get the wander point. Maybe thats it?

[Attachment Removed]

Hello James,

Yes the long State Tree tick is also a part of the Trace I attached. I’m attaching the VisLog recording and new trace file in the Zip. Its the same testing scenario, with 100 AIs and MoveTo Player task. Regarding the BP_Task, I dont have State Tree BP Task, all of the task are in C++, same with conditions. I have one evaluator in c++ which is polling the data from Perception Component but I don’t think it might impact performance that much. Also when there is a large amount of MoveTo Request from the State Tree at the same time, it seems to queue those request making not all the AI’s move to at once. Is there some setting in Unreal to increase that limit? Or maybe there is some way to slice the MoveTo Request so they wont be called in the same frame, but all 100 ai’s will be able to move towards player?

[Attachment Removed]

For some reason the zip attachement doesnt work when I reply here, but I attached it to the original post as a third ZIP Attachement

[Attachment Removed]

Thank you, but I still managed to attach it as a ZIP File in the original post attachements

[Attachment Removed]

Okay Thank you,

And regarding the MoveTo limit part where I have many AI’s chasing Player and all of them request a MoveTo, then most of them is not moving (probably waiting in Queue for their request to be processed), is there some setting somewhere I can set to increase that limit if there is some or should I approach it differently, sliced the request somehow? Imagine like hordes in Days Gone

[Attachment Removed]

I tried to do some debugging and change the TaskGraph.TaskPriorities.NavTriggerAsyncQueries, but it didn’t help sadly :confused:

Enabled the LogNavigation but I cant see those logs for queries processes and postponed. Is it from the console command in editor right? and it will show in the output log or on screen?

Upon further testing and digging deeper, it looks like after 50 AI’s the next ones I spawn will not move (even though they are entering the MoveTo task and stay there)

I noticed that the AI’s that are not moving have `NOT ACTIVE` in Gameplay Debugger, but the BrainComponent is active. I tried to trace to where that debug is so I can check where this NOT ACTIVE is coming from but couldn’t find it in the `FGameplayDebuggerCategory_AI::DrawData` function or anywhere.

[Image Removed]I also dig deeper into AITask_MoveTo and at first I saw it was failing because the Movement was BLOCKED, but even though I set the SetMoveBlockDetection(false); in AIController, it didnt help so I guess it was not it. Tried to put smoe breakpoints in there but nothing triggered when they were already stuck in that task.

And this happens only to specific AI’s that are spawned after initial 50, when I move away from those 50 and then come back to them, their move to works normally.

Upon spawning new ones which dont move (after 50) I breakpoint into UAITask_MoveTo::Activate() and the flows goes normally through COnditionalPerformMove and later to PerformMove which goes to case EPathFollowingRequestResult::RequestSuccessful: but then the Request is never Finished because they are not moving :confused:

Perception also works for all of them (raising hands up upon detection), NavMesh is definetely there also .

do you know where I can look deeper why it blocks them? I don’t have much special set up, its just normal MoveTo node, but I will try to setup on blank project ot see if it happens there too

[Attachment Removed]

Okay I found out that the issue was with DetourCrowdAIController.

I set up the same scenario in the GASP and with normal AIController it was working with some delays, but eventually every AI would get their MoveTo processed.

With DetourCrowd there is a default limit of 50 max agents, but the ones who are spawned after the initial 50 are not considered anymore and I guess they don’t have like distance priority switching or anything. So the initial AI’s would have to die for new ones to be Registered?

I wonder how much I can increase it ( will definetely profile it), but we have a PvPvE zombie game so definetely there will be at least 100 and more AI’s spread across the map.

I guess what will need to be done is to dynamically enable/disable crowd simulation for AI’s that are close to player in a bigger group and for the rest just have it disabled.

But then I would also need to handle adding/removing Agents from the CrowdManager right?

I mean that let say we have 300 AI max on the map, but I have the Crowd Limit to 100.

all of them would register to CrowdManager upon spawn but only first 100 will run the logic for avoidance right?

Later if I let say go somewhere else to those 200 then would disabling CrowdSimulation on those initial 100, free up the space for the rest 200?

Or I would have to handle it manually the priority? I think its the dtCrowdAgent m_agents field in DetourCrowd?

[Attachment Removed]