using System.Collections; using Unity.Netcode; using UnityEngine; using UnityEngine.XR.Interaction.Toolkit; using UnityEngine.XR.Interaction.Toolkit.Interactables; using UnityEngine.XR.Interaction.Toolkit.Interactors; namespace XRMultiplayer { /// /// NetworkInteractableGrab adds some extra functionality to the NetworkBaseInteractable class /// to allow for better immediate feedback on non owner players. /// [RequireComponent(typeof(Rigidbody), typeof(ClientNetworkTransform))] public class NetworkPhysicsInteractable : NetworkBaseInteractable { /// /// Enabling this will call Request Ownership on collision with non-local NetworkPhysicsInteractables. /// [Header("Collision Based Ownership Transfer"), SerializeField, Tooltip("Enabling this will call Request Ownership on collision with non-local NetworkPhysicsInteractables.")] protected bool m_AllowCollisionOwnershipExchange = true; /// /// Determines the minimum velocity magnitude required to request ownership on collision. /// [SerializeField, Tooltip("Determines the minimum velocity magnitude required to request ownership on collision.")] protected float m_MinExchangeVelocityMagitude = .025f; /// /// Sets the to on spawn. /// /// /// This is useful for objects that you want to be non interactable on spawn. /// [Header("Spawn Options")] public bool spawnLocked = true; /// /// Used to get the current networked value of . /// public bool lockedOnSpawn => m_LockedOnSpawn.Value; protected NetworkVariable m_LockedOnSpawn = new(true, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner); /// /// This flag is used to prevent multiple attempts at requesting ownership. /// protected bool m_RequestingOwnership = false; /// /// Client Network Transform used for synchronizing transform data. /// protected ClientNetworkTransform m_ClientNetworkTransform; protected Rigidbody m_Rigidbody; protected Collider m_Collider; /// /// This flag is used to determine if the object should be reset on disconnect. /// protected NetworkVariable m_ResettingObject = new(false, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner); /// /// Coroutine for checking ownership after a request has been made based on current user RTT to server. /// protected IEnumerator checkOwnershipRoutine; bool m_PauseVelocityCalcuations = false; Vector3 m_AverageVelocity; Vector3[] m_CurrentCalculatedVelocity; int m_FramesToCalculate = 3; int m_CurrentFrame = 0; Vector3 m_PrevPos; /// public override void Awake() { base.Awake(); // Get associated required components if (!TryGetComponent(out m_Rigidbody) || !TryGetComponent(out m_ClientNetworkTransform)) { Utils.Log("Missing Components! Disabling Now.", 2); enabled = false; return; } m_CurrentCalculatedVelocity = new Vector3[m_FramesToCalculate]; m_ClientNetworkTransform.enabled = false; m_Collider = GetComponentInChildren(); } void Update() { if (m_PauseVelocityCalcuations) return; Vector3 velocity = (transform.position - m_PrevPos) / Time.deltaTime; m_CurrentCalculatedVelocity[m_CurrentFrame] = velocity; m_CurrentFrame = (m_CurrentFrame + 1) % m_FramesToCalculate; m_PrevPos = transform.position; m_AverageVelocity = GetWorldVelocity(); } Vector3 GetWorldVelocity() { Vector3 averageVelocity = Vector3.zero; for (int i = 0; i < m_FramesToCalculate; i++) { averageVelocity += m_CurrentCalculatedVelocity[i]; } return averageVelocity / m_FramesToCalculate; } /// public override void OnNetworkSpawn() { base.OnNetworkSpawn(); m_ResettingObject.OnValueChanged += OnObjectPhysicsReset; if (IsOwner) { m_LockedOnSpawn.Value = spawnLocked; m_Rigidbody.constraints = spawnLocked ? RigidbodyConstraints.FreezeAll : RigidbodyConstraints.None; } m_ClientNetworkTransform.enabled = syncSelect; } /// public override void OnNetworkDespawn() { base.OnNetworkDespawn(); if (m_ResetObjectOnDisconnect & !m_Rigidbody.isKinematic) { ResetObjectPhysics(); } } /// protected override void OnIsInteractingChanged(bool oldValue, bool newValue) { base.OnIsInteractingChanged(oldValue, newValue); if (IsOwner && newValue && m_LockedOnSpawn.Value) { m_LockedOnSpawn.Value = false; } if (!newValue) { m_Collider.enabled = false; m_Collider.enabled = true; } } /// /// This function is called when the value changes. /// void OnObjectPhysicsReset(bool oldValue, bool currentValue) { if (currentValue) { m_CurrentCalculatedVelocity = new Vector3[m_FramesToCalculate]; m_PauseVelocityCalcuations = true; } else { m_PauseVelocityCalcuations = false; } } /// public override void ResetObject() { base.ResetObject(); ResetObjectPhysics(); } /// /// Resets the object to its original state. /// public void ResetObjectPhysics() { m_CurrentCalculatedVelocity = new Vector3[m_FramesToCalculate]; // Take a snapshot of the current rigidbody int snapShotRigidbodyInterpolation = (int)m_Rigidbody.interpolation; bool wasKinematic = m_Rigidbody.isKinematic; if (!m_Rigidbody.isKinematic) { m_Rigidbody.linearVelocity = Vector3.zero; m_Rigidbody.angularVelocity = Vector3.zero; } m_Rigidbody.interpolation = RigidbodyInterpolation.None; m_Rigidbody.isKinematic = true; if (IsOwner && NetworkManager.IsConnectedClient & !NetworkManager.Singleton.ShutdownInProgress) m_ResettingObject.Value = true; // Wait for a fixed update to reset the object. StartCoroutine(ResetPhysicsRoutine(wasKinematic, snapShotRigidbodyInterpolation)); } IEnumerator ResetPhysicsRoutine(bool wasKinematic, int interpolation) { // Since the Rigidbody is using Interpolate, we need to disable Kinematic for a frame to reset the object. yield return new WaitForFixedUpdate(); m_Rigidbody.interpolation = (RigidbodyInterpolation)interpolation; m_Rigidbody.isKinematic = wasKinematic; if (IsOwner && NetworkManager.IsConnectedClient & !NetworkManager.Singleton.ShutdownInProgress) m_ResettingObject.Value = false; } /// public override void OnSelectEnteredLocal(BaseInteractionEventArgs args) { base.OnSelectEnteredLocal(args); // Return out early if the interactor is ignoring sockets or not syncing select. if (m_IgnoreSocketSelectedCallback && args.interactorObject.transform.GetComponent() != null) return; // Disable the network transform to allow smooth interaction with high latency and wait for ownership or timeout to re-enable. if (CanHold() & !IsOwner) { m_ClientNetworkTransform.enabled = false; } } /// public override void OnSelectExitedLocal(BaseInteractionEventArgs args) { base.OnSelectExitedLocal(args); // Return out early if the interactor is ignoring sockets or not syncing select. if (m_IgnoreSocketSelectedCallback && args.interactorObject.transform.GetComponent() != null) return; // Check if still holding with other hand. if (m_BaseInteractable.isSelected) return; if (!IsOwner) { // Enable Network Transform if releasing the object before ownership has been gained. m_ClientNetworkTransform.enabled = true; } if (IsOwner) { // Check for interactable type and update kinematic state on release. if (baseInteractable.GetType() == typeof(XRGrabInteractable)) { if (((XRGrabInteractable)baseInteractable).movementType == XRBaseInteractable.MovementType.VelocityTracking || ((UnityEngine.XR.Interaction.Toolkit.Interactables.XRGrabInteractable)baseInteractable).throwOnDetach) { m_Rigidbody.isKinematic = false; } } } } /// /// Override for enabling the NetworkTransform once ownership has been gained. /// public override void OnGainedOwnership() { base.OnGainedOwnership(); if (IsOwner) { m_RequestingOwnership = false; m_ClientNetworkTransform.enabled = true; m_IsInteracting.Value = baseInteractable.isSelected; if (!baseInteractable.isSelected & !m_Rigidbody.isKinematic) { m_Rigidbody.linearVelocity = m_AverageVelocity; } } } /// /// If Ownership was lost due to another player taking over, make sure we cancel all ownership requests and re-enable the ClientNetworkTransform. /// public override void OnLostOwnership() { base.OnLostOwnership(); if (checkOwnershipRoutine != null) StopCoroutine(checkOwnershipRoutine); m_RequestingOwnership = false; m_ClientNetworkTransform.enabled = true; } /// void OnCollisionEnter(Collision collision) { if (!IsOwner || !m_AllowCollisionOwnershipExchange) return; NetworkPhysicsInteractable networkPhysicsInteractable = collision.transform.GetComponentInParent(); if (networkPhysicsInteractable != null && (isInteracting || IsMovingFaster(networkPhysicsInteractable.m_Rigidbody))) { networkPhysicsInteractable.RequestOwnership(); } } /// /// Checks if the current object is moving faster than the other object based on velocity magnitude. /// /// The Other Rigidbody you are colliding with /// Returns true if this object is moving faster than the and the object we are hitting. protected bool IsMovingFaster(Rigidbody otherBody) { return m_Rigidbody.linearVelocity.magnitude > m_MinExchangeVelocityMagitude && m_Rigidbody.linearVelocity.magnitude > otherBody.linearVelocity.magnitude; } /// /// Checks if ownership transfer is blocked by current conditions. /// /// public bool OwnershipTransferBlocked() { return isInteracting || IsOwner || m_RequestingOwnership || m_LockedOnSpawn.Value || !m_AllowCollisionOwnershipExchange || !NetworkObject.IsSpawned || baseInteractable.isSelected || m_ResettingObject.Value; } /// /// This function will request ownership of this object. /// This will disable the to allow for smooth interaction. /// This will also set the flag to true. /// This will also start a Coroutine to check if ownership was not granted based on the current RTT to the server. /// public void RequestOwnership() { if (IsOwner || OwnershipTransferBlocked()) return; m_RequestingOwnership = true; m_ClientNetworkTransform.enabled = false; if (!m_Rigidbody.isKinematic) { m_Rigidbody.linearVelocity = m_AverageVelocity; } if (((XRGrabInteractable)baseInteractable).movementType != XRBaseInteractable.MovementType.Kinematic) { m_Rigidbody.isKinematic = false; } RelinquishOwnershipAfterTime(); RequestOwnershipRpc(NetworkManager.Singleton.LocalClientId); if (checkOwnershipRoutine != null) StopCoroutine(checkOwnershipRoutine); checkOwnershipRoutine = CheckOwnershipRoutine(); StartCoroutine(checkOwnershipRoutine); } [Rpc(SendTo.Server)] void RequestOwnershipRpc(ulong clientId) { NetworkObject.ChangeOwnership(clientId); } /// /// Coroutine to check if ownership was granted based on the current RTT to the server. /// /// IEnumerator CheckOwnershipRoutine() { // Get the current RTT to the server and wait for twice that time before checking if ownership was granted. float waitTime = NetworkManager.Singleton.NetworkConfig.NetworkTransport.GetCurrentRtt(NetworkManager.ServerClientId) * 2; if (waitTime < .025f || waitTime > 5.0f) { waitTime = 1.0f; } yield return new WaitForSeconds(waitTime); if (!IsOwner) { Utils.Log($"Ownership Request Timed Out on Object {gameObject.name}"); } m_RequestingOwnership = false; m_ClientNetworkTransform.enabled = true; } } }