using UnityEngine; using Unity.Netcode; using UnityEngine.XR.Interaction.Toolkit; using UnityEngine.Events; using UnityEngine.XR.Interaction.Toolkit.Interactables; using UnityEngine.XR.Interaction.Toolkit.Interactors; using UnityEngine.XR.Interaction.Toolkit.Filtering; using UnityEngine.XR.Interaction.Toolkit.AffordanceSystem.State; using System; using System.Collections; #if UNITY_EDITOR using UnityEditor; #endif namespace XRMultiplayer { /// /// NetworkInteractableBase class synchronizes the events over the network. /// Options are exposed to determine which functionality you want to syncrhonize. /// /// /// This is meant to be a parent class that handles the core networking functionality. /// Classes can interhit from this class and override where applicable. /// See for an example of how to extend this class. /// [RequireComponent(typeof(XRBaseInteractable))] [DisallowMultipleComponent] public class NetworkBaseInteractable : NetworkBehaviour, IXRSelectFilter, IXRHoverFilter { /// /// Allow users to take ownership of currently controlled objects. /// public bool allowOverrideOwnership { get => m_AllowOverrideOwnership; set => m_AllowOverrideOwnership = value; } [Header("General Options"), SerializeField, Tooltip("Allow users to take ownership of currently controlled objects")] protected bool m_AllowOverrideOwnership = false; /// /// Amount of time before checking for false positives for the object interaction state /// public float interactionCheckTime { get => m_InteractionCheckTime; set => m_InteractionCheckTime = value; } [SerializeField, Tooltip("Amount of time before checking for false positives of the object interaction state.")] protected float m_InteractionCheckTime = 2.0f; /// /// Ignore Socket Interaction /// public bool ignoreSocketSelectedCallback { get => m_IgnoreSocketSelectedCallback; set => m_IgnoreSocketSelectedCallback = value; } [SerializeField, Tooltip("Ignore Socket Interaction")] protected bool m_IgnoreSocketSelectedCallback = true; /// /// Resets the object position, scale, and rotation on disconnect. /// public bool resetObjectOnDisconenct { get => m_ResetObjectOnDisconnect; set => m_ResetObjectOnDisconnect = value; } [SerializeField, Tooltip("Reset object on disconnect")] protected bool m_ResetObjectOnDisconnect = true; /// /// Amount of time before relinquishing ownership of the object back to the host. /// public bool relinquishOwnershipAfterTime { get => m_RelinquishOwnershipAfterTime; set => m_RelinquishOwnershipAfterTime = value; } [Header("Ownership Relinquish"), SerializeField, Tooltip("Should we relinquish ownership back to the room host after a set amount of time?")] protected bool m_RelinquishOwnershipAfterTime = true; /// /// Amount of time before relinquishing ownership of the object back to the host. /// public float relinquishOwnershipTime { get => m_RelinquishOwnershipTime; set => m_RelinquishOwnershipTime = value; } [SerializeField, Tooltip("Amount of time before relinquishing ownership of the object back to the host.")] protected float m_RelinquishOwnershipTime = 5.0f; /// /// Gets the current state of an object being interacted over the network. /// public bool isInteracting { get => m_IsInteracting.Value; } /// /// Syncs the current state of being interacted with or not. /// Prevents users from taking control of currently controlled objects, unless is true. /// /// /// will allow users to bypass this value and take ownership from other players. /// protected NetworkVariable m_IsInteracting = new(false, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner); [HideInInspector, SerializeField, Tooltip("Use a Unity Event for a callback when the IsInteracting value changes.")] //Disabling warning for unused variable since it's used in the editor script #pragma warning disable 0414 bool m_UseInteractingChangedEvent = false; #pragma warning restore 0414 [HideInInspector] public UnityEvent OnInteractingChanged; /// /// Sync Hover interaction over network. /// public bool syncHover { get => m_SyncHover; set => m_SyncHover = value; } [HideInInspector, SerializeField, Tooltip("Sync Hover interaction over network")] protected bool m_SyncHover = false; [HideInInspector, SerializeField, Tooltip("Use Unity Events for Networked Hooks")] protected bool m_UseHoverEvents = false; [HideInInspector] public UnityEvent HoverNetworkedEventServer; [HideInInspector] public UnityEvent HoverNetworkedEventAll; /// /// Sync Select interaction over network. /// public bool syncSelect { get => m_SyncSelect; set => m_SyncSelect = value; } [HideInInspector, SerializeField, Tooltip("Sync Select interaction over network")] protected bool m_SyncSelect = true; [HideInInspector, SerializeField, Tooltip("Use Unity Events for Networked Hooks")] protected bool m_UseSelectEvents = false; [HideInInspector] public UnityEvent SelectNetworkedEventServer; [HideInInspector] public UnityEvent SelectNetworkedEventAll; /// /// Sync Activate interaction over network. /// public bool syncActivate { get => m_SyncActivate; set => m_SyncActivate = value; } [HideInInspector, SerializeField, Tooltip("Sync Activate interaction over network")] protected bool m_SyncActivate = true; [HideInInspector, SerializeField, Tooltip("Use Unity Events for Networked Hooks")] protected bool m_UseActivateEvents = false; [HideInInspector] public UnityEvent ActivateNetworkedEventServer; [HideInInspector] public UnityEvent ActivateNetworkedEventAll; /// /// Base Interactable used for syncing events. /// public XRBaseInteractable baseInteractable { get => m_BaseInteractable; set => m_BaseInteractable = value; } protected XRBaseInteractable m_BaseInteractable; public bool canProcess => isActiveAndEnabled; /// /// Starting Pose for the object transform. /// protected Pose m_OriginalPose; /// /// Starting scale for the object transform. /// protected Vector3 m_OriginalScale; protected XRInteractionManager m_InteractionManager; #pragma warning disable CS0618 // Type or member is obsolete protected BaseAffordanceStateProvider m_AffordanceStateProvider; #pragma warning restore CS0618 // Type or member is obsolete #if UNITY_EDITOR /// /// Foldout states for the editor. /// [HideInInspector, SerializeField] bool[] m_FoldoutValues = {true, true, true}; #endif /// /// After a set amount of time, relinquish ownership of the object back to the host. /// IEnumerator m_RelinquishToHostEnumerator; /// /// Check for false positives of the object being interacted with. /// IEnumerator m_HostInteractionCheckEnumerator; /// public virtual void Awake() { // Get associated components if (!TryGetComponent(out m_BaseInteractable)) { Utils.Log("Missing Components! Disabling Now.", 2); enabled = false; return; } m_BaseInteractable.selectFilters.Add(this); m_BaseInteractable.hoverFilters.Add(this); m_InteractionManager = FindFirstObjectByType(); #pragma warning disable CS0618 // Type or member is obsolete m_AffordanceStateProvider = GetComponentInChildren(); #pragma warning restore CS0618 // Type or member is obsolete } /// private void OnEnable() { // Set initial pose and scale m_OriginalPose.position = transform.position; m_OriginalPose.rotation = transform.rotation; m_OriginalScale = transform.localScale; SetupListeners(true); } /// private void OnDisable() { SetupListeners(false); } /// /// Handles the listeners for the interactable events. /// /// /// Whether or not we are adding or removing the listeners. /// void SetupListeners(bool setup) { if (setup) { if (baseInteractable != null) { // Add all the listeners for interactable events baseInteractable.hoverEntered.AddListener(OnHoverEnterLocal); baseInteractable.hoverExited.AddListener(OnHoverExitLocal); baseInteractable.selectEntered.AddListener(OnSelectEnteredLocal); baseInteractable.selectExited.AddListener(OnSelectExitedLocal); baseInteractable.activated.AddListener(OnActivateLocal); baseInteractable.deactivated.AddListener(OnDeactivateLocal); } } else { if (baseInteractable != null) { // Removes listeners from the interactable events baseInteractable.hoverEntered.RemoveListener(OnHoverEnterLocal); baseInteractable.hoverExited.RemoveListener(OnHoverExitLocal); baseInteractable.selectEntered.RemoveListener(OnSelectEnteredLocal); baseInteractable.selectExited.RemoveListener(OnSelectExitedLocal); baseInteractable.activated.RemoveListener(OnActivateLocal); baseInteractable.deactivated.RemoveListener(OnDeactivateLocal); } } } /// public override void OnNetworkSpawn() { base.OnNetworkSpawn(); NetworkObject.DontDestroyWithOwner = true; m_IsInteracting.OnValueChanged += OnIsInteractingChanged; if (IsOwner) m_IsInteracting.Value = false; if (m_ResetObjectOnDisconnect) { ResetObject(); } } /// public override void OnNetworkDespawn() { base.OnNetworkDespawn(); m_IsInteracting.OnValueChanged -= OnIsInteractingChanged; if (IsOwner) m_IsInteracting.Value = false; if (m_ResetObjectOnDisconnect) { ResetObject(); } } /// /// Resets the object position, scale, and rotation based on the original pose determined in /// public virtual void ResetObject() { transform.SetPositionAndRotation(m_OriginalPose.position, m_OriginalPose.rotation); transform.localScale = m_OriginalScale; } /// /// Callback for the Hover Enter event executed for the local user. /// /// public virtual void OnHoverEnterLocal(BaseInteractionEventArgs args) { Hovered(true); if (syncHover) { OnHoverServerRpc(true, NetworkManager.Singleton.LocalClientId); } } /// /// Callback for the Hover Exit event executed for the local user. /// /// public virtual void OnHoverExitLocal(BaseInteractionEventArgs args) { Hovered(false); if (syncHover) { OnHoverServerRpc(false, NetworkManager.Singleton.LocalClientId); } } /// /// Hover event executed on the Server. /// /// True if hover entered, False if hover exited. /// ClientId who sent the RPC. [ServerRpc(RequireOwnership = false)] public virtual void OnHoverServerRpc(bool entered, ulong clientId) { OnHoverClientRpc(entered, clientId); if (m_UseHoverEvents) HoverNetworkedEventServer.Invoke(entered); } /// /// Hover event executed on all clients. /// /// True if hover entered, False if hover exited. /// ClientId who sent the RPC. [ClientRpc] public virtual void OnHoverClientRpc(bool entered, ulong clientId) { if (clientId != NetworkManager.Singleton.LocalClientId) { Hovered(entered); if (m_AffordanceStateProvider != null) { #pragma warning disable CS0618 // Type or member is obsolete m_AffordanceStateProvider.UpdateAffordanceState(new AffordanceStateData(Convert.ToByte(entered ? 2 : (isInteracting ? 4 : 0)), 1.0f)); #pragma warning restore CS0618 // Type or member is obsolete } } } /// /// This function gets called immediately for the local user, /// and gets called remotely from the server on all clients. /// /// True if hover entered, False if hover exited. public virtual void Hovered(bool entered) { if (m_UseHoverEvents) HoverNetworkedEventAll.Invoke(entered); } /// /// Callback for the Select Enter event executed for the local user. /// /// public virtual void OnSelectEnteredLocal(BaseInteractionEventArgs args) { // Return out early if the interactor is ignoring sockets or not syncing select. if (m_IgnoreSocketSelectedCallback && args.interactorObject.transform.GetComponent() != null) return; if (CanHold()) { Selected(true); if (syncSelect) { OnSelectServerRpc(true, NetworkManager.Singleton.LocalClientId); } // If already the owner, set the network variable for isHeld if (IsOwner) { m_IsInteracting.Value = true; } } } /// /// Callback for the Select Exit event executed for the local user. /// /// public virtual void OnSelectExitedLocal(BaseInteractionEventArgs 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; // Check that it is still a spawned object. Select will fire on object destruction. if (!IsSpawned) return; Selected(false); if (syncSelect) { OnSelectServerRpc(false, NetworkManager.Singleton.LocalClientId); } // If still the owner, set the network variable for isHeld if (IsOwner) { m_IsInteracting.Value = false; RelinquishOwnershipAfterTime(); } } [ServerRpc(RequireOwnership = false)] void ResetObjectToHostServerRpc() { if (NetworkObject.OwnerClientId != NetworkManager.Singleton.LocalClientId) NetworkObject.ChangeOwnership(NetworkManager.Singleton.LocalClientId); } /// /// Select event executed on the Server. /// /// True if select entered, False if select exited. /// ClientId who sent the RPC. [ServerRpc(RequireOwnership = false)] public virtual void OnSelectServerRpc(bool selected, ulong clientId) { OnSelectClientRpc(selected, clientId); // If we are not the owner and we are selecting the object, request to change ownership if (selected && OwnerClientId != clientId) { NetworkObject.ChangeOwnership(clientId); } SelectNetworkedEventServer.Invoke(selected); } /// /// Select event executed on all clients. /// /// True if select entered, False if select exited. /// ClientId who sent the RPC. [ClientRpc] public virtual void OnSelectClientRpc(bool selected, ulong clientId) { if (clientId != NetworkManager.Singleton.LocalClientId) { Selected(selected); } } /// /// This function gets called immediately for the local user, /// and gets called remotely from the server on all clients. /// /// Whether or not selected was called. public virtual void Selected(bool selected) { } /// /// Callback for the Activate event executed for the local user. /// /// public virtual void OnActivateLocal(BaseInteractionEventArgs args) { if (!IsOwner) return; Activated(true); if (syncActivate) { OnActivateServerRpc(true, NetworkManager.Singleton.LocalClientId); } } /// /// Callback for the Deactivate event executed for the local user. /// /// public virtual void OnDeactivateLocal(BaseInteractionEventArgs args) { if (!IsOwner) return; Activated(false); if (syncActivate) { OnActivateServerRpc(false, NetworkManager.Singleton.LocalClientId); } } /// /// Activate event executed on the Server. /// /// True if activated, False if Deactivated. /// ClientId who sent the RPC. [ServerRpc(RequireOwnership = false)] public virtual void OnActivateServerRpc(bool activate, ulong clientId) { OnActivateClientRpc(activate, clientId); if (m_UseActivateEvents) ActivateNetworkedEventServer.Invoke(activate); } /// /// Activate event executed on all clients. /// /// True if activated, False if Deactivated. /// ClientId who sent the RPC. [ClientRpc] public virtual void OnActivateClientRpc(bool activate, ulong clientId) { if (clientId != NetworkManager.Singleton.LocalClientId) { Activated(activate); if (m_AffordanceStateProvider != null) { #pragma warning disable CS0618 // Type or member is obsolete m_AffordanceStateProvider.UpdateAffordanceState(new AffordanceStateData(Convert.ToByte(activate ? 5 : isInteracting ? 4 : 0), 1.0f)); #pragma warning restore CS0618 // Type or member is obsolete } } } /// /// This function gets called immediately for the local user, /// and gets called remotely from the server on all clients. /// /// True if activated, False if Deactivated. public virtual void Activated(bool activate) { if (m_UseActivateEvents) ActivateNetworkedEventAll.Invoke(activate); } /// /// Checks if another user can hold or pickup an interactable. /// /// protected virtual bool CanHold() { return !isInteracting || allowOverrideOwnership; } /// /// See . /// public override void OnGainedOwnership() { base.OnGainedOwnership(); // Check for gaining ownership of an object when a player disconnects if (IsOwner && IsServer && isInteracting & !baseInteractable.isSelected) { m_IsInteracting.Value = false; } // Workaround for NGO calling this always on Server, even if Server is not owner. // So we check IsOwner and Interactable selected state, and loop through to check for sockets. if (IsOwner) { if (m_HostInteractionCheckEnumerator != null) StopCoroutine(m_HostInteractionCheckEnumerator); m_HostInteractionCheckEnumerator = CheckForOwnerInteraction(); StartCoroutine(m_HostInteractionCheckEnumerator); if (m_RelinquishToHostEnumerator != null) StopCoroutine(m_RelinquishToHostEnumerator); if (baseInteractable.isSelected & !isInteracting) { if (!IsSelectedBySocket()) { m_IsInteracting.Value = true; } } } } /// /// See . /// public override void OnLostOwnership() { base.OnLostOwnership(); // Have to check ownership since this is always called on the Server if (!IsOwner) { if (m_HostInteractionCheckEnumerator != null) StopCoroutine(m_HostInteractionCheckEnumerator); if (m_RelinquishToHostEnumerator != null) StopCoroutine(m_RelinquishToHostEnumerator); if (baseInteractable.isSelected) m_InteractionManager.CancelInteractableSelection((IXRSelectInteractable)baseInteractable); } } /// /// Checks every for false positives of the object being interacted with. /// IEnumerator CheckForOwnerInteraction() { while (IsOwner) { if (isInteracting) { if (!baseInteractable.isSelected || IsSelectedBySocket()) { Utils.Log($"Interacting is true and either selected is false or selected is a socket on object {gameObject.name}. Is this intentional?"); m_IsInteracting.Value = false; } } yield return new WaitForSeconds(interactionCheckTime); } } /// /// Checks if the object is selected by a socket. /// bool IsSelectedBySocket() { if (baseInteractable.isSelected) { foreach (var interactor in baseInteractable.interactorsSelecting) { if (interactor is XRSocketInteractor) { return true; } } } return false; } /// /// Callback for anytime the value changes. /// /// /// protected virtual void OnIsInteractingChanged(bool oldValue, bool newValue) { OnInteractingChanged.Invoke(newValue); if (m_AffordanceStateProvider != null) { #pragma warning disable CS0618 // Type or member is obsolete m_AffordanceStateProvider.UpdateAffordanceState(new AffordanceStateData(Convert.ToByte(newValue ? 4 : 0), 1.0f)); #pragma warning restore CS0618 // Type or member is obsolete } SelectNetworkedEventAll.Invoke(newValue); // If we are interacting and the owner, stop the coroutine to relinquish ownership if (newValue && IsOwner && m_RelinquishToHostEnumerator != null) { StopCoroutine(m_RelinquishToHostEnumerator); } } /// /// Relinquish ownership of the object back to the host after a set amount of time. /// protected void RelinquishOwnershipAfterTime() { if (!m_RelinquishOwnershipAfterTime) return; if (m_RelinquishToHostEnumerator != null) StopCoroutine(m_RelinquishToHostEnumerator); m_RelinquishToHostEnumerator = RelinquishOwnershipToHost(); StartCoroutine(m_RelinquishToHostEnumerator); } /// /// Coroutine to relinquish ownership of the object back to the host after a set amount of time. /// IEnumerator RelinquishOwnershipToHost() { yield return new WaitForSeconds(relinquishOwnershipTime); if (!IsServer && !baseInteractable.isSelected) { ResetObjectToHostServerRpc(); } } /// /// Process the select filter. /// /// Interactor being used to process the Select. /// /// public bool Process(IXRSelectInteractor interactor, IXRSelectInteractable interactable) { return IsOwner || allowOverrideOwnership || (!IsOwner & !isInteracting); } /// /// Process the hover filter. /// /// Interactor being used to process the Hover. /// /// public bool Process(IXRHoverInteractor interactor, IXRHoverInteractable interactable) { return IsOwner || allowOverrideOwnership || (!IsOwner & !isInteracting); } } #if UNITY_EDITOR /// /// Custom Editor for the class. /// [CustomEditor(typeof(NetworkBaseInteractable), true), CanEditMultipleObjects] public class NetworkBaseInteractableEditor : Editor { // Serialized properties SerializedProperty m_UseInteractingChangedEvent; SerializedProperty m_InteractingChangedEvent; SerializedProperty m_SyncHover; SerializedProperty m_UseHoverEvents; SerializedProperty m_SyncHoverEventServer; SerializedProperty m_SyncHoverEventAll; SerializedProperty m_SyncSelect; SerializedProperty m_UseSelectEvents; SerializedProperty m_SyncSelectEventServer; SerializedProperty m_SyncSelectEventAll; SerializedProperty m_SyncActivate; SerializedProperty m_UseActivateEvents; SerializedProperty m_SyncActivateEventServer; SerializedProperty m_SyncActivateEventAll; SerializedProperty m_FoldoutStates; /// /// Called when the editor is enabled. /// Initializes the serialized properties. /// void OnEnable() { m_UseInteractingChangedEvent = serializedObject.FindProperty("m_UseInteractingChangedEvent"); m_InteractingChangedEvent = serializedObject.FindProperty("OnInteractingChanged"); m_SyncHover = serializedObject.FindProperty("m_SyncHover"); m_UseHoverEvents = serializedObject.FindProperty("m_UseHoverEvents"); m_SyncHoverEventServer = serializedObject.FindProperty("HoverNetworkedEventServer"); m_SyncHoverEventAll = serializedObject.FindProperty("HoverNetworkedEventAll"); m_SyncSelect = serializedObject.FindProperty("m_SyncSelect"); m_UseSelectEvents = serializedObject.FindProperty("m_UseSelectEvents"); m_SyncSelectEventServer = serializedObject.FindProperty("SelectNetworkedEventServer"); m_SyncSelectEventAll = serializedObject.FindProperty("SelectNetworkedEventAll"); m_SyncActivate = serializedObject.FindProperty("m_SyncActivate"); m_UseActivateEvents = serializedObject.FindProperty("m_UseActivateEvents"); m_SyncActivateEventServer = serializedObject.FindProperty("ActivateNetworkedEventServer"); m_SyncActivateEventAll = serializedObject.FindProperty("ActivateNetworkedEventAll"); m_FoldoutStates = serializedObject.FindProperty("m_FoldoutValues"); } /// /// Called to draw the custom inspector GUI. /// public override void OnInspectorGUI() { base.OnInspectorGUI(); serializedObject.Update(); EditorGUILayout.Space(10); EditorGUILayout.LabelField("Unity Events", EditorStyles.boldLabel); EditorGUILayout.PropertyField(m_UseInteractingChangedEvent); if (m_UseInteractingChangedEvent.boolValue) { EditorGUILayout.PropertyField(m_InteractingChangedEvent); } // Hover Logic EditorGUILayout.Space(10); SerializedProperty option = m_FoldoutStates.GetArrayElementAtIndex(0); option.boolValue = EditorGUILayout.Foldout(option.boolValue, "Hover Options", true); if (option.boolValue) { if (m_UseHoverEvents.boolValue) { m_SyncHover.boolValue = true; GUI.enabled = false; } EditorGUILayout.PropertyField(m_SyncHover); GUI.enabled = true; EditorGUILayout.PropertyField(m_UseHoverEvents); if (m_UseHoverEvents.boolValue) { EditorGUILayout.PropertyField(m_SyncHoverEventServer); EditorGUILayout.PropertyField(m_SyncHoverEventAll); } } // Select Logic EditorGUILayout.Space(10); option = m_FoldoutStates.GetArrayElementAtIndex(1); option.boolValue = EditorGUILayout.Foldout(option.boolValue, "Select Options", true); if (option.boolValue) { if (m_UseSelectEvents.boolValue) { m_SyncSelect.boolValue = true; GUI.enabled = false; } EditorGUILayout.PropertyField(m_SyncSelect); GUI.enabled = true; EditorGUILayout.PropertyField(m_UseSelectEvents); if (m_UseSelectEvents.boolValue) { EditorGUILayout.PropertyField(m_SyncSelectEventServer); EditorGUILayout.PropertyField(m_SyncSelectEventAll); } } // Activate Logic EditorGUILayout.Space(10); option = m_FoldoutStates.GetArrayElementAtIndex(2); option.boolValue = EditorGUILayout.Foldout(option.boolValue, "Activate Options", true); if (option.boolValue) { if (m_UseActivateEvents.boolValue) { m_SyncActivate.boolValue = true; GUI.enabled = false; } EditorGUILayout.PropertyField(m_SyncActivate); GUI.enabled = true; EditorGUILayout.PropertyField(m_UseActivateEvents); if (m_UseActivateEvents.boolValue) { EditorGUILayout.PropertyField(m_SyncActivateEventServer); EditorGUILayout.PropertyField(m_SyncActivateEventAll); } } serializedObject.ApplyModifiedProperties(); } } #endif }