I’m working on developing AI NPCs using a Behavior Graph that will operate in a Distributed Authority multiplayer environment. On initial spawn, the NPCs work as expected. But when a client joins and the NPC changes ownership, the AI stops processing.
I understand this is because the way the BehaviorGraphAgent works when NetcodeRunOnlyOnOwner is set to true that the Agent component will disable itself to non-owners.
#if NETCODE_FOR_GAMEOBJECTS
public bool NetcodeRunOnlyOnOwner = false;
public override void OnNetworkSpawn()
{
if (!IsOwner && NetcodeRunOnlyOnOwner)
{
enabled = false;
return;
}
}
#endif
As such, the Agents don’t sync. Instead they just “restart” when a new client joins and takes ownership when enabled within OnOwnershipChanged.
How would I ensure that an Agent from its previous client can continue processing once another client takes ownership? I’m not sure in what direction to start so any feedback is appreciated!
The trick is to only have the brain, ie the BehaviorAgent for Behavior, enabled only on the Owner
You can use OnGainedOwnership and OnLostOwnership with the HasAuthority flag to enable the Agent.
This snippet comes from a class that can be used for the client or NPC. When used as NPC, ie when useAI is true , you want to disable the controls, ignore any camera setup but enable the AI
//AI Behavior
public BehaviorGraphAgent agent = null;
public bool useAI = false;
/// <summary>
/// Save camera's old position, and make camera child of unit.
/// setup unit
/// Disable controls on non Authority units
/// </summary>
protected override void OnNetworkPostSpawn()
{
if (HasAuthority)
{
if (!useAI)
{
// Store the original position and rotation to be applied when the player despawns
originalCameraPosition = Camera.main.transform.position;
originalCameraRotation = Camera.main.transform.rotation;
Camera.main.transform.SetParent(transform, false);
}
}
else
{
controls.Disable();
}
if (useAI && agent)
{
agent.enabled = HasAuthority;
agent.Restart();
}
base.OnNetworkPostSpawn();
}
/// <summary>
/// Update Agents brain as ownership is gained
/// </summary>
public override void OnGainedOwnership()
{
if (useAI && agent)
{
agent.enabled = HasAuthority;
agent.Restart();
}
base.OnGainedOwnership();
}
/// <summary>
/// Update Agents brain as ownership is lost
/// </summary>
public override void OnLostOwnership()
{
if (useAI && agent)
{
agent.enabled = HasAuthority;
agent.Restart();
}
base.OnLostOwnership();
}
One other thing to consider, is that as AI are spawned you could distribute their ownership across all the clients in the scene to reduce the workload on the host.
In regard to the Restart issue, since the AI is not active on non owners, when the new owner takes control it must populate the BehaviorGraph state and Blackboard. The call to Restart would cause the Agent to start evaluating from the OnStart Node. I don’t think there is a mechanism to tell the graph to start at an arbitrary location inside the graph. In most cases it shouldn’t be noticeable. The one exception is the AI Navigating to a position, it might pause, but if the world state is the same, it should work it’s way back to that node and resume walking.
Hi @mockah , this is a good point you raise, and thank you @AnthonyRUnity for the response here.
We’re in talks with the Netcode for GameObjects team to figure out how to make integration better between the 2 packages. It’ll unfortunately only be around Q1 probably.
For now, as Anthony mentioned, you’d need to add your own mechanism to transfer ownership, as well as synchronizing the graph state. I know that’s not great for now.
It does sound like the approach that is most accurate across clients is to just sync each Agent’s graph and blackboards across all clients which does sound tricky to implement from my current understanding of both the NGO and Behavior libraries.
This does seem like a more approachable way to handle this scenario for my current app since I don’t expect there to be clients constantly connecting/disconnecting as the max amount of players I’m developing for is currently 3-4. So the restarts shouldn’t be too noticeable as long as I sync important world data to a single source (most likely on the session owner). Thank you for this insight.
I agree the best approach for user experience would be to sync the graph states which I’ll see if I can manage a mechanism to handle this if I determine that having consistent AI is crucial instead of having their logic restarted, but as you implied that is easier said than done .
And thanks to both the Behavior Team and NGO Team for looking into possible solutions for this. The effort is appreciated!