Skip to content

Add TargetTransform parameter to NetworkTransform #3436

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
marcusx2 opened this issue May 3, 2025 · 4 comments
Open

Add TargetTransform parameter to NetworkTransform #3436

marcusx2 opened this issue May 3, 2025 · 4 comments
Labels
stat:awaiting-response Awaiting response from author. This label should be added manually. type:feature New feature, request or improvement

Comments

@marcusx2
Copy link

marcusx2 commented May 3, 2025

Is your feature request related to a problem? Please describe.
Yes. Currently, NetworkTransform (and ClientNetworkTransform) desyncs if you disable the gameobject it is attached to. I am currently instead disabling the renderers, etc.

Describe the solution you'd like
If you add a TargetTransform serializable field, I can add the NetworkTransform script to a gameobject I won't disable. Then I can keep enabling/disabling the gameobject with the target transform, which is a lot easier. Right now I always have to attach the NetworkTransform to the gameobject with the transform I want to move, and a lot of scripts like rigidbody, colliders, etc are also attached to it and I can't disable the gameobject because of NetworkTransform, so instead I need to disable the behaviours, renderers, etc.

@marcusx2 marcusx2 added stat:awaiting-triage Status - Awaiting triage from the Netcode team. stat:reply-needed Awaiting reply from Unity account type:feature New feature, request or improvement labels May 3, 2025
@NoelStephensUnity
Copy link
Collaborator

The recommended way to handle this would be to have something like this:

  • Root GameObject (NetworkObject) (NetworkTransform)(Rigidbody)(NetworkRigidbody)
    • Child which could contain
      • Visuals (RenderMesh, etc)
      • Colliders
      • Etc.

This way you just need to disable the one Child while the root still can be manipulated and synch'd.

@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 May 5, 2025
@marcusx2
Copy link
Author

marcusx2 commented May 9, 2025

Yes, but implementing my feature request adds more flexibility that would be extremely useful. This way I don't have to remember this specific hierarchy late in development when I notice things breaking, and can make an adjustment that is easier. By not having a target transform, I am being forced to use a specific hierarchy structure.

Just a small vent, but this whole netcode thing where you can't disable the gameobject that it breaks things, desyncs, is just wrong. It goes against the way we code in Unity, and this is the biggest underlying issue that should be fixed. However, at least not forcing us to use a specific hierarchy structure alleviates the problem, even if just a little.

@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 May 9, 2025
@NoelStephensUnity
Copy link
Collaborator

NoelStephensUnity commented May 15, 2025

@marcusx2
Yeah, the fact that you didn't have documentation that provided some of these details is primarily my fault (due to lack of time in each day to get to all things). Having that information ahead of time would have helped prevent your situation... and I do feel that pain.

The target transform idea was contemplated but there started to be complexities in regards to parenting and it could become an order of operations issue without adding some pretty serious code analyzers and/or "rules" to check for "odd-ball" scenarios... as an example:

  • Root GameObject NetworkTransform (points to Child-D transform)
    • Child-A (NetworkTransform points to Child-C transform)
      • Child-B (NetworkTransform points to Root GameObject transform)
        • Child-C
          • Child-D
            With referencing there is the question as to whether you could point to a higher level in the hierarchy or not. Also, there becomes the issue of how to properly synchronize skewed parent-child transform references (i.e. The Root NetworkTransform synchronizes Child-D but Child-A synchronizes Child-C).

In the end the decision was to made to preserve the hierarchy and just use nested NetworkTransforms where the above would just look like:

  • Root GameObject (NetworkTransform)
    • Child-A
      • Child-B
        • Child-C (NetworkTransform)
          • Child-D (NetworkTransform)

Generally speaking, we recommend only disabling components and not disabling GameObjects since disabling GameObjects stops MonoBehaviour updates and is the more common way to handle pooled objects.

So....it was a decision based on what would be less problematic for all users.

However, knowing it can be a pain to try and add specific components and other UnityEngine.Object derived types(i.e. RenderMesh etc)... I went ahead and made you this nifty script that you can drop on the root of your network prefabs, add various component types (etc) to a list, and then just invoke a single method to enable or disable all of those components in one call.

using System.Collections.Generic;
using System.Reflection;
using UnityEngine;


public class ObjectController : MonoBehaviour
{
    public List<Object> Objects;
    public Dictionary<Object, PropertyInfo> ObjectProperties = new Dictionary<Object, PropertyInfo>();

    private void Awake()
    {
        var emptyEntries = 0;
        foreach (var someObject in Objects)
        {
            if (someObject == null)
            {
                emptyEntries++;
                continue;
            }
            var propertyInfo = someObject.GetType().GetProperty("enabled", BindingFlags.Instance | BindingFlags.Public);
            if (propertyInfo != null && propertyInfo.PropertyType == typeof(bool))
            {
                ObjectProperties.Add(someObject, propertyInfo);
            }
            else
            {
                Debug.LogWarning($"{name} does not contain a public enable property! (Ignoring)");
            }
        }
        if(emptyEntries > 0)
        {
            Debug.LogWarning($"{name} has {emptyEntries} emtpy(null) entries in the Objects list!");
        }
    }

    public void SetEnabled(bool isEnabled)
    {
        foreach (var someObject in Objects)
        {
            if (ObjectProperties.ContainsKey(someObject))
            {
                ObjectProperties[someObject].SetValue(someObject, isEnabled);
            }
        }
    }
}

I tested it with several different things:

  • NetworkBehaviour
  • MonoBehaviour
  • MeshRenderer
  • MeshFilter
  • Collider(s)

There may be one or two unique scenarios where you could make adjustments to the above and look for a Field as opposed to a property (or the like), but it covers a large portion of the typical types you would want to be able to enable and disable... and you don't need to have any specific property type references (the painful part)... just add it to a network prefab...add an entry to the Objects list and repeat until you have added everything you want to be able to enable or disable.

It might help reduce the pain of that process for you.
👍

@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 May 15, 2025
@NoelStephensUnity
Copy link
Collaborator

As a follow up to the above.
This version will synchronize the enabling and disabling while also synchronizing any late joining client based on the current state:

using System.Collections.Generic;
using System.Reflection;
using Unity.Netcode;
using UnityEngine;


public class ObjectControllerSynchronized : NetworkBehaviour
{
    public bool InitialState;
    public List<Object> Objects;
    public Dictionary<Object, PropertyInfo> ObjectProperties = new Dictionary<Object, PropertyInfo>();

    // Set read and write permissions based on who you want to be the authority of this
    private NetworkVariable<bool> m_IsEnabled = new NetworkVariable<bool>();

    private void Awake()
    {
        var emptyEntries = 0;
        foreach (var someObject in Objects)
        {
            if (someObject == null)
            {
                emptyEntries++;
                continue;
            }
            var propertyInfo = someObject.GetType().GetProperty("enabled", BindingFlags.Instance | BindingFlags.Public);
            if (propertyInfo != null && propertyInfo.PropertyType == typeof(bool))
            {
                ObjectProperties.Add(someObject, propertyInfo);
            }
            else
            {
                Debug.LogWarning($"{name} does not contain a public enable property! (Ignoring)");
            }
        }
        if(emptyEntries > 0)
        {
            Debug.LogWarning($"{name} has {emptyEntries} emtpy(null) entries in the Objects list!");
        }
    }

    /// <summary>
    /// Assumes NGO v2.x.x (you can replaced HasAuthority with IsServer, IsOwner, or the like for NGO v1.x.x)
    /// </summary>
    public override void OnNetworkSpawn()
    {
        if (HasAuthority)
        {
            m_IsEnabled.Value = InitialState;
        }
        base.OnNetworkSpawn();
    }

    protected override void OnNetworkPostSpawn()
    {
        if (!HasAuthority)
        {
            m_IsEnabled.OnValueChanged += OnEnabledChanged;
            SetEnabled(m_IsEnabled.Value);
        }
        base.OnNetworkPostSpawn();
    }

    public override void OnNetworkDespawn()
    {
        m_IsEnabled.OnValueChanged -= OnEnabledChanged;
        base.OnNetworkDespawn();
    }

    private void OnEnabledChanged(bool previous, bool current)
    {
        SetEnabled(current);
    }

    public void SetEnabled(bool isEnabled)
    {
        if (HasAuthority)
        {
            m_IsEnabled.Value = isEnabled;
        }
        foreach (var someObject in Objects)
        {
            if (ObjectProperties.ContainsKey(someObject))
            {
                ObjectProperties[someObject].SetValue(someObject, isEnabled);
            }
        }
    }
}

@michalChrobot michalChrobot removed the stat:awaiting-triage Status - Awaiting triage from the Netcode team. label May 19, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
stat:awaiting-response Awaiting response from author. This label should be added manually. type:feature New feature, request or improvement
Projects
None yet
Development

No branches or pull requests

3 participants