com.unity.netcode.gameobjects icon indicating copy to clipboard operation
com.unity.netcode.gameobjects copied to clipboard

Add a single unified “OnNetworkStarted” event to NetworkManager

Open Extrys opened this issue 2 months ago • 5 comments

The problem

There’s no single event that fires once when networking starts, regardless of mode Currently you need to handle both OnServerStarted and OnClientConnectedCallback, which leads to redundant or missing calls depending on the mode (client/server) Using per-connection callbacks for this is unreliable, and managing timing around the singleton or validation properties(e.g IsListening) adds unnecessary complexity

Unless i am missing something, to me this dont feels right

Current workflow

The current workaround is using coroutines or polling to wait until the singleton exists and the NetworkManager is listening. This adds overhead and makes initialization logic fragile, even more for singleplayer sessions where the code may keep waiting forever as no connections are made

Example (based on forums suggestions):

void OnEnable()
{
   StartCoroutine(SubscribeToNetworkManagerEvents());
}

IEnumerator SubscribeToNetworkManagerEvents()
{
   yield return new WaitUntil(() => NetworkManager.Singleton); //This is fragile, and may add other problems, and no mention to cancelations
   NetworkManager.Singleton.OnClientConnectedCallback += OnClientConnectedCallback; //called on clients but unclear about standalone server
   NetworkManager.Singleton.OnServerStarted += OnServerStarted; //never called on non host client
}

void OnDestroy()
{
   if (NetworkManager.Singleton)
   {
      NetworkManager.Singleton.OnClientConnectedCallback -= OnClientConnectedCallback;
      NetworkManager.Singleton.OnServerStarted -= OnServerStarted;
   }
   initialized = false;
}

void OnClientConnectedCallback(parameters)
{
   RegisterMessageHandler();
}

void OnServerStarted(parameters)
{
   RegisterMessageHandler();
}

bool initialized;
//Both on client connected callback and server started calls this other method
void RegisterMessageHandler()
{
   if(initialized)
      return;
//For example
		NetworkManager.Singleton.CustomMessagingManager.RegisterNamedMessageHandler("a", B);
// In this case, Singleton can be null and CustomMessagingManager also can be null, thats why we need the yield
   initialized = true;
}

These yields are required only because there is no dedicated lifecycle event indicating when the NetworkManager and its subsystems (like CustomMessagingManager) are fully initialized As result, developers must manually simulate this missing initialization phase using coroutines or polling, which makes code more complex

Posible solution

Add new public static Action<NetworkManager> OnNetworkStarted event to NetworkManager Triggered once when the network stack has been initialized and the transport is ready, regardless of whether it’s running as server, client, or host. Passing the Network manager initialized (as after working on the last pull request, i saw on the tests part and other parts of code might exist more than one NetworkManager)

This would change the workflow to a much more convenient one like this

NetworkManager networkManager;

void OnEnable()
{
   NetworkManager.OnNetworkStarted += OnNetworkStarted;
}

void OnNetworkStarted(NetworkManager networkManager) 
{
   this.networkManager = networkManager; 
   networkManager.CustomMessagingManager.RegisterNamedMessageHandler("a", B);
}


void OnDestroy()
{
   if (networkManager)
      networkManager.CustomMessagingManager.UnregisterNamedMessageHandler("a");
}

[!NOTE] While NetworkBehaviour.OnNetworkSpawn can be used for components, this event would support use cases where no scene objects are involved (e.g ScriptableObjects or systems initialized outside scene contexts). It provides a clean, unified hook for all modes.

This change would mostly take 2 lines

  1. public static Action<NetworkManager> OnNetworkStarted on NetworkManager class
  2. OnNetworkStarter?.Invoke(This); at the very end of NetworkManager's internal void Initialize(bool server) method

It only may cause problems on having it static with the editor reloading feature, but that could be arranged cleaning the event with the [RuntimeInitializeOnLoadAttribute]

Extrys avatar Oct 06 '25 14:10 Extrys

We generally recommend that the server logic and client logic are separated at this point. The biggest reason is that there's a difference between the network being started and a client being fully synchronized and ready to receive events. We prefer the use-pattern of explicitly defining which point in the startup loop that setup should happen.

For more information, what sort of logic are you running for server, hosts and clients?

A workaround is to create a NetworkManagerExtension class that extends from NetworkManager and listens to both existing events to provide this behaviour. Would that work for your use-case?

EmandM avatar Oct 07 '25 15:10 EmandM

@Extrys

Just subscribe to the static events: NetworkManager.OnInstantiated & NetworkManager.OnDestroying These are invoked when the NetworkManager is instantiated and just before it is destroyed.

Then your script could look something like this:

[RuntimeInitializeOnLoadMethod]
private void ListenForNetworkManager()
{
    NetworkManager.OnInstantiated += NetworkManager_OnInstantiated;
    NetworkManager.OnDestroying += NetworkManager_OnDestroying;
}

private static void NetworkManager_OnInstantiated(NetworkManager networkManager)
{
    networkManager.OnClientConnectedCallback += OnClientConnectedCallback;
    networkManager.OnServerStarted += OnServerStarted
}

private static void NetworkManager_OnDestroying(NetworkManager networkManager)
{
    networkManager.OnClientConnectedCallback -= OnClientConnectedCallback;
    networkManager.OnServerStarted -= OnServerStarted;
}

void OnClientConnectedCallback(parameters)
{
    RegisterMessageHandler();
}

void OnServerStarted(parameters)
{
    RegisterMessageHandler();
}

bool initialized;
//Both on client connected callback and server started calls this other method
void RegisterMessageHandler()
{
    if (initialized)
        return;
    //For example
    NetworkManager.Singleton.CustomMessagingManager.RegisterNamedMessageHandler("a", B);
    // In this case, Singleton can be null and CustomMessagingManager also can be null, thats why we need the yield
    initialized = true;
}

Where you would obviously have script to handle disconnecting too (in order to reset things like initialized etc).

NoelStephensUnity avatar Oct 14 '25 23:10 NoelStephensUnity

Thanks for the response EmandM and Noel! First of all, I'm sorry for the late reply 🙏 I had a really crazy week and couldn't reply earlier

Your solution works and could be wrapped in an external layer, but what I'm suggesting is more about improving the user experience overall

Having a built-in event in NetworkManager (e.g OnStartListening / OnStopListening) would make these scenarios much simpler and more intuitive for most users

This kind of logic tends to appear frequently for example when registering these message handlers, initializing other clients/host systems once the manager is ready, or handling game multiplayer/singleplayer state transitions so having a native event would prevent everyone from needing to build their own workaround

The workaround you proposed in fact simplifies my initial example, but i feel it could be much simpler with these new events

Extrys avatar Oct 15 '25 03:10 Extrys

@Extrys No worries at all in regard to the response time... 👍

I am open to helping further simplify the startup and shutdown process indeed and would like to better understand how this area of Netcode for GameObjects can be improved.

Currently, OnClientStarted and/or OnServerStarted are pretty much the same as OnStartListening and OnClientStopped and OnServerStopped are the same as OnStopListening. It all happens linearly from the moment the NetworkManager is started (as a client, host, or server).

Are you saying that it would be easier to have two single events that would be invoked (whether client or server) as opposed to having the split client and server events?

As a side note: If the NetworkManager were to be refactored into a state machine where you could subscribe to changes in the state (while still maintaining the original event callbacks), would you view that as a similar improvement/simplification?

NoelStephensUnity avatar Oct 15 '25 14:10 NoelStephensUnity

Yes, exactly. I was referring to having unified events that trigger regardless of the specific mode (client, server, or host) In most cases, the logic that needs to run on startup or shutdown is identical, so having two generic events like OnStartListening / OnStopListening would simplify things and reduce branching code

The existing events (OnClientStarted, OnServerStarted, etc.) works fine, but they require checking roles or duplicating some setup logic, mostly when dealing with shared initialization or teardown functionalities, that's why I ask for additional events (OnClientStarted/OnServerStarted would remain unchanged)

And also yes, refactoring the NetworkManager into a state machine that exposes state change events would be an excellent improvement. It would make startup and shutdown transitions much easier to reason about and would fit perfectly with this kind of unified event flow

Extrys avatar Oct 15 '25 14:10 Extrys