Skip to content

How do I force an update with ClientNetworkTransform or NetworkTransform? #3455

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
marcusx2 opened this issue May 21, 2025 · 2 comments
Closed
Labels
type:support Questions or other support

Comments

@marcusx2
Copy link

marcusx2 commented May 21, 2025

I have the following situation. I am using the Update loop to do some stuff, and I'm moving a network object with a clientnetworktransform. The states are supposed to synchronize by moving the transform itself, because the movement hits colliders and triggers behaviour.

My problem is that the ClientNetworkTransform is not always synchronized across clients. Sometimes a trigger doesn't fire on a client because the object is not on the place it's supposed to be. It can be slightly off.

My question is: Is there a way to force the transform synchronization? Say, I hit a collider with the transform. As the owner of the networkobject, I want to send an rpc to all clients to move the tranform to that position. The owner keeps moving the transform, but I want to guarantee that a specific position happens across clients, so they all trigger the collider.

What is the proper way of dealing with this?

@marcusx2 marcusx2 added the type:support Questions or other support label May 21, 2025
@NoelStephensUnity
Copy link
Collaborator

NoelStephensUnity commented May 21, 2025

You can use NetworkTransform.SetState to apply state directly.
With the trigger, I would recommend:

  • Exiting early if not the authority.
  • When triggered by the authority, invoke an RPC.
    • Pass the position of the authority player instance.
    • Start a coroutine or set a flag that checks the position of the non-authority instance(s) that received the RPC until the non-authority instance is "approximately" the save position (don't try to match 1:1 as floating point values will not always be precise... like 1/1000th of each axial value is typically fine).
      • Upon reaching the destination, the non-authority instances invokes the same logic that is invoked when the owner triggered it.

It really requires two components:

/// <summary>
/// Placed on object with trigger
/// </summary>
public class TriggerObject : NetworkBehaviour
{
    private TagHandle m_TagHandle;

    private void Awake()
    {
        // Add a "Player" tag to your players to help with filtering out
        // unwanted triggers.
        m_TagHandle = TagHandle.GetExistingTag("Player");
    }

    private void OnTriggerEnter(Collider other)
    {
        if (!other.CompareTag(m_TagHandle))
        {
            return;
        }
        var playerNetworkTransform = other.gameObject.GetComponent<PlayerNetworkTransform>();
        if (playerNetworkTransform)
        {
            playerNetworkTransform.HandleTrigger(other, this);
        }
    }
}

/// <summary>
/// Placed on the Player prefab.
/// Will delay trigger until the non-authority instance(s) have
/// interpolated to the point where the authority entered the
/// trigger.
/// </summary>
public class PlayerNetworkTransform : NetworkTransform
{
    public float DeferTriggerTimeout = 5.0f;
    public bool TriggerOnTimeout = false;
    public float PositionPrecision = 1E-04F;

    private struct DeferredTrigger
    {
        public ulong ClientThatTriggered;
        public float TimeOut;
        public TriggerObject TriggerObject;
        public Vector3 TriggerPosition;
    }

    private List<DeferredTrigger> m_DeferredTriggers = new List<DeferredTrigger>();

    /// <summary>
    /// One way to make this owner/client authoritative
    /// </summary>
    protected override bool OnIsServerAuthoritative()
    {
        return false;
    }

    public void HandleTrigger(Collider other, TriggerObject triggerObject)
    {
        // Non-authority exits early
        if (!CanCommitToTransform)
        {
            return;
        }

        // Send notification to all non-authority instances
        OnTriggerEnterRpc(new NetworkBehaviourReference(triggerObject), transform.position);
        // Handle trigger locally
        OnTriggerEntered(triggerObject, NetworkManager.LocalClientId);
    }

    /// <summary>
    /// The script that normally you would place within the OnTriggerEnter method
    /// </summary>
    /// <param name="triggerObject">object that triggered </param>
    /// <param name="clientThatTriggered"></param>
    protected virtual void OnTriggerEntered(TriggerObject triggerObject, ulong clientThatTriggered)
    {
        // Place the script you would normally have within OnTriggerEnter here
    }

    [Rpc(SendTo.NotMe)]
    private void OnTriggerEnterRpc(NetworkBehaviourReference triggerObjectReference,
        Vector3 triggerPosition, RpcParams rpcParams = default)
    {
        // Add to the list of deferred triggers
        if (triggerObjectReference.TryGet<TriggerObject>(out var triggerObject))
        {
            m_DeferredTriggers.Add(new DeferredTrigger()
            {
                TimeOut = Time.realtimeSinceStartup + DeferTriggerTimeout,
                TriggerObject = triggerObject,
                TriggerPosition = triggerPosition,
                ClientThatTriggered = rpcParams.Receive.SenderClientId
            });
        }
    }

    /// <summary>
    /// Handles processing the deferred triggers
    /// </summary>
    private void ProcessDeferredTriggers()
    {
        if (m_DeferredTriggers.Count == 0)
        {
            return;
        }
        for (int i = m_DeferredTriggers.Count - 1; i >= 0; i--)
        {
            var deferredTrigger = m_DeferredTriggers[i];
            var isApproximately = IsApproximately(transform.position, deferredTrigger.TriggerPosition);
            var timedOut = deferredTrigger.TimeOut < Time.realtimeSinceStartup;

            if (isApproximately || timedOut)
            {
                if (isApproximately || (!isApproximately && TriggerOnTimeout && timedOut))
                {
                    OnTriggerEntered(deferredTrigger.TriggerObject, deferredTrigger.ClientThatTriggered);
                }
                m_DeferredTriggers.RemoveAt(i);
            }
        }
    }

    /// <summary>
    /// Will be continuously invoked on non-authority instances
    /// </summary>
    public override void OnUpdate()
    {
        base.OnUpdate();

        ProcessDeferredTriggers();
    }

    /// <summary>
    /// Generic Vector3 approximately method
    /// </summary>
    private bool IsApproximately(Vector3 first, Vector3 second)
    {
        return System.Math.Round(Mathf.Abs(first.x - second.x), 2) <= PositionPrecision &&
            System.Math.Round(Mathf.Abs(first.y - second.y), 2) <= PositionPrecision &&
            System.Math.Round(Mathf.Abs(first.z - second.z), 2) <= PositionPrecision;
    }
}

The TriggerObject is what has the trigger on it (unless it is on the player...then you would just migrate the TriggerObject .OnTriggerEntered into the PlayerNetworkTransform...but you still want the TriggerObject in order to know "what thing" was causing the trigger) and the PlayerNetworkTransform handles deferring the trigger on non-authority instances until they "catch-up" (i.e. finish interpolating) to the authority's position when it caused the trigger...to trigger.

Something like the above should assure that the trigger logic is invoked on all instances when they reach the point where the trigger should "trigger". To get a "perfect" synchronized triggering (and be visually correct on all clients without the delay due to latency) requires prediction of movement...which that is a much more complex problem to solve for...I would start with something like the above and see if it works for you. Obviously, add to and/or modify as you need... but something like the above should do the trick.

I am going to close this support ticket out as I believe this should give you a reasonable starting point.

@marcusx2
Copy link
Author

@NoelStephensUnity Please consider adding a sample that involves this to the SDK.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type:support Questions or other support
Projects
None yet
Development

No branches or pull requests

2 participants