Unity-Technologies / com.unity.netcode.gameobjects

Netcode for GameObjects is a high-level netcode SDK that provides networking capabilities to GameObject/MonoBehaviour workflows within Unity and sits on top of underlying transport layer.
MIT License
2.15k stars 434 forks source link

Manually set `NetworkObjectId` #2412

Open FreshlyBrewedCode opened 1 year ago

FreshlyBrewedCode commented 1 year ago

Is your feature request related to a problem? Please describe. I am porting a single user app to a multi user experience and need to make sure interactable objects are synced. My existing app loads 3D meshes from disk at runtime and dynamically instantiates GameObjects for the mesh. I would like to keep the current loading/spawning logic and just add a NetworkObject component and tell NGO to use the same NetworkObjectId for the matching GameObjects (e.g. using the GameObject name/scene path as a unique identifier).

Describe the solution you'd like I would like to add a NetworkObject component manually on each client and tell NGO to treat it like a NetworkObject that was spawned by the server, ideally by manually assigning a NetworkObjectId and registering the object with NGO.

Describe alternatives you've considered Register the GameObjects using e.g. AddNetworkPrefab and calling Spawn on the server, or wrapping the dynamic GameObject with a pre-defined prefab.

Additional context Having to rely on NGO to instantiate networked GameObjects feels like an unneccessary step if there is existing spawning/instantiation logic in place. I understand that this is more of an advanced use case and ideally one would design their app/game with the NGO spawning approach in mind but this is not always the case. So far I have not found a good workaround.

NoelStephensUnity commented 1 year ago

@FreshlyBrewedCode

Message Routing

Each NetworkObject instance has to have a unique NetworkObjectId. The reason is that any messages sent to each unique instance needs to know "which" unique instance.

Example: Two NetworkObjet instances of the same network prefab, with the same GlobalObjectIdHash value, would need unique NetworkObjectId values in order to be able to send/receive messages to their respective NetworkBehaviours. If you had two NetworkObjects with the same NetworkObjectId, then this process would break.

Alternate Approach

Have you tried just creating a "blank network prefab" where your dynamically instantiated GameObject for each mesh loaded is parented under the spawned instance of the "blank network prefab"? image

The LoadMeshBehaviour script would handle letting the clients know what 3D mesh was associated with the instance and could be set on the server side.

The pseudo code example of that script:

using Unity.Netcode;

public class LoadMeshBehaviour : NetworkBehaviour
{
    private void OnServerSerializeMeshInfo(FastBufferWriter writer)
    {
        // The server-host would write out the unique 3D mesh
        // specific information pertaining to this instance here
        // (i.e. path to asset or the like)
    }

    private void OnClientDeserializeMeshInfo(FastBufferReader reader)
    {
        // Clients would read the serialized 3D mesh specific information
        // (i.e. path to asset or the like), and either load it at this point
        // or store the information to be loaded when spawned locally
    }

    /// <summary>
    /// Automatically invoked when:
    /// Server-Side (Write): the associated NetworkObjet is being synchronized with
    /// a joining client or spawned for the first time (and there are connected clients).
    /// Client-Side (Read): the associated NetworkObject is spawned
    /// </summary>
    protected override void OnSynchronize<T>(ref BufferSerializer<T> serializer)
    {
        if (serializer.IsWriter)
        {
            OnServerSerializeMeshInfo(serializer.GetFastBufferWriter());
        }
        else
        {
            OnClientDeserializeMeshInfo(serializer.GetFastBufferReader());
        }
        base.OnSynchronize(ref serializer);
    }
}

Then when the client instance is spawned the 3D mesh can be loaded and parented under the spawned instance.

If you had specific NetworkBehaviour components associated with various 3D Meshes, then you would need to create a unique "blank network prefab" per unique combination of NetworkBehaviour components.

Example:

=========================================

========================================= For example, you could have 5 of your 3D Meshes that use BlankNetworkPrefabA, 10 that use BlankNetworkPrefabB, and 3 that use BlankNetworkPrefabC (in other words if you map out what NetworkBehaviour components are needed and then determine which 3D Mesh needs which NetworkBehaiour components you could then narrow down the number of "blank Network Prefab types" to a small set).

If this misses the mark by a long shot, then maybe you could describe what you are trying to accomplish a bit further?
i.e. What is the purpose of the 3D Meshes? Will you be sending messages via RPCs or will they have NetworkVariables associated with them?

Let me know if the alternative seems like it will work or if you have different needs (with more details on the 3D meshes being loaded and what kind of network synchronization they will need to provide).

FreshlyBrewedCode commented 1 year ago

Thank you so much for the detailed response @NoelStephensUnity!

The approach you describe sounds good and it is what I would probably consider doing if I were to implement my app from scratch. However as I mentioned, I am in the process of adding multi user support via NGO to an existing app so ideally I don't want to mess too much with the existing mesh loading and instantiation logic (I am not going to go in depth here because it is not really relevant to the issue, but it is slightly more complex since it involves loading the 3D mesh using 3rd party library and heavily post processing the created GameObjects).

What is the purpose of the 3D Meshes? Will you be sending messages via RPCs or will they have NetworkVariables associated with them?

The meshes are mainly just visual but users are able to move/rotate/scale them. Ideally, I just want to attach a working NetworkTransform to each mesh. In fact, I already have a custom NetworkBehavior that handles all of the interactions and works perfectly for prefabs I instantiate with NGO.

maybe you could describe what you are trying to accomplish a bit further?

In short, I want to

Here is how I would expect this to work:

It would be even better if one could provide a custom id or a "seed" value for the id (e.g. the scene path, custom hash value, etc.), but I understand that this could potentially lead to duplicate NetworkObjectIds.

Again, I realize that this would be an advanced feature that is more on the low level side. But as far as I can tell this would already be possible if we would have access to part of the internal/private API. So my feature request is to expose this as an option via a public API, maybe even indicate that it is potentially unsafe, e.g. NetworkObject.AssignIdUnsafe(ulong id).

I might even consider drafting a PR for such feature but I would like to get some input/feedback first.

NoelStephensUnity commented 1 year ago

@FreshlyBrewedCode Your details helped clear up a bunch of fuzzy spots for me. Thank you for that! 👍

So, this does indeed sound like an advanced feature request and would be something that could potentially take longer than you might want for your project. The method, using "blank network prefabs", I described would be the best way you can handle this without running into any major issues.

The biggest road block you would find out about if you tried to make your own PR is all of the pre-runtime code generation that has to occur. I would take some time to browse through everything that is handled during ILPP before attempting to make a PR or create a fork of your own. This is actually the primary reason behind the lack of runtime dynamic NetworkObject/network prefab generation support in NGO today.

My recommendation for your project would be to approach it using pre-created "blank Network Prefabs" that have the framework of what you would need to make specific 3D Mesh sets operate properly and then dynamically add the MeshRenderer and MeshFilter to existing or newly created GameObjects.

Side Note: Since you can have one (1) NetworkObject that has many nested NetworkTransforms, you would only need to create the "frame" for each type of 3D mesh that you need (i.e. if your 3D Meshes have complex hierarchies). Blank Network Prefab A

However, there is something that we don't recommend as it can get complex quickly but... 😸 you seem like you are adventurous...and it is really the only other thing I could think of that "may" be something you might look into...with caution!

Contemplate this: Once a NetworkObject (that could have many children with several NetworkBehaviours nested under it in children) is spawned... there is nothing saying that you can't --migrate-- the children to some completely different GameObject.

(Again we don't officially support this approach but it just so happens that it can be done...with a bunch of additional tracking code that can become complex quickly!)

You might spawn a "blank Network Prefab" that has many different NetworkBehaviours associated with it but only one NetworkObject at the root of the prefab.

So, once it is spawned... everything is "initialized" for NGO messaging and the like. If you were to migrate a child to a completely different GameObject then it would still send/receive messages and work as expected.

========================================

========================================

Again, this is not an officially supported (or recommended) approach as you still need to handle late joining clients. So, you would need a way to track what child from which instance was being used for a specific runtime loaded 3D Mesh. This information would need to be applied when the instances were spawned... which theoretically (again...not saying we recommend or support this approach) you could use the override void OnSynchronize<T> functionality to send this information needed to pull apart the spawned "blank Network Prefab" and use the "pieces/parts" of it for various things that you are loading during runtime. This would need to be synchronized to happen after the original "blank Network Prefab" was spawned. so there are some timing related things you would need to map out for this approach. Since we don't support this approach you could run into a scenario where a future update makes any works towards this obsolete/unusable (as another caution).

The recommend (and safest approach) is to create a set of "blank Network Prefabs" that you dynamically add the MeshFilter and MeshRenderer to once the 3D Mesh is loaded or that you parent the 3D mesh under a GameObject of the newly spawned "blank Network Prefab" instance. While this might seem time consuming it is worth contemplating the time that would take over the other possible alternatives and then make a decision from there.

I will go ahead and mark this issue for import, but I also wanted to make sure you understood the technical issues behind this particular limitation in NGO are "semi-hidden" as there is ILPP related things that have to execute ahead of time before entering into play mode/runtime so just exposing methods to assign a NetworkObjectId or NetwokrBehaviourId makes the source code look like it is "logically possible"...but that is slightly deceiving as there still is the code generation side of things that happens when you enter into play mode or make a stand alone build.

FreshlyBrewedCode commented 1 year ago

I've been working with NGO for several weeks now, and I would like to provide some additional thoughts on this issue along with the solution I ended up using.

Singleplayer vs. Multiplayer Consideration

One aspect that I have not mentioned in the original issue description is that any approach that involves spawning and working with network prefabs forces you to fully commit to NGO in your project. I am currently using a transport setup that requires me to have a separate server backend running in order to start an NGO session (even if you are the host). If I want to be able to run in "singleplayer" mode I would either have to run as host (which requires connecting to the backend), or I need to maintain a singleplayer spawning approach and a separate multiplayer spawning approach which can be difficult.

Solutions I Considered

The main problem I have with NGO is that on the client side NGO instantiates objects for you. This means it is very difficult to deal with dynamicly created NetworkObjects and NetworkBehaviors. In order to "connect" a NetworkObject (e.g. establish proper message routing between NetworkObject and NetworkBehaviors) all participants need to agree on what prefab to use beforehand, ideally at design time in the editor. On the serverside you can work around this by adding your network related components before invoking NetworkObject.Spawn. However, on the clientside this is not possible.

As far as I know, the only way to intercept the spawning process is by using a custom INetworkPrefabInstanceHandler. This looks like a perfect solution for advanced use cases, however, there is a big limitation. The prefab instance handler does not provide any option to pass user defined meta data. Let's say I spawn a NetworkObject on the serverside and I associate some custom meta data e.g. a custom ID for some advanced use case, there is no way for me to receive the custom ID before the object is spawned on the clientside. Even if I use a custom prefab instance handler, the only information available before spawning the object is the prefab, position, rotation, and owner id. This means I can not use my custom ID (or other custom meta data) to dynamically determine which object to spawn or which network components to include. Only after the object has been spawned I can sync the custom meta data (using e.g. NetworkVariable or OnSynchronize) but at this point I can not add any new network components.

TLDR: Having the option to include user defined parameters when spawning a NetworkObject that are passed to INetworkPrefabInstanceHandler would make it possible to support more advanced spawning use cases.

My Current Solution

The solution I ended up going with is rather simple. I moved all network components I need to a custom network prefab that is spawned on the serverside after my custom (non NGO ) spawning logic completes. In my custom, non-NGO spawning logic I generate a unique ID for the spawned object (I just use the path in the scene for now) and I assign that ID to a custom NetworkBehavior that sits on the network prefab. The NetworkBehavior syncs the id to all clients. When the custom, non-NGO spawning logic is invoked on a client I check if a network prefab with the matching custom ID has already been spawned by NGO (i.e. I check if my custom NetworkBehavior with the given ID already exists in the scene). Similarly, whenever the network prefab is spawned by NGO on the client side, I sync the custom ID from the serverside and I check if my non-NGO GameObject with the matching ID already exists in the scene. If a matching pair is found I fire an event to "assign" both objects to each other. In the assign event I can also run any additional code to "migrate" from plain GameObject to my network prefab.

NoelStephensUnity commented 1 year ago

@FreshlyBrewedCode Your solution is clever indeed! 💯