Skip to content

feat: Instantiation payload support for INetworkPrefabInstanceHandler #3430

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

Open
wants to merge 33 commits into
base: develop-2.0.0
Choose a base branch
from

Conversation

Extrys
Copy link

@Extrys Extrys commented Apr 27, 2025

Solves Issue #3421

Related to the discussions in Pull Request #3419 and Issue #3421 (follow-up proposal based on new approach).

This PR introduces support for sending custom instantiation payloads through INetworkPrefabInstanceHandlerWithData to receive metadata before calling Instantiate().

The feature is fully optional, backward-compatible, and requires no changes to existing user workflows.

Changelog

  • Added INetworkPrefabInstanceHandlerWithData, a variant of INetworkPrefabInstanceHandler that supports synchronizing custom data prior to the Instantiate() method call.

Testing and Documentation

  • New tests have been added.
  • Inline code documentation provided; INetworkPrefabInstanceHandler.Instantiate() summary updated to mention INetworkPrefabInstanceHandlerWithData
  • Further external documentation is recommended for INetworkPrefabInstanceHandlerWithData.

Deprecated API

  • None.

Backport

  • Can be backported to v1.x. with a cherry pick if merged

Implementation Example

public class MyPrefabInstanceHandler : INetworkPrefabInstanceHandlerWithData
{
    GameObject[] prefabs;
    public int customSpawnID = 0;

	//NEW
    void INetworkPrefabInstanceHandlerWithData.OnSynchronizeInstantiationData<T>(ref BufferSerializer<T> serializer)
        => serializer.SerializeValue(ref customSpawnID);

    public NetworkObject Instantiate(ulong clientId, Vector3 position, Quaternion rotation)
        => Object.Instantiate(prefabs[customSpawnID], position, rotation).GetComponent<NetworkObject>();

    public void Destroy(NetworkObject networkObject)
        => Object.Destroy(networkObject.gameObject);
}

Spawning flow:

MyPrefabInstanceHandler prefabInstanceHandler = new MyPrefabInstanceHandler();
GameObject basePrefab;

void RegisterHandler() => NetworkManager.Singleton.PrefabHandler.AddHandler(basePrefab, prefabInstanceHandler);
void UnregisterHandler() => NetworkManager.Singleton.PrefabHandler.RemoveHandler(basePrefab);

public void Spawn(int id)
{
    prefabInstanceHandler.customSpawnID = id; //Plot twist: simply modify the handler's data
    NetworkManager.Singleton.SpawnManager.InstantiateAndSpawn(basePrefab.GetComponent<NetworkObject>());
}

Important

When spawning, you must update the handler's data before calling Spawn() or InstantiateAndSpawn().
The data set in the handler will be serialized automatically prior the instantiation process.

Highlights

  • Optional and non-breaking
  • Intuitive to configure and resilient to errors
  • Fully aligns with NGO patterns (Serialize/Deserialize symmetry)
  • Late-join and scene object support
  • No public API modifications

@Extrys Extrys requested a review from a team as a code owner April 27, 2025 02:26
@Extrys Extrys changed the title feat: Network Object Instantiation Payload feat: Instantiation payload support for INetworkPrefabInstanceHandler Apr 27, 2025
@Extrys
Copy link
Author

Extrys commented Apr 27, 2025

I just posted a video demonstrating how the system works:

  • * is a symbol set on objects spawned through PrefabHandlers.
  • & is a symbol added to indicate the deterministic ID of an object.

In the video, I spawn A1 and A2, which are instances of the same prefab, displaying the letter A.
Each button spawns the prefab with a different number, and this number is sent via the instantiation payload.
No RPCs or NetworkVariables are needed.

The B object is registered with a regular PrefabHandler (no custom interface implemented here, just for basic testing).

D, E, and F are instantiated directly via Unity's regular Instantiate method.
Each of these sets its deterministic ID manually and registers itself into a local dictionary, indexed by that ID.

2025-04-27.21-45-44.mp4

By implementing a custom INetworkPrefabInstanceHandler together with INetworkInstantiationPayloadSynchronizer,
I simply retrieve the ID from the payload and use it to link the correct instance deterministically.

Here is the core implementation:

public class TestHandlerDeterministicLink : INetworkPrefabInstanceHandler, INetworkInstantiationPayloadSynchronizer
{
	public Dictionary<int, DeterministicIDHolder> deterministicSpawns = new Dictionary<int, DeterministicIDHolder>();

	public int customSpawnID = 0;

	void INetworkInstantiationPayloadSynchronizer.OnSynchronize<T>(ref BufferSerializer<T> serializer) => serializer.SerializeValue(ref customSpawnID);

	public NetworkObject Instantiate(ulong clientId, Vector3 position, Quaternion rotation)
	{
		var obj = deterministicSpawns[customSpawnID];
		TMP_Text text = obj.GetComponent<TMP_Text>();
		text.SetText(text.text + "*");
		return obj.GetComponent<NetworkObject>();
	}

	public void Destroy(NetworkObject networkObject) => GameObject.Destroy(networkObject.gameObject);

	int nextDeterministicId = 0;

	public void InstantiateLocally(GameObject linkablePrefab)
	{
		var spawned = GameObject.Instantiate(linkablePrefab);
		spawned.transform.position = UnityEngine.Random.insideUnitCircle * 0.01f;
		var text = spawned.GetComponent<TMP_Text>();
		text.SetText(nextDeterministicId.ToString() + "&" + text.text);
		var deterministicIdHolder = spawned.GetComponent<DeterministicIDHolder>();
		deterministicSpawns[nextDeterministicId] = deterministicIdHolder;
		deterministicIdHolder.SetID(nextDeterministicId);
		nextDeterministicId++;
	}
}

Warning

While this system enables advanced workflows,
it is important to note that developers are responsible for ensuring that the linked instances are compatible.
This flexibility is intentional to support a variety of custom deterministic linking strategies.

@victorLanga17
Copy link

This would actually save us a lot of trouble.

Right now when after we spawn stuff we have to run like two or three RPCs just to finish setting up objects properly.
If we could send a bit of info during the spawn itself I think we could solve a couple of problems easier.

I wish you luck on get it merged on Unity 6.1, it would be super useful for our current project.

@Extrys
Copy link
Author

Extrys commented Apr 28, 2025

This would actually save us a lot of trouble.

Right now when after we spawn stuff we have to run like two or three RPCs just to finish setting up objects properly. If we could send a bit of info during the spawn itself I think we could solve a couple of problems easier.

I wish you luck on get it merged on Unity 6.1, it would be super useful for our current project.

Sure! I'm just waiting for reviewers to be assigned to this PR.
In the worst case, you can always use my fork, which I will keep updated for my use cases only, so sometimes it might not be fully up to date.

@Extrys Extrys marked this pull request as draft April 29, 2025 04:51
@Extrys
Copy link
Author

Extrys commented Apr 29, 2025

I just got more feedback on the Issue #3421

I'm thinking about a way to have the prefab handler "write" the payload right before the spawn happens.
The idea is that this write just stores the data locally in the instance that directly calls CreateObjectMessage, and then the actual message would consume that cached external variable right before sending.

In this approach, I would try to move most of the logic into CreateObjectMessage, removing it from the object data.
Although I feel there would still need to be a way to link the payload to the generated instances to make things work correctly for late joiners and similar cases.

This would avoid all the newly added generics and any potential object boxing.

I'm converting this PR into a draft to keep modifying the implementation and will get back to comment once it's ready for feedback again.

@Extrys
Copy link
Author

Extrys commented Apr 29, 2025

[Sorry bad writting i might edit this text later]

I did requested changes by @EmandM into the PR, currently im reusing the same buffer serializer from the object serialization.
Now the changeset is much smaller and easier to check and review, i would like you to give me feedback on that

Also i could make the new interface to not have the OnSynchronize, and having instead Serialize and Deserialize methods, but that would make the usage not as comfortable.

Copy link
Collaborator

@EmandM EmandM left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a fantastic next step! The video was super helpful to understand what the intention was. Is there any chance you have a more minimalistic example of this feature that doesn't require linking the items together later. The added complexity of having separate objects that are linked implies that this feature is doing the linking. My understanding is simply that this feature enables changing the object that is instantiated as part of the instantiation flow.

A few notes on the code:

Out of interest, is there a reason you chose to implement the custom data passing only on scene objects? We'd prefer a solution that works everywhere where the prefab handler can work. Again, the symmetry in the approach is important. If you can do something with prefab handlers in one area, that feature should work in all areas.

It would also be fantastic if you can add a test showing how this feature is used.

/// Interface for synchronizing custom instantiation payloads during NetworkObject spawning.
/// Used alongside <see cref="INetworkPrefabInstanceHandler"/> to extend instantiation behavior.
/// </summary>
public interface INetworkInstantiationPayloadSynchronizer
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This name could be more descriptive. How about something like INetworkPrefabInstantiationHandler?

Copy link
Author

@Extrys Extrys Apr 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, this interface doesn't handle instantiation itself, that’s entirely the role of INetworkPrefabInstanceHandler.

The purpose of INetworkInstantiationPayloadSynchronizer is strictly to serialize and deserialize the instantiation payload before Instantiate() is called. That’s why I went with a name that emphasizes its function in data synchronization, rather than suggesting it’s involved in the instantiation logic directly.

If we go with the OnPreInstantiate or OnBeforeInstantiation naming you suggested, perhaps something like INetworkPrefabPreInstantiationHandler or INetworkPrefabBeforeInstantiationHandler would better reflect the purpose. I’m happy to update the name as long as it clearly communicates what the interface does.

I used the term payload since it directly refers to the custom data being passed along with the spawn message, which is exactly what this interface handles

I'll explore a few alternative naming options in tomorrow commit to see if any feel like a better fit.

Copy link
Collaborator

@EmandM EmandM Apr 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Along with this note, I was also thinking it might be nice if this new interface extends from the base interface.

So rather than needing

class MyHandler : INetworkPrefabInstanceHandler, INetworkInstantiationPayloadSynchronizer

It could instead be used as

class MyHandler : INetworkPrefabPayloadHandler

or something of that type. Keeps it clearer for developers to implement and simpler to use.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a pretty good idea, i like how it looks simpler now, something like INetworkPefabInstanceHandlerWithData would be similar to the old namings of the job system, like IJobComponent IJobWithECB and similar.

INetworkPrefabPayloadHandler looks good to me but I feel INetworkPefabInstanceHandlerWithData its even more descriptive for developers who will use it.

What do you think, could we name it INetworkPefabInstanceHandlerWithData ?
Im sure we can find a better naming later for the second interface it wraps.

/// allowing you to cache or prepare information needed during instantiation.
/// </summary>
/// <param name="serializer">The buffer serializer used to read or write custom instantiation data.</param>
void OnSynchronize<T>(ref BufferSerializer<T> serializer) where T : IReaderWriter;
Copy link
Collaborator

@EmandM EmandM Apr 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To avoid confusion with NetworkBehaviours, could we rename this to something more descriptive like OnInstantiation() or OnBeforeInstantiation()?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good to me! As long as the name clearly reflects that this method is solely responsible for synchronizing data prior to instantiation, I’m happy to update it.

OnBeforeInstantiation works well for that, as long as we keep in mind that it is actually serializing and deserializing data

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might rename the interface to something like:
INetworkPrefabPreInstantianceDataSerializer INetworkPrefabPreInstantianceSynchronizer INetworkPrefabPreInstantiantiateHandler
And the method could be:
OnPreInstanceSerialization, SynchronizePreInstance, PreInstanceSynchronization, HandlePreInstantiateData

This way, the naming makes it clearer that its about the data flow before instantiation, not the instantiation itself, and it also avoids confusion with NetworkBehaviour.OnSynchronize()

Let me know if any of these seem closer to NGO’s naming conventions, or if you would prefer a shorter variation

@Extrys
Copy link
Author

Extrys commented Apr 29, 2025

This is a fantastic next step! The video was super helpful to understand what the intention was. Is there any chance you have a more minimalistic example of this feature that doesn't require linking the items together later. The added complexity of having separate objects that are linked implies that this feature is doing the linking. My understanding is simply that this feature enables changing the object that is instantiated as part of the instantiation flow.

A few notes on the code:

Out of interest, is there a reason you chose to implement the custom data passing only on scene objects? We'd prefer a solution that works everywhere where the prefab handler can work. Again, the symmetry in the approach is important. If you can do something with prefab handlers in one area, that feature should work in all areas.

It would also be fantastic if you can add a test showing how this feature is used.

Regarding this, I've tested it, and in the video only one object is an in-scene placed object. The rest are dynamically instantiated through the prefab instance handler, not just scene objects. Maybe I misunderstood what you meant?

I’ll work on a simpler example, though to be honest, the linking case is the most valuable use case I’ve found so far, its actually what motivated this feature in the first place.

Right now I dont have many alternative examples because most of the benefit comes from enabling that exact flow: having objects pre-created and deterministically selected or connected based on payload metadata. Of course, it also opens the door to more advanced use cases, like sending special setup/configuration data before instantiation (For example in the video that sets up A to be configured with a text that says the id to the next), but those are things I imagine users will find creative uses for once the mechanism is available.

In a way, I don’t yet fully know the limits of the feature, I just know it unlocks workflows that weren’t previously possible.

About the other changes, I will answer these and also come with some changes you might like tomorrow.

@Extrys
Copy link
Author

Extrys commented Apr 30, 2025

Although the struct is named SceneObject, it’s already used across the codebase to represent any instance of NetworkObject, not just those previously placed in the scene.
Even dynamically instantiated objects are serialized using this structure.
So this change applies universally to all uses of INetworkPrefabInstanceHandler, not only in-scene-object cases.


With that in mind, I’ve prepared a visual comparison to clarify why the deserialization buffer for the instantiation payload currently lives inside SceneObject.Deserialize
image

As shown above, the HasInstantiationPayload flag is handled symmetrically in both Serialize() and Deserialize(). Keeping the payload deserialization inline within SceneObject.Deserialize ensures that all object-related synchronization logic remains co-located, which helps maintain readability and traceability when following the execution flow.

Moving the deserialization logic to AddSceneObject, while possible, would separate serialization and deserialization across different layers.

That said, since AddSceneObject already deals with other responsibilities like calling SynchronizeNetworkBehaviours, moving the deserialization logic there wouldn’t be unreasonable either, especially if buffer reuse is preferred.

Here's a visual representation of how that could look if moved into AddSceneObject:
image

I’m happy to move the deserialization logic if preferred, just wanted to show why it currently sits in SceneObject.Deserialize() to keep things cohesive and close to where the payload is originally serialized.

Let me know what direction you'd like to take, and I’ll gladly adapt the implementation accordingly.


About the naming
I'm open to renaming the interface and method to better align with NGO conventions. Below are a five of options I’ve considered:

/// Option A  - Short concise and easy to undesrtand
INetworkPrefabInstanceDataHandler
   HandleInstanceData()

// Option B   
INetworkPrefabPreInstanceDataHandler
   HandlePreInstanceData()
   
// Option C   - This is also pretty descriptive
INetworkPrefabInstanceSynchronizer
   SynchronizeInstanceData()
   
// Option D
INetworkPrefabInstanceDataSerializer 
   SerializeInstanceData()

// Option E - For me, this is the most descriptive
INetworkPrefabInstantiancePayloadHandler
   HandleInstancePayload()

Happy to hear your thoughts or preferences here!

@EmandM
Copy link
Collaborator

EmandM commented Apr 30, 2025

I had another pass today. Definitely agree with what you've said about SceneObject! Unfortunately the reason SceneObject.Serialize and SceneObject.Deserialize are not perfect mirrors is due to the deferred messaging system for distributed authority, so we do need to follow the SynchronizeNetworkBehaviours flow. That does mean deserializing inside the AddSceneObject function, just before the call to CreateLocalNetworkObject.

I also took a bit more time with the example. I absolutely see what you're doing there. Thank you for the detailed explanations.

It'll be best if the function naming we go with follows the On<event> naming convention. Also how do you feel about the idea of having the new interface extend from INetworkPrefabInstanceHandler?

Mixing and matching from your naming options, what do you think of something like these two options?

/// Option A  
INetworkPrefabInstanceWithDataHandler
   OnPreInstantiate()
   
// Option B
INetworkPrefabWithSynchronizeHandler
   OnPrefabSynchronize()

@Extrys
Copy link
Author

Extrys commented Apr 30, 2025

I had another pass today. Definitely agree with what you've said about SceneObject! Unfortunately the reason SceneObject.Serialize and SceneObject.Deserialize are not perfect mirrors is due to the deferred messaging system for distributed authority, so we do need to follow the SynchronizeNetworkBehaviours flow. That does mean deserializing inside the AddSceneObject function, just before the call to CreateLocalNetworkObject.

No problem, I’ll make that change in a few minutes!


Also how do you feel about the idea of having the new interface extend from INetworkPrefabInstanceHandler?

Its a perfect idea, making it easier to use for developers. The next commit will include that.


Mixing and matching from your naming options, what do you think of something like these two options?

Since it’s still an INetworkPrefabInstanceHandler, just extended with support for instantiation-time data, I think INetworkPrefabInstanceHandlerWithData makes the most sense.

In contrast, INetworkPrefabInstanceWithDataHandler might suggest that the "instance" has a data handler, which doesn’t quite match the intended semantics.

Would this option work for you?

public interface INetworkPrefabInstanceHandlerWithData : INetworkPrefabInstanceHandler
{
   void OnSynchronizeInstantiationData<T>(ref BufferSerializer<T> serializer) where T : IReadWrite
}

The method name OnSynchronizeInstantiationData clearly indicates that it’s used to synchronize data for the instantiation process, implying that this happens before the object is actually instantiated.

Let me know what you think. I’ll go ahead and push a commit with these changes in the meantime and await your feedback. 😄

Extrys added 2 commits May 1, 2025 01:32
1) Moved the payload deserialization into the AddSceneObject, for deferred instantiation compatibility
2) Changed the new interface to be a direct single extended implementation, instead a complement of the handler
3) Some renames to better match what the feature does for developers
@Extrys
Copy link
Author

Extrys commented Apr 30, 2025

All requested changes have been implemented.

  • Moved the payload deserialization into the AddSceneObject, for deferred instantiation compatibility
  • Changed the new interface to be a direct single extended implementation, instead a complement of the handler
  • Some renames to better match what the feature does for developers

This is the same example shown earlier, but simplified and updated to reflect the new interface and naming conventions:

public class TestHandlerDeterministicLink : INetworkPrefabInstanceHandlerWithData
{
    Dictionary<int, DeterministicIDHolder> deterministicSpawns = new Dictionary<int, DeterministicIDHolder>();

    int nextDeterministicId = 0;

    int customSpawnID = 0;

    public void OnSynchronizeInstantiationData<T>(ref BufferSerializer<T> serializer) where T : IReaderWriter
    {
        serializer.SerializeValue(ref customSpawnID);
    }

    public NetworkObject Instantiate(ulong clientId, Vector3 position, Quaternion rotation)
    {
        return deterministicSpawns[customSpawnID].GetComponent<NetworkObject>();
    }

    public void Destroy(NetworkObject networkObject) => GameObject.Destroy(networkObject.gameObject);

    public void DoSpawn(GameObject linkablePrefab)
    {
        var deterministicIdHolder = GameObject.Instantiate(linkablePrefab).GetComponent<DeterministicIDHolder>();
        deterministicSpawns[nextDeterministicId] = deterministicIdHolder;
        deterministicIdHolder.SetID(nextDeterministicId);
        nextDeterministicId++;
    }
}

Marking the pull request as ready for review ✅
Let me know if anything else is needed!

Extrys added a commit to Extrys/com.unity.netcode.gameobjects_Extended that referenced this pull request May 5, 2025
- Added `INetworkPrefabInstanceHandlerWithData`, a variant of `INetworkPrefabInstanceHandler` that supports synchronizing custom data prior to the `Instantiate()` method call. (Unity-Technologies#3430)
@EmandM
Copy link
Collaborator

EmandM commented May 5, 2025

The big issue with this approach at this time is synchronizing new clients. I understand that this branch works great for your use case, but as we have to maintain this into the future we need to ensure that we're not adding a "foot-gun". That means, we need to be sure that this feature is hard to accidentally use incorrectly.

When a late joining client joins, the OnSynchronizeInstantiationData method is going to get called per-object for the newly joining client. We need to ensure that it's hard to serialize "bad" data on that method (e.g. old data or incorrect data).

There's a few approaches we're discussing NGO side, however we don't have the time at the moment to do a full design process. As we have backwards compatibility support requirements, we can't easily add a temporary option that will be made safer later.

We're still figuring out what the next steps look like. We're super intrigued by this option and would love it if anyone else who is interested in this design could chip in with their use-cases so we can better imagine how this feature will be used by different workflows.

@Extrys
Copy link
Author

Extrys commented May 5, 2025

Oh, I see...

So right now, OnSynchronizeInstantiationData() runs again when a new client connects and receives an already-spawned object (scene or dynamic). But since the handler only keeps the current value, not the one used at spawn time, the new client might get outdated or wrong data.

This means late joiners can get inconsistent state, depending on how the payload is used, which can lead to subtle bugs.

I didn’t consider this when refactoring the code, and it hasn’t caused issues in my use-case yet. but it’s definitely a valid concern, and not a hard problem to solve.

Previous versions of this included per-object payload storage to avoid that, but it was removed to keep things minimal. That logic can be added back pretty easily.

I'll work on updating the PR to reintroduce that logic, either by storing the payload inside each NetworkObject, or by keeping a dictionary keyed by NetworkObjectId. This will ensure late joiners receive the same instantiation data as everyone else, while maintaining flexibility in the design.

@Extrys
Copy link
Author

Extrys commented May 6, 2025

@EmandM I just pushed some changes that should address the main concern.

The buffer is reused when possible, and the payload is now serialized and deserialized directly into the NetworkObject. The SceneObject serialization process no longer relies on the PrefabHandler, which improves performance and avoids re-calling the synchronization method on the server when late joiners connect. The new test is still passing without modifications.

I've reintroduced instantiation payload injection into the NetworkObject before sending the spawn call. This step is skipped automatically if INetworkPrefabInstanceHandlerWithData isn't associated, so performance remains unaffected. If stricter control is needed, I can add an explicit boolean flag on the NetworkObject to skip earlier, but that seems overkill as the dictionary lookup is fast enough. It could technically be optimized further by storing INetworkPrefabInstanceHandlerWithData separately from INetworkPrefabInstanceHandler to avoid type checks and reduce lookup size, though again, probably overkill.

EDIT
I went ahead and implemented that optimization. Lookup overhead is now effectively eliminated when the feature is unused.

These changes should resolve the issue around late join synchronization and accidental misuse.

At this point I believe the implementation is solid, and I’ve refined it extensively based on the feedback so far. This feature is already being used across three active projects and has proven to be highly useful when you need different instantiation behaviors for the same prefab. I’ll wait for the review now in case anything else comes up.

Important

A common case is selecting the instantiation source:
For example, if a weapon is spawned from a weapon spawner, it might be configured differently than if it's spawned by an enemy.

Being able to send metadata at spawn time ensures that the system responsible for instantiating the object (whether it's a spawner, enemy AI, or another context) can apply the correct local setup logic immediately.
This avoids relying on NetworkVariables or RPCs, which are less reliable due to timing and ordering constraints.

This goes beyond linking pre-instantiated objects; it enables precise control over how objects are created based on context. In these real-world scenarios, pre-instantiation data is not just useful, it’s essential.

Extrys added 2 commits May 6, 2025 03:02
Updates the description of the method that retrieves
instantiation data for a NetworkObject.
@Extrys
Copy link
Author

Extrys commented May 6, 2025

I’d like to expand a bit on why this feature matters and the kinds of problems it helps solve.

The core issue with NGO's current spawn system is that it does not allow sending contextual metadata during instantiation. When the server tells the client that an object has spawned, the only thing you can customize is how the object is instantiated. You cannot specify who spawned it, why it was spawned, or include any data that describes the instantiation context.

This becomes a major problem in projects where the same prefab can be spawned by different systems, such as a weapon spawner, an enemy's hand, or custom gameplay logic. Each of these cases requires different setup behavior (e.g.):

  • disabling physics temporarily to avoid unwanted collisions at spawn time
  • configuring collisions or layers
  • linking to client-predicted object instances
  • skipping instantiation if the object already exists in the scene
  • passing in setup parameters directly

Currently, most of these differences must be handled using post-spawn RPCs or NetworkVariables, even though some cases such as linking to pre-existing objects cannot be handled that way at all. This adds latency, increases network traffic, introduces race conditions, and fragments the setup logic. The client receives the object without knowing why it was spawned or how it should behave until metadata arrives later.

I will also share this in the forums and my linked-in to gather feedback from users who may benefit from this feature. If you are following this PR and have relevant use cases, your input would be very helpful in demonstrating the value of this addition.

@turdann
Copy link

turdann commented May 6, 2025

Hi! We have been looking for a solution to a problem that we currently have and I think it could be solved with this! We're adding multiplayer to our game, which includes configurable enemies to add variety. Our enemies need to be configured at the moment they spawn, but we can't find a way to do that. The workaround we're trying adds too many edge cases for both single player and multiplayer modes, as well as latency issues, which isn't ideal for our game. The option you're suggesting might work well for us but not sure if we should try it out... Will it continue to be supported?

Extrys added 2 commits May 6, 2025 14:55
Prevents instantiation data synchronization failures from propagating,
ensuring that exceptions thrown during synchronization are caught and
logged, and that resources are properly released. This change improves
the robustness of the instantiation process.
Improves prefab handler lookup performance by introducing a dedicated dictionary for handlers with instantiation data.

This allows for faster injection of instantiation data into NetworkObjects.
@Tspk91
Copy link

Tspk91 commented May 6, 2025

Hello, I saw this on Discussions and I believe it would be quite useful as it mirrors better what we do for singleplayer. Currently we are hamstringed by only being able to do things post-instantiation where in singleplayer code it's as easy as adding the lines before.

@Extrys Extrys requested a review from EmandM May 6, 2025 14:10
@Nrosa01
Copy link

Nrosa01 commented May 6, 2025

I feel this feature could open the door to build a more proper client predicted spawning in server-client architecture. And as @Tspk91 said above, being able to use data for instantiation makes thing more similar to single player, and avoids all the headaches of syncing post-spawn

@Delunado
Copy link

Delunado commented May 6, 2025

Hey! Just saw this conversation and it looks like a neat addition to the netcode system. I have found myself in the situation of having to sync after spawning the GOs and it looks like the process can be simplified with this idea. Hoping to see it coming in the near future!

@Extrys
Copy link
Author

Extrys commented May 7, 2025

Edit: You can skip this comment if needed, the next one presents a significantly improved approach.

Hi @EmandM, sorry in advance for the wall of text, but as I keep working with this feature in a real production setup, I uncovered a few powerful use cases that I hadn't fully anticipated at first. Sharing them here in case they help illustrate the broader value of this approach.

We have networked enemies that spawn with networked weapons. Here's the situation:

  • Each enemy is initialized using a random seed.
  • The spawn logic varies depending on the spawner (e.g. horde spawner vs individual spawner), and that spawner determines key parameters like the enemy's height, skin, and the weapons they carry.
  • The weapon is also a networked object, and it is spawned and configured by the enemy itself, not by a spawner.

This is where instantiation metadata becomes essential and solves several problems at once:

  • When spawning the enemy on the server, we pass metadata that includes the spawner ID and the seed. This allows the client to instantiate and configure the enemy deterministically (including predictively spawning and configuring the weapon).
  • When the enemy spawns its weapon, it also passes metadata pointing to the owning enemy.

This enables hybrid predictive workflows on the client:

  • If the client receives the enemy first, it can predictively spawn the weapon locally. When the actual server-driven weapon instantiation arrives, it detects that the weapon already exists and links it correctly.
  • If the weapon arrives first, the metadata can include the enemy's instantiation data, enabling the inverse: predictive spawning of the enemy based on the weapon’s metadata, and linking the two afterward.
    • Alternatively, the client can simply cache the weapon until the enemy spawns, and then assign and enable it using the contextual information.

All of this becomes possible with a single metadata field per spawn. This approach supports:

  • Seed synchronization
  • Contextual instantiation and configuration
  • Predictive workflows for tightly coupled object hierarchies

And I’m not even getting into how this becomes absolutely essential when adding another layer of complexity, like procedural dungeon generation (which we’re actually doing) where having deterministic spawners combined with this system is pure gold.

It's a clean and extensible solution that significantly expands what’s possible in server-authoritative architectures, while avoiding RPC timing issues and reducing implementation complexity.


Also, thanks again for your feedback and all the iteration we made. Having the metadata synchronization built directly into the handler makes this incredibly easy to use and extend.

We were also able to build a lightweight SpawnerSystem on top of the handler API, without modifying any internal NGO code. Here's a simplified example:

Example 1

public class MySpawnerWithData<Y> : INetworkPrefabInstanceHandlerWithData, IDisposable where Y : struct, INetworkSerializable
{
    Y spawnData;
    void INetworkPrefabInstanceHandlerWithData.OnSynchronizeInstantiationData<T>(ref BufferSerializer<T> serializer) => serializer.SerializeValue(ref spawnData);
    public NetworkObject Instantiate(ulong clientId, Vector3 position, Quaternion rotation)
    {
        //My logic with this spawnData for instance handling
    }
    public void Destroy(NetworkObject networkObject) => GameObject.Destroy(networkObject.gameObject);

    List<GameObject> registeredPrefabs = new List<GameObject>();


    public void RegisterPrefabs(GameObject[] gameObjects)
    {
        foreach (var gameObject in gameObjects)
            RegisterPrefab(gameObject);
    }
    public void RegisterPrefab(GameObject gameObject)
    {
        NetworkManager.Singleton.PrefabHandler.AddHandler(gameObject, this);
        registeredPrefabs.Add(gameObject);
    }
    public void UnregisterPrefab(GameObject gameObject)
    {
        NetworkManager.Singleton.PrefabHandler.RemoveHandler(gameObject);
        registeredPrefabs.Remove(gameObject);
    }

    public void Dispose()
    {
        for (int i = registeredPrefabs.Count - 1; i >= 0; i--)
            UnregisterPrefab(registeredPrefabs[i]);
    }

    public void SpawnWithData(GameObject instance, Y data)
    {
        if (!registeredPrefabs.Contains(instance)) //This could be much more efficient, but is just an example to simplify our approach
            throw new Exception("Trying to spawn a prefab not registered to this handler instance");
        spawnData = data;
        instance.GetComponent<NetworkObject>().Spawn();
    }
    public NetworkObject InstantiateAndSpawnWithData(GameObject prefab,Y data)
    {
        if (!registeredPrefabs.Contains(prefab)) //This could be much more efficient, but is just an example to simplify our approach
            throw new Exception("Trying to spawn a prefab not registered to this handler instance");
        spawnData = data;
        return NetworkManager.Singleton.SpawnManager.InstantiateAndSpawn(prefab.GetComponent<NetworkObject>());
    }
}
public class MyServerCode
{
    public MySpawnerWithData<WeaponInstantiationData> weaponHandler = new MySpawnerWithData<WeaponInstantiationData>();
    GameObject[] weaponPrefabs;

    void Initialize()
    {
            weaponHandler.RegisterPrefabs(weaponPrefabs);
    }

    void GameLoop()
    {
        if (/*condition to spawn a weapon*/)
        {
            weaponHandler.SpawnWithData(/*weaponInstance*/, /*InstantiationData*/);
        }
    }

    void Dispose()
    {
        weaponHandler.Dispose();
    }
}

This is just one example, but the same pattern can be extended easily. Because the system is cleanly isolated, it allows users to build custom workflows or logic without ever needing to touch internal NGO code.

Example 2

public class MySpawnerWithDataExtended<Y> : MyExtendedHandler<Y> where Y : struct, INetworkSerializable
{

   //                                                                                                                                              /// this ///
    public NetworkObject Instantiate(ulong clientId, Vector3 position, Quaternion rotation, Y customData)
    {
        //My logic with this spawnData for instance handling
    }

 public void SpawnWithData(GameObject instance, Y data)
    {
        if (!registeredPrefabs.Contains(instance)) //This could be much more efficient, but is just an example to simplify our approach
            throw new Exception("Trying to spawn a prefab not registered to this handler instance");
        spawnData = data;
        instance.GetComponent<NetworkObject>().Spawn();
    }
}

This makes spawning context-aware prefabs as easy as calling MySpawnerWithData.SpawnWithData(), with clean separation of logic and full determinism support.


Just to wrap up:

I honestly can't find any flaws in this design. it solves real problems cleanly, feels robust and flexible, and stays safe by default.

Curious to hear what you think and open to any feedback or thoughts you might have.

@Extrys Extrys marked this pull request as draft May 8, 2025 11:29
@Extrys
Copy link
Author

Extrys commented May 8, 2025

Hey @EmandM, marking this as draft again. Feel free to skip the earlier messages. This one reflects the most relevant update.
I just uncovered a new edge case in real production use and have come up with a cleaner and more robust approach that I’m confident you’ll find much better.

the edge case in question:
When a networked object changes in a way that should affect how it is instantiated for late joiners (e.g. a weapon dropped by an enemy becomes a world item), the original instantiation metadata can become outdated. Late joiners would still use the old metadata, leading to invalid reconstruction (e.g. re-spawning the enemy that dropped the weapon).


To solve this cleanly, I'm moving toward a stateless and fully explicit model.
Instantiation metadata will be set externally on a per-instance basis.

For clarity: whenever I refer to "the handler" in this context, I'm specifically talking about INetworkPrefabInstanceHandlerWithData<T>, not Unity’s default INetworkPrefabInstanceHandler.

This involves:

  • Enforcing a strongly-typed contract on the handler interface for serialization.
  • Exposing a method on NetworkObject to explicitly inject metadata, ensuring deterministic behavior and full context control.

A typical flow would look like this:

networkObject.SetInstantiationData(instantiationData); 
//Throws an exception if no handler is associated with compatible data

networkObject.Spawn();
// If data was injected, it will be serialized and used during instantiation.
// If not, an exception is thrown to prevent undefined behavior.

Internally, the system can safely check if the injected metadata matches the handler’s expected type and apply it if compatible.

And the handler interface would become even simpler:

public class MySpawnerWithData : INetworkPrefabInstanceHandlerWithData<T> where T : struct, INetworkSerializable
{
    public NetworkObject Instantiate(ulong clientId, Vector3 position, Quaternion rotation, T instantiationData)
    {
        // Logic based on instantiationData
    }
}

With this design, the handler is fully stateless and reacts only to the data provided at instantiation.

This avoids bugs from reused or outdated values and gives developers precise control over how each object is spawned. It also supports dynamic workflows where an object's context may evolve after creation, without relying on RPCs or mutable state.

Since instantiation data is now injected explicitly, the spawning logic no longer needs to handle implicit synchronization. The system stays lean and efficient, with no added overhead and unchanged performance.

I’ll finalize the implementation and push the updated version shortly.
Let me know if you see any red flags or have thoughts before I wrap it up.

@EmandM
Copy link
Collaborator

EmandM commented May 8, 2025

Thanks everyone for all the extra use cases! Makes it easier to reason about what is needed.

I think I'm seeing two top level approaches to this type of feature:

  1. Set the data when the authority locally spawns the object, and then the data is fixed on the object on that point for the rest of the game and all network spawns/late join spawns use the configured data

or

  1. Each time any object is synchronized over the network, the associated prefab handler has the opportunity to send custom data on the spawn call

I was understanding that option 2 was the request, but I see here that option 1 might be more what was intended.

Thanks for the patience with this process. Adding new features always take a lot of back and forth and consideration. I've also been sick the last few days, sorry for the delay in responding.

@Extrys
Copy link
Author

Extrys commented May 8, 2025

Yeah, because I initially went with option 2, but real production use quickly exposed issues.
I’ve since shifted to option 1, with one key addition: the ability to explicitly override instantiation data if the object’s context changes (e.g. a dropped weapon).

The idea is to set metadata once per instance and have it persist across all synchronizations, including for late joiners.

Handlers remain fully stateless and act only on the injected data.

Hope that clarifies the approach. Really appreciate your input, and hope you're feeling better soon!

@Extrys
Copy link
Author

Extrys commented May 8, 2025

The full implementation is ready

Handler definition:

public class HandlerWithData : INetworkPrefabInstanceHandlerWithData<MyData>
{
    public NetworkObject Instantiate(ulong ownerClientId, Vector3 position, Quaternion rotation, MyData data)
    {
        //My logic with this instantiationData for instance handling
    }

    public void Destroy(NetworkObject networkObject) => throw new NotImplementedException();
}

Server-side spawn flow:

networkObject.InjectInstantiationData(instantiationData); 
networkObject.Spawn();

Additionally, I improved the integration tests for this feature and added a new one to simulate late joining.
Marking this PR as ready for review again.

@Extrys Extrys marked this pull request as ready for review May 8, 2025 23:22
Extrys added 2 commits May 9, 2025 02:10
Clarifies the purpose and usage of the static data map.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants