Skip to content

Linking Deterministically client-pre-instanced NetworkObjects #3421

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Extrys opened this issue Apr 23, 2025 · 9 comments
Closed

Linking Deterministically client-pre-instanced NetworkObjects #3421

Extrys opened this issue Apr 23, 2025 · 9 comments
Assignees
Labels
type:feature New feature, request or improvement

Comments

@Extrys
Copy link

Extrys commented Apr 23, 2025

Problem

Right now, Netcode for GameObjects (NGO) doesn’t provide a way to pass custom metadata during a NetworkObject.Spawn that can be read inside the INetworkPrefabInstanceHandler.

This makes it difficult to set up custom spawn logic, for example, using an ID or config sent from the server to decide which object to instantiate on the client side.


Use Case

In my case, I manually instantiate objects on both the server and the clients before they’re spawned over the network.

I have a level editor, and both clients need to create the level following the creation cycle I defined.

so my intention is BOTH clients creates the objects for the level and they store them where needed
then sending a signal to LINK currently existing items by an ID

For example i create, Apple, Orange, Banana, in the client
and Apple, Orange, Banana, in the other client

Then one of the clients (the host)
sends Link data, to spawn their objects in network, but i dont want the other client to re-create these objects, i want the client to use the exact same instaces that where deterministically created

When the server spawns the object, I want each client to match that spawn with an already existing local instance, using something like a shared ID.

Right now, the only way to do this is by sending an RPC first (to set the metadata into the PrefabHandler instance), and then calling Spawn().

But this has a problem: the spawn message and the RPC might not arrive in order, especially if several spawns happen in the same frame.

That makes the system fragile and hard to scale.

Proposal

Allow developers to send a small data buffer (like metadata or an ID) alongside the spawn call.

This buffer would then be passed into the Instantiate() method in the INetworkPrefabInstanceHandler, so that the developer can choose or configure the right object at the time of spawn.

For example:

// On the server
FastBufferWriter writer = new FastBufferWriter(128, Allocator.Temp);
writer.WriteValueSafe(myCustomId);
NetworkManager.SpawnManager.InstantiateAndSpawn(basePrefabN, writer);
// In a custom prefab handler
public NetworkObject Instantiate(ulong clientId, Vector3 position, Quaternion rotation, ref FastBufferReader reader)
{
    reader.ReadValueSafe(out int myCustomId);
    var instance = MyObjectRegistry.ResolveById(myCustomId);
    return instance.GetComponent<NetworkObject>();
}

The key points on this chanfe is:

  • No major API changes needed
  • Could be optional, for users who want it
  • Works perfectly with the current INetworkPrefabInstanceHandler flow
  • No need to rely on RPC order
  • Enables deterministic matching of client-side objects
  • Keeps the spawn logic clean and predictable

here is the pull request of the change already made in order to understand it better
#3419

@Extrys Extrys added the type:support Questions or other support label Apr 23, 2025
@EmandM EmandM added type:feature New feature, request or improvement and removed type:support Questions or other support labels Apr 24, 2025
@EmandM
Copy link
Collaborator

EmandM commented Apr 24, 2025

Thanks for being so responsive on this! This looks like something that is a real gap in NGO at this time.

To summarize what I understand of the problem:
There is a need for runtime data to be synchronized before an object is instantiated. Currently, NGO only has custom data synchronization after instantiation. It's possible to fake this behaviour via sending RPCs before the CreateObject call, however because the RPC is not linked the the CreateObject call that approach has issues.

The most important thing from the NGO side is that the solution chosen needs to be:

  • completely optional
  • backwards compatible - any user with existing NGO code shouldn't see anything change when they update to a version with this new flow
  • intuitive to configure and resilient to errors.

In order for the approach to be intuitive, we prefer symmetrical solutions and to follow patterns that are already existing in the NGO package.

NGO currently has two patterns main patterns for serializing custom user data around network objects: INetworkSerializable and NetworkBehaviour.OnSynchronize.

I'm thinking a flow where somehow a custom data handler of some type is registered against the PrefabHandler instance. This custom handler has methods that are invoked as part of the local spawn flow (where the custom data is serialized) and the remote spawn flow (where the custom data is deserialized), then the custom data can be used inside the Instantiate() method.

What are your thoughts on this?

@github-actions github-actions bot added the stat:awaiting-response Awaiting response from author. This label should be added manually. label Apr 24, 2025
@Extrys
Copy link
Author

Extrys commented Apr 24, 2025

Thanks! You understood it perfectly, and yes, that's exactly what I’ve been proposing.

This isn't about the initial PR description, but one of the follow-up comments I made later in the thread:
👉 #3419 (comment)

In short: the idea is to introduce an INetworkCustomSpawnDataReceiver interface that gets registered alongside the existing INetworkPrefabInstanceHandler.
This allows users to pass custom metadata in a way that's:

  • Fully optional
  • Backward-compatible
  • Consistent with existing NGO patterns

Something like this:

// On Server:

// Sets custom spawn data for the specified prefab handler.
// The handler must be registered for the given prefab and must implement INetworkCustomSpawnDataReceiver.
// Logs an error if the handler is not registered or does not implement the required interface.
NetworkManager.SpawnManager.SetCustomSpawnData(myBasePrefabToPointTheHandler, customData);

// Internally, the spawn system passes the custom spawn data to the handler.
// This allows the registered INetworkCustomSpawnDataReceiver to process it prior instantiation.
NetworkManager.SpawnManager.InstantiateAndSpawn(myBasePrefabToPointTheHandler);
// On the handler:

class MyHandler : INetworkPrefabInstanceHandler, INetworkCustomSpawnDataReceiver
{
    // Called just before Instantiate() , providing the spawn data.
    // (Note: Could use FastBufferReader instead of byte[] for better alignment with NGO)
    public void OnCustomSpawnDataReceived(byte[] customData)
    {
         // Parse and store the data for use in Instantiate().
    }

    // Called immediately after OnCustomSpawnDataReceived().
    public void NetworkObject Instantiate(ulong ownerClientId, Vector3 position, Quaternion rotation)
    {
        // Use the cached data to apply advanced logic during the spawn flow,
        // for example: returning a pre-instantiated object based on the received data.
    }
}

To support late joins and scene objects, I suggest placing customSpawnData directly inside SceneObject, toggled via a bitmask.
That way, the metadata is always available, whether the object was dynamically spawned or part of the scene, and routed through the same receiver-based system.

This avoids the need for:

  • Pre-spawn RPC timing guarantees
  • Client-side re-instancing just to support OnSynchronize()
  • Loosely indexed pools tied to the default INetworkPrefabInstanceHandler

🔶Happy to move forward with implementation if this direction works for you.


💡 Additional follow-up idea and potential extension (not part of main proposal)

I’d also propose introducing an additional interface, something like INetworkSpawner that would allow spawning directly from a handler instance, without needing to route through a base prefab.

This would let users bypass the need to register an "unused" prefab just to point to a handler.
Instead, you'd simply call:

myCustomSpawner.Spawn(customData); //also could be an optional parameter

And the handler would handle the instantiation and metadata injection directly.

This would further simplify advanced workflows without using a base prefab as a pointer and staying fully compatible with the current system.

@github-actions github-actions bot added stat:reply-needed Awaiting reply from Unity account and removed stat:awaiting-response Awaiting response from author. This label should be added manually. labels Apr 24, 2025
@EmandM
Copy link
Collaborator

EmandM commented Apr 25, 2025

This approach you've outlined is definitely an improvement on changing the SpawnObject interface directly!

The concern at the moment is that the approach is not symmetrical. It's important for NGO from a usability perspective that if you set custom data, it's hard to forget to get that custom data. From our side we'd prefer a single place with a single hook for both serializing and deserializing.

The two main approaches that NGO currently has for this symmetrical synchronization of data are:

  1. INetworkVariableSerializable where the user has to define the different serialization fields related to the full lifecycle of a NetworkVariable
  2. NetworkBehaviour.OnSynchronize which provides a single function that is called inside both the serialization and deserialization flow that provides a single place for custom data to be synchronized.

We could put something similar to NetworkBehaviour.OnSynchronize onto the NetworkObject, so that the custom data can exist before the NetworkBehaviour is instantiated? That way the NetworkObject can control things before the NetworkBehaviour is instantiated?

The other option you have is to have a root NetworkObject with a NetworkBehaviour, and then do the instantiation as a child object based on the data that is passed through OnSynchronize. Would this approach potentially work for your project?

@github-actions github-actions bot added stat:awaiting-response Awaiting response from author. This label should be added manually. and removed stat:reply-needed Awaiting reply from Unity account labels Apr 25, 2025
@EmandM EmandM self-assigned this Apr 25, 2025
@github-actions github-actions bot added the stat:Investigating Issue is currently being investigated label Apr 25, 2025
@Extrys
Copy link
Author

Extrys commented Apr 25, 2025

Ok i get why you prefer having symmetry.
Honestly, I think I can adapt it to work that symmetrically without any issues.

That said, about the alternative you proposed, these are details on why wont work on my case:

Using a root NetworkObject plus child instantiation based on OnSynchronize:

  • In my case, the whole object graph has to be pre-instantiated deterministically during level loading, before networking even kicks in.
  • Objects are heavily interconnected (hierarchies, wiring, dynamic references) and need to exist upfront so those links are valid from the start. Rebuilding all that externally would add way too much complexity to the level editor.
  • The goal is just to link the server-spawned object to the already created local instance. I do not want NGO to instantiate new things, modify parenting, or rebuild objects after the fact.
  • Metadata (like deterministic IDs) needs to be available right when selecting the instance to spawn, not after it's already there.
  • And every object should still be able to use their own RPCs normally once they are linked.

Note

Other alternatives i thought for mi case would be more datadriven or using Netcode for Entities, but would mean a massive rework just to handle something that right now is basically working cleanly in that PR.

A Cleaner, Symmetrical Alternative

A way to align perfectly with NGO’s symmetry style would be to tweak the INetworkCustomSpawnDataReceiver idea a bit.
For now on renamed to INetworkCustomSpawnDataSynchronizer

Instead of just introducing SetCustomSpawnData(..., ...) or OnCustomSpawnDataReceived(...) methods, the interface could follow the same pattern as OnSynchronize(), like this:

public class MyCustomHandler : INetworkPrefabInstanceHandler, INetworkCustomSpawnDataSynchronizer
{
    public int deterministicId;
    public float otherDataExample;

    protected void OnSynchronize<T>(ref BufferSerializer<T> serializer)
    {
        serializer.SerializeValue(ref deterministicId);
        serializer.SerializeValue(ref otherDataExample);
    }

    public NetworkObject Instantiate(ulong ownerClientId, Vector3 position, Quaternion rotation)
    {
        // Use the custom spawn data here
    }
}

Setup would remain the same as always:

MyCustomHandler myCustomHandler;

public override void OnNetworkSpawn()
{
    NetworkManager.AddNetworkPrefab(basePrefab);
    NetworkManager.PrefabHandler.AddHandler(basePrefab, myCustomHandler);
    // If the handler implements INetworkCustomSpawnDataReceiver, no extra work needed
}

public override void OnNetworkDespawn()
{
    NetworkManager.PrefabHandler.RemoveHandler(basePrefab);
    NetworkManager.RemoveNetworkPrefab(basePrefab);
}

void ExecuteSpawnMethod()
{
    // Pass the custom spawn data before calling Spawn()
    myCustomHandler.deterministicId = oneOfMyAlreadyInstantiatedNetworkObjects.deterministicId;
    oneOfMyAlreadyInstantiatedNetworkObjects.GetComponent<NetworkObject>().Spawn();
}

Internally, NGO would just call OnSynchronize() before the actual spawn happens, both when sending and receiving, keeping everything clean and predictable.

Does this sound like a better fit for NGO design?
I Would love to work on this if you’re happy with the direction! 😄

@github-actions github-actions bot added stat:reply-needed Awaiting reply from Unity account and removed stat:awaiting-response Awaiting response from author. This label should be added manually. labels Apr 25, 2025
@Extrys
Copy link
Author

Extrys commented Apr 27, 2025

Hello @EmandM
I've submitted a new PR addressing this issue with a much cleaner approach:
👉 #3430

It allows synchronizing payloads in INetworkPrefabInstanceHandler, preserving symmetry and NGO workflow consistency.

@EmandM
Copy link
Collaborator

EmandM commented Apr 29, 2025

Copying the OnSynchronize approach is a great option! I really like the usage pattern of this design.

Unfortunately, there's a few further constraints on the code design. It's very important to ensure that adding this feature is not going to change or slow down the object instantiation process for any game that does not need custom data to be sent before instantiation.

Any new system that is being added into NGO needs to be added in a way that does not require changes in any of the existing test suite. Any tests that need to be changed as a result of this change means that we are requiring changes in user's projects when they want to upgrade to the new version of NGO with containing new design.

Generics are hard to maintain and expensive to run, we tend to prefer to avoid generics in the NGO codebase for optimization reasons.

It would be preferable if this approach to synchronizing the custom data is synchronized during the CreateObjectMessage serialization. That way we can use the already existing writer and reader. I don't know if the injection approach will handle all of the weird and wonderful edge cases of object spawning. We'd also really rather avoid unsafe code as much as possible.

The pattern of usage in this approach is really nice. We just need to keep iterating to ensure we're future-proofing the code, and ensuring that we're keeping the NGO package open and flexible to all different types of games.

@github-actions github-actions bot added stat:awaiting-response Awaiting response from author. This label should be added manually. and removed stat:reply-needed Awaiting reply from Unity account labels Apr 29, 2025
@Extrys
Copy link
Author

Extrys commented Apr 29, 2025

It's ok!
Marking the pull request as a draft to keep iterating.

I will try the "moving as much of possible into the CreateObjectMessage" approach and explore a way to remove the newly added generics without relying on object boxing, aiming to find a solution that avoids both overheads.
I will do my best to ensure it doesn't impact performance for current projects.
In fact, your feedback gave me an idea on how to get it keeping everything more external even.

I will keep working on it and get back to this post once I'm ready. It would be great if we could stay in touch through the PR draft instead, linking yourself as a reviewer, so feedback would make more sense in context.

And again thanks for the feedback! ❤️
Much appreciated!

@github-actions github-actions bot added stat:reply-needed Awaiting reply from Unity account and removed stat:awaiting-response Awaiting response from author. This label should be added manually. labels Apr 29, 2025
@Extrys
Copy link
Author

Extrys commented Apr 29, 2025

I got something working with your feedback already, you might wanna check it when you have time 😄

@EmandM
Copy link
Collaborator

EmandM commented Apr 29, 2025

This approach is much cleaner. Now that we've iterated much closer to a solution that works, I'm happy to move the conversation into the pull request. Closing this issue and moving the conversation into #3430

@EmandM EmandM closed this as completed Apr 29, 2025
@github-actions github-actions bot removed stat:Investigating Issue is currently being investigated stat:reply-needed Awaiting reply from Unity account labels Apr 29, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type:feature New feature, request or improvement
Projects
None yet
Development

No branches or pull requests

2 participants