Initial commit

This commit is contained in:
Thorbjoern
2025-05-26 00:46:28 +02:00
commit e5bca03433
3896 changed files with 1434297 additions and 0 deletions
@@ -0,0 +1,51 @@
using Unity.Netcode;
using Unity.VRTemplate;
using UnityEngine;
using XRMultiplayer;
/// <summary>
/// Represents a networked billboard that will always billboard towards the person who owns the object.
/// </summary>
[RequireComponent(typeof(TurnToFace))]
public class NetworkBillboard : NetworkBehaviour
{
/// <summary>
/// Billboard effect.
/// </summary>
TurnToFace m_TurnToFace;
/// <inheritdoc/>
private void Start()
{
m_TurnToFace = GetComponent<TurnToFace>();
}
/// <summary>
/// Handles the change in the "isInteracting" state of the object.
/// </summary>
/// <param name="old">The previous value of the "isInteracting" state.</param>
/// <param name="current">The current value of the "isInteracting" state.</param>
public void IsHeldChanged(bool current)
{
if (current)
{
m_TurnToFace.enabled = true;
if (XRINetworkGameManager.Instance.GetPlayerByID(NetworkObject.OwnerClientId, out XRINetworkPlayer player))
{
m_TurnToFace.faceTarget = player.head;
}
}
}
/// <summary>
/// Called when the object is selected or deselected.
/// </summary>
/// <param name="selected">Indicates whether the object is selected or deselected.</param>
public void Selected(bool selected)
{
if (!selected)
{
m_TurnToFace.enabled = false;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 98ee6bff869e2994a9693cb00d641e22
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 8ec560b35f7c9bb4c96b9ac61130a74b
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,887 @@
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
{
/// <summary>
/// NetworkInteractableBase class synchronizes the <see cref=XRBaseInteractable"/> events over the network.
/// Options are exposed to determine which functionality you want to syncrhonize.
/// </summary>
/// <remarks>
/// 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 <see cref=NetworkPhysicsInteractable"/> for an example of how to extend this class.
/// </remarks>
[RequireComponent(typeof(XRBaseInteractable))]
[DisallowMultipleComponent]
public class NetworkBaseInteractable : NetworkBehaviour, IXRSelectFilter, IXRHoverFilter
{
/// <summary>
/// Allow users to take ownership of currently controlled objects.
/// </summary>
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;
/// <summary>
/// Amount of time before checking for false positives for the object interaction state
/// </summary>
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;
/// <summary>
/// Ignore Socket Interaction
/// </summary>
public bool ignoreSocketSelectedCallback
{
get => m_IgnoreSocketSelectedCallback;
set => m_IgnoreSocketSelectedCallback = value;
}
[SerializeField, Tooltip("Ignore Socket Interaction")]
protected bool m_IgnoreSocketSelectedCallback = true;
/// <summary>
/// Resets the object position, scale, and rotation on disconnect.
/// </summary>
public bool resetObjectOnDisconenct
{
get => m_ResetObjectOnDisconnect;
set => m_ResetObjectOnDisconnect = value;
}
[SerializeField, Tooltip("Reset object on disconnect")]
protected bool m_ResetObjectOnDisconnect = true;
/// <summary>
/// Amount of time before relinquishing ownership of the object back to the host.
/// </summary>
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;
/// <summary>
/// Amount of time before relinquishing ownership of the object back to the host.
/// </summary>
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;
/// <summary>
/// Gets the current state of an object being interacted over the network.
/// </summary>
public bool isInteracting
{
get => m_IsInteracting.Value;
}
/// <summary>
/// Syncs the current state of being interacted with or not.
/// Prevents users from taking control of currently controlled objects, unless <see cref="allowOverrideOwnership"/> is true.
/// </summary>
/// <remarks>
/// <see cref="allowOverrideOwnership"/> will allow users to bypass this value and take ownership from other players.
/// </remarks>
protected NetworkVariable<bool> 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<bool> OnInteractingChanged;
/// <summary>
/// Sync Hover interaction over network.
/// </summary>
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<bool> HoverNetworkedEventServer;
[HideInInspector] public UnityEvent<bool> HoverNetworkedEventAll;
/// <summary>
/// Sync Select interaction over network.
/// </summary>
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<bool> SelectNetworkedEventServer;
[HideInInspector] public UnityEvent<bool> SelectNetworkedEventAll;
/// <summary>
/// Sync Activate interaction over network.
/// </summary>
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<bool> ActivateNetworkedEventServer;
[HideInInspector] public UnityEvent<bool> ActivateNetworkedEventAll;
/// <summary>
/// Base Interactable used for syncing events.
/// </summary>
public XRBaseInteractable baseInteractable
{
get => m_BaseInteractable;
set => m_BaseInteractable = value;
}
protected XRBaseInteractable m_BaseInteractable;
public bool canProcess => isActiveAndEnabled;
/// <summary>
/// Starting Pose for the object transform.
/// </summary>
protected Pose m_OriginalPose;
/// <summary>
/// Starting scale for the object transform.
/// </summary>
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
/// <summary>
/// Foldout states for the editor.
/// </summary>
[HideInInspector, SerializeField]
bool[] m_FoldoutValues = {true, true, true};
#endif
/// <summary>
/// After a set amount of time, relinquish ownership of the object back to the host.
/// </summary>
IEnumerator m_RelinquishToHostEnumerator;
/// <summary>
/// Check for false positives of the object being interacted with.
/// </summary>
IEnumerator m_HostInteractionCheckEnumerator;
/// <inheritdoc/>
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<XRInteractionManager>();
#pragma warning disable CS0618 // Type or member is obsolete
m_AffordanceStateProvider = GetComponentInChildren<BaseAffordanceStateProvider>();
#pragma warning restore CS0618 // Type or member is obsolete
}
/// <inheritdoc/>
private void OnEnable()
{
// Set initial pose and scale
m_OriginalPose.position = transform.position;
m_OriginalPose.rotation = transform.rotation;
m_OriginalScale = transform.localScale;
SetupListeners(true);
}
/// <inheritdoc/>
private void OnDisable()
{
SetupListeners(false);
}
/// <summary>
/// Handles the listeners for the interactable events.
/// </summary>
/// <param name="setup">
/// Whether or not we are adding or removing the listeners.
/// </param>
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);
}
}
}
/// <inheritdoc/>
public override void OnNetworkSpawn()
{
base.OnNetworkSpawn();
NetworkObject.DontDestroyWithOwner = true;
m_IsInteracting.OnValueChanged += OnIsInteractingChanged;
if (IsOwner)
m_IsInteracting.Value = false;
if (m_ResetObjectOnDisconnect)
{
ResetObject();
}
}
/// <inheritdoc/>
public override void OnNetworkDespawn()
{
base.OnNetworkDespawn();
m_IsInteracting.OnValueChanged -= OnIsInteractingChanged;
if (IsOwner)
m_IsInteracting.Value = false;
if (m_ResetObjectOnDisconnect)
{
ResetObject();
}
}
/// <summary>
/// Resets the object position, scale, and rotation based on the original pose determined in <see cref="OnEnable"/>
/// </summary>
public virtual void ResetObject()
{
transform.SetPositionAndRotation(m_OriginalPose.position, m_OriginalPose.rotation);
transform.localScale = m_OriginalScale;
}
/// <summary>
/// Callback for the Hover Enter event executed for the local user.
/// </summary>
/// <param name="args"></param>
public virtual void OnHoverEnterLocal(BaseInteractionEventArgs args)
{
Hovered(true);
if (syncHover)
{
OnHoverServerRpc(true, NetworkManager.Singleton.LocalClientId);
}
}
/// <summary>
/// Callback for the Hover Exit event executed for the local user.
/// </summary>
/// <param name="args"></param>
public virtual void OnHoverExitLocal(BaseInteractionEventArgs args)
{
Hovered(false);
if (syncHover)
{
OnHoverServerRpc(false, NetworkManager.Singleton.LocalClientId);
}
}
/// <summary>
/// Hover event executed on the Server.
/// </summary>
/// <param name="entered">True if hover entered, False if hover exited.</param>
/// <param name="clientId">ClientId who sent the RPC.</param>
[ServerRpc(RequireOwnership = false)]
public virtual void OnHoverServerRpc(bool entered, ulong clientId)
{
OnHoverClientRpc(entered, clientId);
if (m_UseHoverEvents)
HoverNetworkedEventServer.Invoke(entered);
}
/// <summary>
/// Hover event executed on all clients.
/// </summary>
/// <param name="entered">True if hover entered, False if hover exited.</param>
/// <param name="clientId">ClientId who sent the RPC.</param>
[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
}
}
}
/// <summary>
/// This function gets called immediately for the local user,
/// and gets called remotely from the server on all clients.
/// </summary>
/// <param name="entered">True if hover entered, False if hover exited.</param>
public virtual void Hovered(bool entered)
{
if (m_UseHoverEvents)
HoverNetworkedEventAll.Invoke(entered);
}
/// <summary>
/// Callback for the Select Enter event executed for the local user.
/// </summary>
/// <param name="args"></param>
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<XRSocketInteractor>() != 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;
}
}
}
/// <summary>
/// Callback for the Select Exit event executed for the local user.
/// </summary>
/// <param name="args"></param>
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<XRSocketInteractor>() != 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);
}
/// <summary>
/// Select event executed on the Server.
/// </summary>
/// <param name="selected">True if select entered, False if select exited.</param>
/// <param name="clientId">ClientId who sent the RPC.</param>
[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);
}
/// <summary>
/// Select event executed on all clients.
/// </summary>
/// <param name="selected">True if select entered, False if select exited.</param>
/// <param name="clientId">ClientId who sent the RPC.</param>
[ClientRpc]
public virtual void OnSelectClientRpc(bool selected, ulong clientId)
{
if (clientId != NetworkManager.Singleton.LocalClientId)
{
Selected(selected);
}
}
/// <summary>
/// This function gets called immediately for the local user,
/// and gets called remotely from the server on all clients.
/// </summary>
/// <param name="selected">Whether or not selected was called.</param>
public virtual void Selected(bool selected) { }
/// <summary>
/// Callback for the Activate event executed for the local user.
/// </summary>
/// <param name="args"></param>
public virtual void OnActivateLocal(BaseInteractionEventArgs args)
{
if (!IsOwner) return;
Activated(true);
if (syncActivate)
{
OnActivateServerRpc(true, NetworkManager.Singleton.LocalClientId);
}
}
/// <summary>
/// Callback for the Deactivate event executed for the local user.
/// </summary>
/// <param name="args"></param>
public virtual void OnDeactivateLocal(BaseInteractionEventArgs args)
{
if (!IsOwner) return;
Activated(false);
if (syncActivate)
{
OnActivateServerRpc(false, NetworkManager.Singleton.LocalClientId);
}
}
/// <summary>
/// Activate event executed on the Server.
/// </summary>
/// <param name="activate">True if activated, False if Deactivated.</param>
/// <param name="clientId">ClientId who sent the RPC.</param>
[ServerRpc(RequireOwnership = false)]
public virtual void OnActivateServerRpc(bool activate, ulong clientId)
{
OnActivateClientRpc(activate, clientId);
if (m_UseActivateEvents)
ActivateNetworkedEventServer.Invoke(activate);
}
/// <summary>
/// Activate event executed on all clients.
/// </summary>
/// <param name="activate">True if activated, False if Deactivated.</param>
/// <param name="clientId">ClientId who sent the RPC.</param>
[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
}
}
}
/// <summary>
/// This function gets called immediately for the local user,
/// and gets called remotely from the server on all clients.
/// </summary>
/// <param name="activate">True if activated, False if Deactivated.</param>
public virtual void Activated(bool activate)
{
if (m_UseActivateEvents)
ActivateNetworkedEventAll.Invoke(activate);
}
/// <summary>
/// Checks if another user can hold or pickup an interactable.
/// </summary>
/// <returns></returns>
protected virtual bool CanHold()
{
return !isInteracting || allowOverrideOwnership;
}
/// <summary>
/// See <see cref="NetworkBehaviour"/>.
/// </summary>
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;
}
}
}
}
/// <summary>
/// See <see cref="NetworkBehaviour"/>.
/// </summary>
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);
}
}
/// <summary>
/// Checks every <see cref="interactionCheckTime"/> for false positives of the object being interacted with.
/// </summary>
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);
}
}
/// <summary>
/// Checks if the object is selected by a socket.
/// </summary>
bool IsSelectedBySocket()
{
if (baseInteractable.isSelected)
{
foreach (var interactor in baseInteractable.interactorsSelecting)
{
if (interactor is XRSocketInteractor)
{
return true;
}
}
}
return false;
}
/// <summary>
/// Callback for anytime the <see cref="m_IsInteracting"/> value changes.
/// </summary>
/// <param name="oldValue"></param>
/// <param name="newValue"></param>
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);
}
}
/// <summary>
/// Relinquish ownership of the object back to the host after a set amount of time.
/// </summary>
protected void RelinquishOwnershipAfterTime()
{
if (!m_RelinquishOwnershipAfterTime) return;
if (m_RelinquishToHostEnumerator != null) StopCoroutine(m_RelinquishToHostEnumerator);
m_RelinquishToHostEnumerator = RelinquishOwnershipToHost();
StartCoroutine(m_RelinquishToHostEnumerator);
}
/// <summary>
/// Coroutine to relinquish ownership of the object back to the host after a set amount of time.
/// </summary>
IEnumerator RelinquishOwnershipToHost()
{
yield return new WaitForSeconds(relinquishOwnershipTime);
if (!IsServer && !baseInteractable.isSelected)
{
ResetObjectToHostServerRpc();
}
}
/// <summary>
/// Process the select filter.
/// </summary>
/// <param name="interactor">Interactor being used to process the Select.</param>
/// <param name="interactable"></param>
/// <returns></returns>
public bool Process(IXRSelectInteractor interactor, IXRSelectInteractable interactable)
{
return IsOwner || allowOverrideOwnership || (!IsOwner & !isInteracting);
}
/// <summary>
/// Process the hover filter.
/// </summary>
/// <param name="interactor">Interactor being used to process the Hover.</param>
/// <param name="interactable"></param>
/// <returns></returns>
public bool Process(IXRHoverInteractor interactor, IXRHoverInteractable interactable)
{
return IsOwner || allowOverrideOwnership || (!IsOwner & !isInteracting);
}
}
#if UNITY_EDITOR
/// <summary>
/// Custom Editor for the <see cref="NetworkBaseInteractable"/> class.
/// </summary>
[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;
/// <summary>
/// Called when the editor is enabled.
/// Initializes the serialized properties.
/// </summary>
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");
}
/// <summary>
/// Called to draw the custom inspector GUI.
/// </summary>
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
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e06144dec2dcce44ea74dc2ee951a4f5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,372 @@
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
{
/// <summary>
/// NetworkInteractableGrab adds some extra functionality to the NetworkBaseInteractable class
/// to allow for better immediate feedback on non owner players.
/// </summary>
[RequireComponent(typeof(Rigidbody), typeof(ClientNetworkTransform))]
public class NetworkPhysicsInteractable : NetworkBaseInteractable
{
/// <summary>
/// Enabling this will call Request Ownership on collision with non-local NetworkPhysicsInteractables.
/// </summary>
[Header("Collision Based Ownership Transfer"), SerializeField, Tooltip("Enabling this will call Request Ownership on collision with non-local NetworkPhysicsInteractables.")]
protected bool m_AllowCollisionOwnershipExchange = true;
/// <summary>
/// Determines the minimum velocity magnitude required to request ownership on collision.
/// </summary>
[SerializeField, Tooltip("Determines the minimum velocity magnitude required to request ownership on collision.")]
protected float m_MinExchangeVelocityMagitude = .025f;
/// <summary>
/// Sets the <see cref="Rigidbody.constraints"/> to <see cref="RigidbodyConstraints.FreezeAll"/> on spawn.
/// </summary>
/// <remarks>
/// This is useful for objects that you want to be non interactable on spawn.
/// </remarks>
[Header("Spawn Options")]
public bool spawnLocked = true;
/// <summary>
/// Used to get the current networked value of <see cref="m_LockedOnSpawn.Value"/>.
/// </summary>
public bool lockedOnSpawn => m_LockedOnSpawn.Value;
protected NetworkVariable<bool> m_LockedOnSpawn = new(true, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner);
/// <summary>
/// This flag is used to prevent multiple attempts at requesting ownership.
/// </summary>
protected bool m_RequestingOwnership = false;
/// <summary>
/// Client Network Transform used for synchronizing transform data.
/// </summary>
protected ClientNetworkTransform m_ClientNetworkTransform;
protected Rigidbody m_Rigidbody;
protected Collider m_Collider;
/// <summary>
/// This flag is used to determine if the object should be reset on disconnect.
/// </summary>
protected NetworkVariable<bool> m_ResettingObject = new(false, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner);
/// <summary>
/// Coroutine for checking ownership after a request has been made based on current user RTT to server.
/// </summary>
protected IEnumerator checkOwnershipRoutine;
bool m_PauseVelocityCalcuations = false;
Vector3 m_AverageVelocity;
Vector3[] m_CurrentCalculatedVelocity;
int m_FramesToCalculate = 3;
int m_CurrentFrame = 0;
Vector3 m_PrevPos;
/// <inheritdoc/>
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<Collider>();
}
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;
}
/// <inheritdoc/>
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;
}
/// <inheritdoc/>
public override void OnNetworkDespawn()
{
base.OnNetworkDespawn();
if (m_ResetObjectOnDisconnect & !m_Rigidbody.isKinematic)
{
ResetObjectPhysics();
}
}
/// <inheritdoc/>
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;
}
}
/// <summary>
/// This function is called when the <see cref="m_ResettingObject"/> value changes.
/// </summary>
void OnObjectPhysicsReset(bool oldValue, bool currentValue)
{
if (currentValue)
{
m_CurrentCalculatedVelocity = new Vector3[m_FramesToCalculate];
m_PauseVelocityCalcuations = true;
}
else
{
m_PauseVelocityCalcuations = false;
}
}
/// <inheritdoc/>
public override void ResetObject()
{
base.ResetObject();
ResetObjectPhysics();
}
/// <summary>
/// Resets the object to its original state.
/// </summary>
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;
}
/// <inheritdoc/>
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<XRSocketInteractor>() != 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;
}
}
/// <inheritdoc/>
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<XRSocketInteractor>() != 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;
}
}
}
}
/// <summary>
/// Override for enabling the NetworkTransform once ownership has been gained.
/// </summary>
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;
}
}
}
/// <summary>
/// If Ownership was lost due to another player taking over, make sure we cancel all ownership requests and re-enable the ClientNetworkTransform.
/// </summary>
public override void OnLostOwnership()
{
base.OnLostOwnership();
if (checkOwnershipRoutine != null) StopCoroutine(checkOwnershipRoutine);
m_RequestingOwnership = false;
m_ClientNetworkTransform.enabled = true;
}
/// <inheritdoc/>
void OnCollisionEnter(Collision collision)
{
if (!IsOwner || !m_AllowCollisionOwnershipExchange) return;
NetworkPhysicsInteractable networkPhysicsInteractable = collision.transform.GetComponentInParent<NetworkPhysicsInteractable>();
if (networkPhysicsInteractable != null && (isInteracting || IsMovingFaster(networkPhysicsInteractable.m_Rigidbody)))
{
networkPhysicsInteractable.RequestOwnership();
}
}
/// <summary>
/// Checks if the current object is moving faster than the other object based on velocity magnitude.
/// </summary>
/// <param name="otherBody">The Other Rigidbody you are colliding with</param>
/// <returns>Returns true if this object is moving faster than the <see cref="m_MinExchangeVelocityMagitude"/> and the object we are hitting.</returns>
protected bool IsMovingFaster(Rigidbody otherBody)
{
return m_Rigidbody.linearVelocity.magnitude > m_MinExchangeVelocityMagitude && m_Rigidbody.linearVelocity.magnitude > otherBody.linearVelocity.magnitude;
}
/// <summary>
/// Checks if ownership transfer is blocked by current conditions.
/// </summary>
/// <returns></returns>
public bool OwnershipTransferBlocked()
{
return isInteracting || IsOwner || m_RequestingOwnership || m_LockedOnSpawn.Value || !m_AllowCollisionOwnershipExchange || !NetworkObject.IsSpawned || baseInteractable.isSelected || m_ResettingObject.Value;
}
/// <summary>
/// This function will request ownership of this object.
/// This will disable the <see cref="m_ClientNetworkTransform"/> to allow for smooth interaction.
/// This will also set the <see cref="m_RequestingOwnership"/> flag to true.
/// This will also start a Coroutine to check if ownership was not granted based on the current RTT to the server.
/// </summary>
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);
}
/// <summary>
/// Coroutine to check if ownership was granted based on the current RTT to the server.
/// </summary>
/// <returns></returns>
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;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: eb1cca572c123384bbcd5da9749cf9b1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,72 @@
using System.Collections;
using Unity.Netcode;
using UnityEngine;
using UnityEngine.XR.Interaction.Toolkit.Interactables;
using UnityEngine.XR.Interaction.Toolkit.Interactors;
namespace XRMultiplayer
{
/// <summary>
/// NetworkSocketInteractor class is responsible for synchronizing the
/// <see cref="XRSocketInteractor"/> functionality over the network.
/// </summary>
[RequireComponent(typeof(XRSocketInteractor))]
public class NetworkSocketInteractor : NetworkBehaviour
{
/// <summary>
/// Socket Interactor to use.
/// </summary>
XRSocketInteractor m_SocketInteractor;
/// <summary>
/// Coroutine used to disable the <see cref="XRSocketInteractor"/> component on Hover Exit across the network.
/// </summary>
Coroutine m_DisableRoutine;
private void Awake()
{
// Get Socket Component.
if (!TryGetComponent(out m_SocketInteractor))
{
Utils.Log("Missing Components. Disabling Now.", 2);
this.enabled = false;
return;
}
}
public override void OnNetworkSpawn()
{
base.OnNetworkSpawn();
if (m_DisableRoutine != null) StopCoroutine(m_DisableRoutine);
m_DisableRoutine = StartCoroutine(DisableForTime());
}
public override void OnNetworkDespawn()
{
base.OnNetworkDespawn();
XRGrabInteractable grabInteractable = m_SocketInteractor.GetOldestInteractableSelected() as XRGrabInteractable;
m_SocketInteractor.enabled = false;
if (grabInteractable != null)
{
NetworkPhysicsInteractable networkInteractable = grabInteractable.GetComponent<NetworkPhysicsInteractable>();
if (networkInteractable != null)
{
networkInteractable.ResetObject();
networkInteractable.ResetObjectPhysics();
}
}
}
/// <summary>
/// Coroutine to disable <see cref="XRSocketInteractor"/> for 1 second.
/// </summary>
/// <returns></returns>
IEnumerator DisableForTime()
{
m_SocketInteractor.enabled = false;
yield return new WaitForSeconds(.5f);
m_SocketInteractor.enabled = true;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e5abeeeda2c026f4981c504976a74954
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: f21bf63d05ded4047b4d5264f7a61153
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,149 @@
using System.Threading.Tasks;
using Unity.Services.Authentication;
using Unity.Services.Core;
using UnityEngine;
#if UNITY_EDITOR
// Unity 6 Only
#if HAS_MPPM
using Unity.Multiplayer.Playmode;
using UnityEngine.XR.Interaction.Toolkit.UI;
#endif
#if HAS_PARRELSYNC
using ParrelSync;
#endif
#endif
namespace XRMultiplayer
{
public class AuthenticationManager : MonoBehaviour
{
const string k_DebugPrepend = "<color=#938FFF>[Authentication Manager]</color> ";
/// <summary>
/// The argument ID to search for in the command line args.
/// </summary>
const string k_playerArgID = "PlayerArg";
/// <summary>
/// Determines if the AuthenticationManager should use command line args to determine the player ID when launching a build.
/// </summary>
[SerializeField] bool m_UseCommandLineArgs = true;
/// <summary>
/// Simple Authentication function. This uses bare bones authentication and anonymous sign in.
/// </summary>
/// <returns></returns>
public virtual async Task<bool> Authenticate()
{
// Check if UGS has not been initialized yet, and initialize.
if (UnityServices.State == ServicesInitializationState.Uninitialized)
{
var options = new InitializationOptions();
string playerId = "Player";
// Check for editor clones (MPPM or ParrelSync).
// This allows for multiple instances of the editor to connect to UGS.
#if UNITY_EDITOR
playerId = "Editor";
#if HAS_MPPM
//Check for MPPM
playerId += CheckMPPM();
#elif HAS_PARRELSYNC
// Check for ParrelSync
playerId += CheckParrelSync();
#endif
#endif
// Check for command line args in builds
if (!Application.isEditor && m_UseCommandLineArgs)
{
playerId += GetPlayerIDArg();
}
options.SetProfile(playerId);
Utils.Log($"{k_DebugPrepend}Signing in with profile {playerId}");
// Initialize UGS using any options defined
await UnityServices.InitializeAsync(options);
}
// If not already signed on then do so.
if (!AuthenticationService.Instance.IsAuthorized)
{
// Signing in anonymously for simplicity sake.
await AuthenticationService.Instance.SignInAnonymouslyAsync();
}
// Cache PlayerId.
XRINetworkGameManager.AuthenicationId = AuthenticationService.Instance.PlayerId;
return UnityServices.State == ServicesInitializationState.Initialized;
}
public static bool IsAuthenticated()
{
try
{
return AuthenticationService.Instance.IsSignedIn;
}
catch (System.Exception e)
{
Utils.Log($"{k_DebugPrepend}Checking for AuthenticationService.Instance before initialized.{e}");
return false;
}
}
string GetPlayerIDArg()
{
string playerID = "";
string[] args = System.Environment.GetCommandLineArgs();
foreach (string arg in args)
{
arg.ToLower();
if (arg.ToLower().Contains(k_playerArgID.ToLower()))
{
var splitArgs = arg.Split(':');
if (splitArgs.Length > 0)
{
playerID += splitArgs[1];
}
}
}
return playerID;
}
#if UNITY_EDITOR
#if HAS_MPPM
string CheckMPPM()
{
Utils.Log($"{k_DebugPrepend}MPPM Found");
string mppmString = "";
if(CurrentPlayer.ReadOnlyTags().Length > 0)
{
mppmString += CurrentPlayer.ReadOnlyTags()[0];
// Force input module to disable mouse and touch input to suppress MPPM startup errors.
var inputModule = FindFirstObjectByType<XRUIInputModule>();
inputModule.enableMouseInput = false;
inputModule.enableTouchInput = false;
}
return mppmString;
}
#endif
#if HAS_PARRELSYNC
string CheckParrelSync()
{
Utils.Log($"{k_DebugPrepend}ParrelSync Found");
string pSyncString = "";
if (ClonesManager.IsClone()) pSyncString += ClonesManager.GetArgument();
return pSyncString;
}
#endif
#endif
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a23511cff0c072e4fb042675bd9c0fb4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,497 @@
using System.Collections;
using System.Collections.Generic;
using Unity.Services.Lobbies.Models;
using Unity.Services.Lobbies;
using UnityEngine;
using System.Threading.Tasks;
using System;
using Unity.Services.Relay;
using Unity.Netcode.Transports.UTP;
using Unity.XR.CoreUtils.Bindings.Variables;
using Unity.Services.Authentication;
using UnityEngine.SceneManagement;
namespace XRMultiplayer
{
/// <summary>
/// This class manages the relationship between Lobby, Relay, and Unity Transport.
/// </summary>
public class LobbyManager : MonoBehaviour
{
// Constants for Lobby Data.
public const string k_JoinCodeKeyIdentifier = "j";
public const string k_RegionKeyIdentifier = "r";
public const string k_BuildIdKeyIdentifier = "b";
public const string k_SceneKeyIdentifier = "s";
public const string k_EditorKeyIdentifier = "e";
static bool s_HideEditorInLobbies;
[Tooltip("This will prevent joining into rooms that are being hosted in different scenes.\nThis should almost always be false.")]
public bool allowDifferentScenes = false;
[Tooltip("This will hide editor created rooms from external builds.\nNOTE: This will not hide editor created rooms from other editors.")]
public bool hideEditorFromLobby = false;
// Action that gets invoked when you fail to connect to a lobby. Primarily used for noting failure messages.
public Action<string> OnLobbyFailed;
// The current connected lobby.
public Lobby connectedLobby
{
get => m_ConnectedLobby;
set => m_ConnectedLobby = value;
}
Lobby m_ConnectedLobby;
// The Transport used for connection.
UnityTransport m_Transport;
// This routine keeps the lobby alive once joined (by default lobbies will close after 30 seconds of inactivity.
Coroutine m_HeartBeatRoutine;
/// <summary>
/// Subscribe to this bindable string for status updates from this class
/// </summary>
public static IReadOnlyBindableVariable<string> status
{
get => m_Status;
}
readonly static BindableVariable<string> m_Status = new("");
const string k_DebugPrepend = "<color=#EC0CFA>[Lobby Manager]</color> ";
/// <summary>
/// See <see cref="MonoBehaviour"/>.
/// </summary>
private void Awake()
{
m_Transport = FindFirstObjectByType<UnityTransport>();
if (!Application.isEditor)
{
hideEditorFromLobby = false;
}
s_HideEditorInLobbies = hideEditorFromLobby;
}
/// <summary>
/// Quick Join Function will try and find any lobbies via QuickJoinLobbyAsync().
/// If no lobbies are found then a new lobby is created.
/// </summary>
/// <returns></returns>
public async Task<Lobby> QuickJoinLobby()
{
m_Status.Value = "Checking For Existing Lobbies.";
Utils.Log($"{k_DebugPrepend}{m_Status.Value}");
Lobby lobby;
try
{
Utils.Log($"{k_DebugPrepend} Getting lobby via Quick Join");
lobby = await LobbyService.Instance.QuickJoinLobbyAsync(GetQuickJoinFilterOptions());
await SetupRelay(lobby);
ConnectedToLobby(lobby);
if (lobby != null)
{
m_ConnectedLobby = lobby;
return lobby;
}
}
catch
{
m_Status.Value = "No Available Lobbies. Creating New Lobby.";
Utils.Log($"{k_DebugPrepend}{m_Status.Value}");
}
// If no existing Lobbies, then create a new one.
lobby = await CreateLobby();
return lobby;
}
/// <summary>
/// Joins a lobby.
/// </summary>
/// <param name="lobby">Lobby to join.</param>
/// <param name="roomCode">Lobby Code to join with.</param>
/// <returns>Returns the Lobby.</returns>
public async Task<Lobby> JoinLobby(Lobby lobby = null, string roomCode = null)
{
try
{
// If Lobby is null, then get the new lobby based on room code
lobby = await GetLobby(lobby, roomCode);
await SetupRelay(lobby);
ConnectedToLobby(lobby);
return lobby;
}
catch (Exception e)
{
string failureMessage = "Failed to Join Lobby.";
Utils.Log($"{k_DebugPrepend}{e.Message}", 1);
if (e is LobbyServiceException)
{
string message = e.Message.ToLower();
if (message.Contains("Rate limit".ToLower()))
failureMessage = "Rate limit exceeded. Please try again later.";
else if (message.Contains("Lobby not found".ToLower()))
failureMessage = "Lobby not found. Please try a new Lobby.";
else
failureMessage = e.Message;
}
Utils.Log($"{k_DebugPrepend}{failureMessage}\n\n{e}", 1);
OnLobbyFailed?.Invoke($"{failureMessage}");
return null;
}
}
/// <summary>
/// This function will try to create a lobby and host a networked session.
/// </summary>
/// <returns></returns>
public async Task<Lobby> CreateLobby(string roomName = null, bool isPrivate = false, int playerCount = XRINetworkGameManager.maxPlayers)
{
try
{
m_Status.Value = "Creating Relay";
// Creates a new Allocation based on the defined max players above
var alloc = await RelayService.Instance.CreateAllocationAsync(XRINetworkGameManager.maxPlayers);
m_Status.Value = "Creating Join Code";
// Get a join code based on the Allocation
var joinCode = await RelayService.Instance.GetJoinCodeAsync(alloc.AllocationId);
// Creates Lobby Options Dictionary for other clients to find and join
var options = new CreateLobbyOptions
{
// Set the Data to be used for lobby filtering
Data = new Dictionary<string, DataObject>
{
{
// Set Join Code Key
k_JoinCodeKeyIdentifier, new DataObject(DataObject.VisibilityOptions.Public, joinCode)
},
{
// Set Region Key
k_RegionKeyIdentifier, new DataObject(DataObject.VisibilityOptions.Public, alloc.Region)
},
{
// Set Build ID Key
k_BuildIdKeyIdentifier, new DataObject(DataObject.VisibilityOptions.Public, Application.version, DataObject.IndexOptions.S1)
},
{
// Set Scene Key
k_SceneKeyIdentifier, new DataObject(DataObject.VisibilityOptions.Public, SceneManager.GetActiveScene().name, DataObject.IndexOptions.S2)
},
{
// Set Editor Key
k_EditorKeyIdentifier, new DataObject(DataObject.VisibilityOptions.Public, hideEditorFromLobby.ToString(), DataObject.IndexOptions.S3)
},
},
IsPrivate = isPrivate,
};
m_Status.Value = "Creating Lobby";
// Creates the Lobby with the specified max players and lobby options. Currently just naming "General Lobby"
string lobbyName = string.IsNullOrEmpty(roomName) ? $"{XRINetworkGameManager.LocalPlayerName.Value}'s Room" : $"{roomName}";
// RATE LIMIT: 2 request per 6 seconds
var lobby = await LobbyService.Instance.CreateLobbyAsync(lobbyName, playerCount, options);
Utils.Log($"{k_DebugPrepend}Created Lobby with Join Code: {joinCode}, Region: {alloc.Region}, Build ID: {Application.version}, Scene: {SceneManager.GetActiveScene().name}, Editor: {hideEditorFromLobby}");
// Stop the heartbeat routine if one exists, and starts a new one. This keeps the lobby active for visibility
if (m_HeartBeatRoutine != null) StopCoroutine(m_HeartBeatRoutine);
m_HeartBeatRoutine = StartCoroutine(LobbyHeartbeatCoroutine(lobby.Id));
//Populate the transport data with the relay info for the host (IP, port, etc...)
m_Transport.SetHostRelayData(alloc.RelayServer.IpV4, (ushort)alloc.RelayServer.Port, alloc.AllocationIdBytes, alloc.Key, alloc.ConnectionData);
ConnectedToLobby(lobby);
return lobby;
}
catch (Exception e)
{
string failureMessage = "Failed to Create Lobby. Please try again.";
Utils.Log($"{k_DebugPrepend}{failureMessage}\n\n{e}", 1);
// Debug.LogWarning($"[XRMPT] {failureMessage}\n\n{e}");
OnLobbyFailed?.Invoke(failureMessage);
return null;
}
}
async Task SetupRelay(Lobby lobby)
{
m_Status.Value = "Connecting To Relay";
// Get the Join Allocation for the lobby based on the key
var alloc = await RelayService.Instance.JoinAllocationAsync(lobby.Data[k_JoinCodeKeyIdentifier].Value);
// Set the transport client data (IP, port, etc..)
m_Transport.SetClientRelayData
(
alloc.RelayServer.IpV4, (ushort)alloc.RelayServer.Port,
alloc.AllocationIdBytes, alloc.Key, alloc.ConnectionData, alloc.HostConnectionData
);
return;
}
QuickJoinLobbyOptions GetQuickJoinFilterOptions()
{
QuickJoinLobbyOptions options = new QuickJoinLobbyOptions();
// Create Filter Option to prevent showing any application versions that are not the same.
QueryFilter applicationVersionIdFilter = new QueryFilter(field: QueryFilter.FieldOptions.S1, value: Application.version, QueryFilter.OpOptions.EQ);
// Create Filter Option for different scenes.
QueryFilter sceneNameFilter = new QueryFilter(field: QueryFilter.FieldOptions.S2, value: SceneManager.GetActiveScene().name, QueryFilter.OpOptions.EQ);
// Create Filter Option for hiding editor created rooms from builds.
QueryFilter editorFilter = new QueryFilter(field: QueryFilter.FieldOptions.S3, value: hideEditorFromLobby.ToString(), QueryFilter.OpOptions.EQ);
options.Filter = new List<QueryFilter> { applicationVersionIdFilter, sceneNameFilter, editorFilter };
return options;
}
public async void ReconnectToLobby()
{
if (Application.isPlaying)
{
await LobbyService.Instance.ReconnectToLobbyAsync(m_ConnectedLobby.Id);
}
}
/// <summary>
/// This function will get a lobby based on the passed in parameters.
/// </summary>
/// <param name="lobby">Lobby to join.</param>
/// <param name="roomCode">Lobby Code to join with.</param>
/// <returns>Returns the Lobby.</returns>
async Task<Lobby> GetLobby(Lobby lobby = null, string roomCode = null)
{
if (roomCode != null)
{
// RATE LIMIT: 2 request per 6 seconds
Utils.Log($"{k_DebugPrepend} Getting lobby via Code: {roomCode}");
return await LobbyService.Instance.JoinLobbyByCodeAsync(roomCode);
}
else if (lobby != null)
{
// RATE LIMIT: 2 request per 6 seconds
Utils.Log($"{k_DebugPrepend} Getting lobby via Lobby Id: {lobby.Id}");
return await LobbyService.Instance.JoinLobbyByIdAsync(lobby.Id);
}
else
{
// RATE LIMIT: 1 request per 10 seconds
Utils.Log($"{k_DebugPrepend} Getting lobby via Quick Join");
return await LobbyService.Instance.QuickJoinLobbyAsync(GetQuickJoinFilterOptions());
}
}
/// <summary>
/// Called after setting transport data with relay allocations when either Creating a new lobby or joining an existing lobby.
/// </summary>
void ConnectedToLobby(Lobby lobby)
{
m_ConnectedLobby = lobby;
m_Status.Value = "Connected To Lobby";
}
/// <summary>
/// Heartbeat used to keep the lobby alive. By default the lobby will shut down after 30 seconds on inactivity.
/// </summary>
/// <param name="lobbyId">Id for the specific lobby to keep alive.</param>
/// <param name="waitTimeSeconds">Time to wait between pings.</param>
/// <returns></returns>
IEnumerator LobbyHeartbeatCoroutine(string lobbyId, float waitTimeSeconds = 15.0f)
{
// Setup a new wait based on wait time in seconds
var delay = new WaitForSecondsRealtime(waitTimeSeconds);
while (true)
{
// Continuously ping the lobby to keep it alive
LobbyService.Instance.SendHeartbeatPingAsync(lobbyId);
Utils.Log($"{k_DebugPrepend}Sending Heartbeat Ping for Lobby {lobbyId}");
yield return delay;
}
}
/// <summary>
/// Changes the existing lobbies name.
/// </summary>
/// <param name="lobbyName">Name to change the lobby to.</param>
public async void UpdateLobbyName(string lobbyName)
{
if (m_ConnectedLobby != null)
{
try
{
UpdateLobbyOptions options = new()
{
Name = lobbyName,
HostId = AuthenticationService.Instance.PlayerId
};
await LobbyService.Instance.UpdateLobbyAsync(m_ConnectedLobby.Id, options);
XRINetworkGameManager.ConnectedRoomName.Value = lobbyName;
}
catch (LobbyServiceException e)
{
Utils.Log($"{k_DebugPrepend}{e}");
}
}
else
{
Utils.Log($"{k_DebugPrepend}Connected Lobby is null");
}
}
/// <summary>
/// Updates the privacy setting for the current room.
/// </summary>
/// <param name="privateRoom">Whether or not to make the room private.</param>
public async void UpdateRoomPrivacy(bool privateRoom)
{
if (m_ConnectedLobby != null)
{
try
{
UpdateLobbyOptions options = new()
{
IsPrivate = privateRoom
};
await LobbyService.Instance.UpdateLobbyAsync(m_ConnectedLobby.Id, options);
}
catch (LobbyServiceException e)
{
Utils.Log($"{k_DebugPrepend}{e}");
}
}
else
{
Utils.Log($"{k_DebugPrepend}Connected Lobby is null");
}
}
/// <summary>
/// Called when leaving a room.
/// If Hosting, this function deletes the lobby for everyone.
/// If a client, this function removes the client from the lobby.
/// </summary>
/// <param name="playerId"></param>
/// <returns></returns>
public async Task<bool> RemovePlayerFromLobby(string playerId)
{
// Stop heartbeat if active (only runs on host)
if (m_HeartBeatRoutine != null) StopCoroutine(m_HeartBeatRoutine);
try
{
if (m_ConnectedLobby != null)
{
// Check if Lobby Host is current Player
if (m_ConnectedLobby.HostId == playerId)
{
// Delete Lobby if current owner
Utils.Log($"{k_DebugPrepend}Owner of lobby, shutting down.");
await LobbyService.Instance.DeleteLobbyAsync(m_ConnectedLobby.Id);
m_ConnectedLobby = null;
}
else
{
//Remove from lobby
await RemoveFromLobby(playerId);
}
return true;
}
}
catch (Exception e)
{
Utils.Log($"{k_DebugPrepend}Error on Lobby Shutdown:\n\n {e}");
}
return false;
}
/// <summary>
/// Attempts to remove the current player from the lobby
/// </summary>
async Task<bool> RemoveFromLobby(string playerId)
{
// If lobby id exists try to remove player from
if (!string.IsNullOrEmpty(m_ConnectedLobby.Id))
{
try
{
await LobbyService.Instance.RemovePlayerAsync(m_ConnectedLobby.Id, playerId);
m_ConnectedLobby = null;
Utils.Log($"{k_DebugPrepend}Successfully removed player from Lobby.");
return true;
}
catch (Exception e)
{
Utils.Log($"{k_DebugPrepend}Failed to remove player from lobby.\n\n{e}");
}
}
return false;
}
public static async Task<QueryResponse> GetLobbiesAsync()
{
// Use these options to apply things like filters, ordering, etc...
// Additionally you can add your own filters like below to have more control over the data.
QueryLobbiesOptions lobbyOptions = new QueryLobbiesOptions();
return await LobbyService.Instance.QueryLobbiesAsync(lobbyOptions);
}
public static bool CheckForLobbyFilter(Lobby lobby)
{
// If the lobby is not in the same scene, skip it
if (lobby.Data.TryGetValue(k_SceneKeyIdentifier, out DataObject sceneData))
{
if (sceneData.Value != SceneManager.GetActiveScene().name)
{
return true;
}
}
if (lobby.Data.TryGetValue(k_EditorKeyIdentifier, out DataObject editorData))
{
// If the lobby is an editor lobby is set to filter return true
if (editorData.Value == "True" & !s_HideEditorInLobbies)
{
return true;
}
}
return false;
}
public static bool CheckForIncompatibilityFilter(Lobby lobby)
{
if (lobby.Data.TryGetValue(k_BuildIdKeyIdentifier, out DataObject data))
{
//Filter out lobbies that are on different build versions
if (data.Value != Application.version)
{
return true;
}
}
return false;
}
public static bool CanJoinLobby(Lobby lobby)
{
return (XRINetworkGameManager.Instance.lobbyManager.connectedLobby == null) ||
(XRINetworkGameManager.Instance.lobbyManager.connectedLobby != null && lobby.Id != XRINetworkGameManager.Instance.lobbyManager.connectedLobby.Id);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4fc9ddf205bd8784da3ae5681e15741d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,79 @@
using Unity.Netcode;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace XRMultiplayer
{
/// <summary>
/// Manages the network functionality for VR multiplayer.
/// </summary>
public class NetworkManagerVRMultiplayer : NetworkManager
{
[SerializeField, Tooltip("Set this to control how much logging is generated")]
LogLevel m_LogLevel;
[SerializeField, Tooltip("This should almost always be set to true")]
bool m_RunInBackground = true;
[SerializeField]
NetworkConfig m_NetworkConfig;
///<inheritdoc/>
void Awake()
{
LogLevel = m_LogLevel;
RunInBackground = m_RunInBackground;
NetworkConfig = m_NetworkConfig;
Utils.s_LogLevel = LogLevel;
}
}
#if UNITY_EDITOR
[CustomEditor(typeof(NetworkManagerVRMultiplayer))]
class VRMutliplayerTemplateNetworkManagerEditor : Editor
{
/// <summary>
/// This function is called when the inspector is drawn.
/// </summary>
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
if (Application.isPlaying)
{
switch (XRINetworkGameManager.CurrentConnectionState.Value)
{
case XRINetworkGameManager.ConnectionState.None:
GUILayout.Box("Authenticating");
break;
case XRINetworkGameManager.ConnectionState.Authenticating:
GUILayout.Box("Authenticating");
break;
case XRINetworkGameManager.ConnectionState.Authenticated:
if (GUILayout.Button("Connect"))
{
XRINetworkGameManager.Instance.QuickJoinLobby();
}
break;
case XRINetworkGameManager.ConnectionState.Connecting:
GUILayout.Box("Connecting");
break;
case XRINetworkGameManager.ConnectionState.Connected:
if (GUILayout.Button("Disconnect"))
{
XRINetworkGameManager.Instance.Disconnect();
}
break;
}
}
else
{
GUILayout.Box("Game not running.");
}
}
}
#endif
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ccc99996a191dc34aab62c66e4aa42b6
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,534 @@
using System.Collections.Generic;
using System.Text;
using Unity.Netcode;
using Unity.Services.Vivox;
using Unity.XR.CoreUtils.Bindings.Variables;
using UnityEngine;
using UnityEngine.Android;
using System.Collections;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace XRMultiplayer
{
/// <summary>
/// Manages the Vivox Voice Chat functionality in the VR Multiplayer template.
/// </summary>
public class VoiceChatManager : MonoBehaviour
{
/// <summary>
/// String used to notify the player that they need to enable microphone permissions.
/// </summary>
const string k_MicrophonePersmissionDialogue = "Microphone Permissions Required.";
/// <summary>
public static BindableVariable<bool> s_HasMicrophonePermission = new(false);
/// <summary>
/// Dictionary of all the <see cref="XRINetworkPlayer"/>'s in the voice chat.
/// </summary>
public static Dictionary<string, XRINetworkPlayer> m_PlayersDictionary = new();
/// <summary>
/// This is the bindable variable for subscribing to the local player muting themselves.
/// </summary>
public IReadOnlyBindableVariable<bool> selfMuted
{
get => m_SelfMuted;
}
readonly BindableVariable<bool> m_SelfMuted = new(false);
/// <summary>
/// This is the bindable variable for subscribing to the connection status of the voice chat service.
/// </summary>
public IReadOnlyBindableVariable<string> connectionStatus
{
get => m_ConnectionStatus;
}
readonly BindableVariable<string> m_ConnectionStatus = new();
/// <summary>
/// The chat capability of the channel, by default it should Audio Only.
/// </summary>
[SerializeField, Tooltip("The chat capability of the channel, by default it should Audio Only")] ChatCapability m_ChatCapability = ChatCapability.AudioOnly;
/// <summary>
/// Update frequency for audio callbacks.
/// </summary>
[SerializeField, Tooltip("Update frequency for audio callbacks")] ParticipantPropertyUpdateFrequency m_UpdateFrequency = ParticipantPropertyUpdateFrequency.TenPerSecond;
/// <summary>
/// The maximum distance from the listener that a speaker can be heard.
/// </summary>
public int AudibleDistance
{
get => m_AudibleDistance;
set => m_AudibleDistance = value;
}
[Header("Voice Chat Properties")]
[SerializeField, Tooltip("The maximum distance from the listener that a speaker can be heard.")]
int m_AudibleDistance = 32;
/// <summary>
/// The distance from the listener within which a speakers voice is heard at its original volume, and beyond which the speaker's voice begins to fade.
/// </summary>
public int ConversationalDistance
{
get => m_ConversationalDistance;
set => m_ConversationalDistance = value;
}
[SerializeField, Tooltip("The distance from the listener within which a speakers voice is heard at its original volume, and beyond which the speaker's voice begins to fade.")]
int m_ConversationalDistance = 7;
/// <summary>
/// The strength of the audio fade effect as the speaker moves away from the listener past the conversational distance.
/// </summary>
public float AudioFadeIntensity
{
get => m_AudioFadeIntensity;
set => m_AudioFadeIntensity = value;
}
[SerializeField, Tooltip("The strength of the audio fade effect as the speaker moves away from the listener past the conversational distance.")]
float m_AudioFadeIntensity = 1.0f;
/// <summary>
/// The model that determines the distance falloff of the voice chat.
/// </summary>
/// The strength of the audio fade effect as the speaker moves away from the listener past the conversational distance.
/// </summary>
public AudioFadeModel AudioFadeModel
{
get => m_AudioFadeModel;
set => m_AudioFadeModel = value;
}
[SerializeField, Tooltip("The model that determines the distance falloff of the voice chat.")]
AudioFadeModel m_AudioFadeModel = AudioFadeModel.LinearByDistance;
/// <summary>
/// The minimum and maximum volume for the voice output.
/// </summary>
[SerializeField, Tooltip("The minimum and maximum volume for the voice output.")] Vector2 m_MinMaxVoiceOutputVolume = new Vector2(-10.0f, 10.0f);
/// <summary>
/// The minimum and maximum volume for the voice input.
/// </summary>
[SerializeField, Tooltip("The minimum and maximum volume for the voice input.")] Vector2 m_MinMaxVoiceInputVolume = new Vector2(-10.0f, 10.0f);
/// <summary>
/// The local participant in the voice chat.
/// </summary>
VivoxParticipant m_LocalParticpant;
/// <summary>
/// The current lobby id the player is connected to.
/// </summary>
string m_CurrentLobbyId;
/// <summary>
/// If the player is connected to a room.
/// </summary>
bool m_ConnectedToRoom;
/// <summary>
/// If the voice chat service is initialized.
/// </summary>
bool m_IsInitialized;
const string k_DebugPrepend = "<color=#0CFAFA>[Voice Chat Manager]</color> ";
///<inheritdoc/>
private void Awake()
{
m_ConnectedToRoom = false;
XRINetworkGameManager.CurrentConnectionState.Subscribe(ConnectionStateUpdated);
XRINetworkGameManager.Connected.Subscribe(ConnectedToGame);
}
///<inheritdoc/>
private void OnDestroy()
{
if (VivoxService.Instance != null)
{
VivoxService.Instance.LoggedIn -= LocalUserLoggedIn;
UnbindParticipantEvents();
}
}
/// <summary>
/// Callback for when the local player connection state is updated.
/// </summary>
/// <param name="connected">Wether or not a player is connected.</param>
void ConnectedToGame(bool connected)
{
if (!m_IsInitialized) return;
if (connected)
{
Login(XRINetworkGameManager.AuthenicationId, XRINetworkGameManager.Instance.lobbyManager.connectedLobby.Id);
}
else
{
LogOut();
}
}
void ConnectionStateUpdated(XRINetworkGameManager.ConnectionState connectionState)
{
if (!m_IsInitialized && connectionState == XRINetworkGameManager.ConnectionState.Authenticated)
{
Utils.Log($"{k_DebugPrepend}Initializing Voice Chat");
m_ConnectionStatus.Value = "Initializing Voice Service";
m_IsInitialized = true;
EnableVoiceChat();
if (!Permission.HasUserAuthorizedPermission(Permission.Microphone))
{
StartCoroutine(ShowPermissionsAfterDelay());
}
else
{
MicrophonePermissionGranted();
}
}
}
IEnumerator ShowPermissionsAfterDelay(float delay = 1.0f)
{
Utils.Log($"{k_DebugPrepend}Requesting Microphone Permissions");
PlayerHudNotification.Instance.ShowText("Requesting Microphone Permissions", 3.0f);
yield return new WaitForSeconds(delay);
PermissionCallbacks permissionCallbacks = new();
permissionCallbacks.PermissionDenied += PermissionDeniedCallback;
permissionCallbacks.PermissionGranted += PermissionGrantedCallback;
Permission.RequestUserPermission(Permission.Microphone, permissionCallbacks);
}
void PermissionGrantedCallback(string permissionName)
{
if (permissionName == Permission.Microphone)
{
MicrophonePermissionGranted();
}
}
void PermissionDeniedCallback(string permissionName)
{
if (permissionName == Permission.Microphone)
{
PlayerHudNotification.Instance.ShowText("Microphone Permissions Denied", 3.0f);
}
}
void MicrophonePermissionGranted()
{
Utils.Log($"{k_DebugPrepend}Microphone Permissions Granted");
s_HasMicrophonePermission.Value = true;
PlayerHudNotification.Instance.ShowText("Microphone Permissions Granted", 3.0f);
}
public async void EnableVoiceChat()
{
try
{
await VivoxService.Instance.InitializeAsync();
m_ConnectionStatus.Value = "Voice Service Initialized";
VivoxService.Instance.LoggedIn += LocalUserLoggedIn;
BindToParticipantEvents();
}
catch (System.Exception e)
{
#if UNITY_EDITOR
EditorGUI.hyperLinkClicked += HyperlinkClicked;
Utils.Log($"{k_DebugPrepend}Vivox Initialization Failed. Please check the Vivox Service Window <a data=\"OpenVivoxSettings\"><b>Project Settings > Services > Vivox</b></a>\n\n{e}", 2);
#else
Utils.Log($"{k_DebugPrepend}Vivox Initialization Failed.\n\n{e}", 2);
#endif
}
}
#if UNITY_EDITOR
void HyperlinkClicked(EditorWindow window, HyperLinkClickedEventArgs args)
{
if(args.hyperLinkData.ContainsValue("OpenVivoxSettings"))
{
SettingsService.OpenProjectSettings("Project/Services/Vivox");
}
}
#endif
public async void Login(string displayName, string roomCode)
{
m_CurrentLobbyId = roomCode;
LoginOptions loginOptions = new()
{
DisplayName = displayName,
ParticipantUpdateFrequency = m_UpdateFrequency
};
if (VivoxService.Instance.IsLoggedIn)
{
Utils.Log($"{k_DebugPrepend}Logging out of Voice Chat");
m_ConnectionStatus.Value = "Logging out of Voice Chat";
await VivoxService.Instance.LogoutAsync();
}
if (!VivoxService.Instance.IsLoggedIn)
{
Utils.Log($"{k_DebugPrepend}Logging In to room {roomCode} as {displayName}");
m_ConnectionStatus.Value = "Logging In To Voice Service";
await VivoxService.Instance.LoginAsync(loginOptions);
}
else
{
Utils.Log($"{k_DebugPrepend}Attempting to login to voice chat while already logged in.", 1);
}
}
void LocalUserLoggedIn()
{
if (VivoxService.Instance.IsLoggedIn)
{
Utils.Log($"{k_DebugPrepend}Local User Logged In to Voice Chat.");
m_ConnectionStatus.Value = "Joining Voice Channel";
ConnectToVoiceChannel();
}
}
public async void ConnectToVoiceChannel()
{
if (NetworkManager.Singleton.IsConnectedClient & !m_ConnectedToRoom)
{
Channel3DProperties properties = new(AudibleDistance, ConversationalDistance, AudioFadeIntensity, AudioFadeModel);
Utils.Log($"{k_DebugPrepend}Joining Voice Channel: {m_CurrentLobbyId}, properties: {properties}");
await VivoxService.Instance.JoinPositionalChannelAsync(m_CurrentLobbyId, m_ChatCapability, properties);
// Once connecting, make sure we are still in the game session, if not, disconnect from the voice chat.
if (!NetworkManager.Singleton.IsConnectedClient)
{
Disconnect();
}
}
else
{
Utils.Log($"{k_DebugPrepend}Failed to join Voice Chat, Player is not connected to a game", 1);
}
}
void BindToParticipantEvents()
{
VivoxService.Instance.ParticipantAddedToChannel += OnParticipantAdded;
VivoxService.Instance.ParticipantRemovedFromChannel += OnParticipantRemoved;
}
void UnbindParticipantEvents()
{
VivoxService.Instance.ParticipantAddedToChannel -= OnParticipantAdded;
VivoxService.Instance.ParticipantRemovedFromChannel -= OnParticipantRemoved;
}
async void DisconnectAsync()
{
m_ConnectionStatus.Value = "Leaving current channel";
await VivoxService.Instance.LeaveAllChannelsAsync();
}
[ContextMenu("Reconnect")]
public void Reconnect()
{
ReconnectAsync();
}
async void ReconnectAsync()
{
m_ConnectionStatus.Value = "Leaving current channel";
await VivoxService.Instance.LeaveAllChannelsAsync();
if (VivoxService.Instance.IsLoggedIn)
{
ConnectToVoiceChannel();
}
else
{
m_ConnectionStatus.Value = "Reconnecting to Voice Chat";
Login(XRINetworkGameManager.AuthenicationId, XRINetworkGameManager.Instance.lobbyManager.connectedLobby.Id);
}
}
public void LogOut()
{
Utils.Log($"{k_DebugPrepend}Logging out of Voice Chat.");
if (VivoxService.Instance.IsLoggedIn && m_ConnectedToRoom)
{
m_ConnectedToRoom = false;
VivoxService.Instance.LeaveAllChannelsAsync();
VivoxService.Instance.LogoutAsync();
}
m_PlayersDictionary.Clear();
}
public void Set3DAudio(Transform localPlayerHeadTransform)
{
if (VivoxService.Instance.IsLoggedIn && VivoxService.Instance.ActiveChannels.Count > 0 && VivoxService.Instance.TransmittingChannels[0] == m_CurrentLobbyId)
{
VivoxService.Instance.Set3DPosition(localPlayerHeadTransform.position,
localPlayerHeadTransform.position,
localPlayerHeadTransform.forward,
localPlayerHeadTransform.up,
m_CurrentLobbyId);
}
}
public void ToggleSelfMute(bool setManual = false, bool mutedOverrideValue = false)
{
if (Permission.HasUserAuthorizedPermission(Permission.Microphone))
{
m_SelfMuted.Value = setManual ? mutedOverrideValue : !m_SelfMuted.Value;
}
else
{
m_SelfMuted.Value = false;
}
if (VivoxService.Instance.IsLoggedIn)
{
if (m_SelfMuted.Value)
{
VivoxService.Instance.MuteInputDevice();
}
else
{
VivoxService.Instance.UnmuteInputDevice();
}
}
else
{
OfflinePlayerAvatar.muted = m_SelfMuted.Value;
}
if (!Permission.HasUserAuthorizedPermission(Permission.Microphone))
{
PlayerHudNotification.Instance.ShowText(k_MicrophonePersmissionDialogue, 3.0f);
}
}
public void SetInputVolume(float volume)
{
volume = Mathf.Clamp(volume, m_MinMaxVoiceInputVolume.x, m_MinMaxVoiceInputVolume.y);
VivoxService.Instance.SetInputDeviceVolume((int)volume);
// Since the slider goes to .001 percent, add a buffer to mute the mic
if (volume <= (m_MinMaxVoiceInputVolume.x + .05f))
{
ToggleSelfMute(true, true);
}
else
{
ToggleSelfMute(true, false);
}
}
public void SetOutputVolume(float volume)
{
volume = Mathf.Clamp(volume, m_MinMaxVoiceOutputVolume.x, m_MinMaxVoiceOutputVolume.y);
VivoxService.Instance.SetOutputDeviceVolume((int)volume);
}
void OnParticipantAdded(VivoxParticipant participant)
{
if (participant.IsSelf)
{
m_ConnectedToRoom = true;
m_LocalParticpant = participant;
m_SelfMuted.Value = false;
XRINetworkPlayer.LocalPlayer.SetVoiceId(m_LocalParticpant.PlayerId);
Utils.Log($"{k_DebugPrepend}Joined Voice Channel: {m_CurrentLobbyId}");
m_ConnectionStatus.Value = "Joined Voice Channel";
PlayerHudNotification.Instance.ShowText("Joined Voice Chat", 3.0f);
}
else
{
Utils.Log($"{k_DebugPrepend}Non-Local Player Joined Voice Channel: {participant.PlayerId}");
foreach (XRINetworkPlayer player in FindObjectsByType<XRINetworkPlayer>(FindObjectsSortMode.None))
{
if (player.playerVoiceId == participant.PlayerId)
{
player.SetupVoicePlayer();
}
}
}
}
void OnParticipantRemoved(VivoxParticipant participant)
{
RemoveVivoxPlayer(participant.PlayerId);
if (participant.IsSelf)
{
Utils.Log($"{k_DebugPrepend}Left Voice Channel: {m_CurrentLobbyId}");
m_ConnectionStatus.Value = "Left Voice Channel";
m_ConnectedToRoom = false;
m_LocalParticpant = null;
PlayerHudNotification.Instance.ShowText("Voice Chat Disconnected", 3.0f);
}
}
public VivoxParticipant GetVivoxParticipantById(string participantPlayerId)
{
foreach (var participant in VivoxService.Instance.ActiveChannels[m_CurrentLobbyId])
{
if (participantPlayerId == participant.PlayerId)
return participant;
}
return null;
}
// Gets called as soon as participant ID is synced
public static void AddNewVivoxPlayer(string participantID, XRINetworkPlayer networkPlayer)
{
if (!m_PlayersDictionary.ContainsKey(participantID))
{
m_PlayersDictionary.Add(participantID, networkPlayer);
}
else
{
Utils.Log($"{k_DebugPrepend}Attempting to load multiple players with same id {participantID}", 1);
}
}
public static void RemoveVivoxPlayer(string participantID)
{
if (participantID == XRINetworkPlayer.LocalPlayer.playerVoiceId)
{
Utils.Log($"{k_DebugPrepend}Local Player Left Voice Chat.");
return;
}
if (m_PlayersDictionary.ContainsKey(participantID))
{
m_PlayersDictionary.Remove(participantID);
}
}
[ContextMenu("Disconnect")]
public void Disconnect()
{
DisconnectAsync();
}
[ContextMenu("Debug Particpants")]
void DebugParticipants()
{
StringBuilder output = new StringBuilder();
output.Append($"[Room Type: Positional\n[Room Code: {m_CurrentLobbyId}]");
foreach (var participant in VivoxService.Instance.ActiveChannels[m_CurrentLobbyId])
{
output.Append($"\n[ParticipantID: {participant.PlayerId}]\n[AudioEnergy: {participant.AudioEnergy}]");
}
Utils.Log($"{k_DebugPrepend}{output}");
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ef0c7cbc48b68dd40923508f2547cc98
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,667 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Unity.Netcode;
using Unity.Services.Lobbies.Models;
using Unity.XR.CoreUtils.Bindings.Variables;
using UnityEngine;
using Unity.Services.Lobbies;
using UnityEditor;
namespace XRMultiplayer
{
#if USE_FORCED_BYTE_SERIALIZATION
/// <summary>
/// Workaround for a bug introduced in NGO 1.9.1.
/// </summary>
/// <remarks> Delete this class once the bug is fixed in NGO.</remarks>
class ForceByteSerialization : NetworkBehaviour
{
NetworkVariable<byte> m_ForceByteSerialization;
}
#endif
/// <summary>
/// Manages the high level connection for a networked game session.
/// </summary>
[RequireComponent(typeof(LobbyManager)), RequireComponent(typeof(AuthenticationManager))]
public class XRINetworkGameManager : NetworkBehaviour
{
/// <summary>
/// Determines the current state of the networked game connection.
/// </summary>
///<remarks>
/// None: No connection state.
/// Authenticating: Currently authenticating.
/// Authenticated: Authenticated.
/// Connecting: Currently connecting to a lobby.
/// Connected: Connected to a lobby.
/// </remarks>
public enum ConnectionState
{
None,
Authenticating,
Authenticated,
Connecting,
Connected
}
/// <summary>
/// Max amount of players allowed when creating a new room.
/// </summary>
public const int maxPlayers = 20;
/// <summary>
/// Singleton Reference for access to this manager.
/// </summary>
public static XRINetworkGameManager Instance => s_Instance;
static XRINetworkGameManager s_Instance;
/// <summary>
/// OwnerClientId that gets set for the local player when connecting to a game.
/// </summary>
public static ulong LocalId;
/// <summary>
/// Authentication Id that gets passed once Authenticated.
/// </summary>
public static string AuthenicationId;
/// <summary>
/// Internal Room Code set by Lobby.
/// </summary>
public static string ConnectedRoomCode;
/// <summary>
/// Current connected region set by Lobby and Relay.
/// </summary>
public static string ConnectedRoomRegion;
/// <summary>
/// Bindable Variable that gets updated when changing the the currently connected room.
/// </summary>
public static BindableVariable<string> ConnectedRoomName = new("");
/// <summary>
/// Bindable Variable that gets updated when the local player changes name.
/// </summary>
public static BindableVariable<string> LocalPlayerName = new("Player");
/// <summary>
/// Bindable Variable that gets updated when the local player changes color.
/// </summary>
public static BindableVariable<Color> LocalPlayerColor = new(Color.white);
/// <summary>
/// Bindable Variable that gets updated when a player connects or disconnects from a networked game.
/// </summary>
public static IReadOnlyBindableVariable<bool> Connected
{
get => m_Connected;
}
static BindableVariable<bool> m_Connected = new BindableVariable<bool>(false);
/// <summary>
/// Bindable Variable that gets updated throughout the authentication and connection process.
/// See <see cref="ConnectionState"/>
/// </summary>
public static IReadOnlyBindableVariable<ConnectionState> CurrentConnectionState
{
get => m_ConnectionState;
}
static BindableEnum<ConnectionState> m_ConnectionState = new BindableEnum<ConnectionState>(ConnectionState.None);
/// <summary>
/// Auto connects to the player to a networked game session once they connect to a lobby.
/// Uncheck if you want to handle joining a networked session separately.
/// </summary>
public bool autoConnectOnLobbyJoin { get => m_AutoConnectOnLobbyJoin; }
[SerializeField] bool m_AutoConnectOnLobbyJoin = true;
/// <summary>
/// Flag for updating positional voice chat.
/// </summary>
/// <remarks>
/// This will be removed in the future with the Vivox v16 update.
/// </remarks>
public bool positionalVoiceChat = false;
/// <summary>
/// Action for when a player connects or disconnects.
/// </summary>
public Action<ulong, bool> playerStateChanged;
/// <summary>
/// Action for when connection status is updated.
/// </summary>
public Action<string> connectionUpdated;
/// <summary>
/// Action for when connection fails.
/// </summary>
public Action<string> connectionFailedAction;
/// <summary>
/// Lobby Manager handles the Lobby and Relay work between players.
/// </summary>
public LobbyManager lobbyManager => m_LobbyManager;
LobbyManager m_LobbyManager;
/// <summary>
/// Lobby Manager handles the Lobby and Relay work between players.
/// </summary>
public AuthenticationManager authenticationManager => m_AuthenticationManager;
AuthenticationManager m_AuthenticationManager;
/// <summary>
/// List that handles all current players by ID.
/// Useful for getting specific players.
/// See <see cref="GetPlayerByID"/>
/// </summary>
readonly List<ulong> m_CurrentPlayerIDs = new();
/// <summary>
/// Flagged whenever the application is in the process of shutting down.
/// </summary>
bool m_IsShuttingDown = false;
const string k_DebugPrepend = "<color=#FAC00C>[Network Game Manager]</color> ";
/// <summary>
/// See <see cref="MonoBehaviour"/>.
/// </summary>
protected virtual async void Awake()
{
// Check for existing singleton reference. If once already exists early out.
if (s_Instance != null)
{
Utils.Log($"{k_DebugPrepend}Duplicate XRINetworkGameManager found, destroying.", 2);
Destroy(gameObject);
return;
}
s_Instance = this;
// Check for Lobby Manager, if none exist, early out.
if (TryGetComponent(out m_LobbyManager) && TryGetComponent(out m_AuthenticationManager))
{
m_LobbyManager.OnLobbyFailed += ConnectionFailed;
}
else
{
Utils.Log($"{k_DebugPrepend}Missing Managers, Disabling Component", 2);
enabled = false;
return;
}
#if UNITY_EDITOR
if(!CloudProjectSettings.projectBound)
{
Utils.Log($"{k_DebugPrepend}Project has not been linked to Unity Cloud." +
"\nThe VR Multiplayer Template utilizes Unity Gaming Services and must be linked to Unity Cloud." +
"\nGo to <b>Settings -> Project Settings -> Services</b> and link your project.", 2);
return;
}
#endif
// Initialize bindable variables.
m_Connected.Value = false;
// Update connection state.
m_ConnectionState.Value = ConnectionState.Authenticating;
// Wait for Authentication to complete.
bool signedIn = await Authenticate();
if (!signedIn)
{
Utils.Log($"{k_DebugPrepend}Failed to Authenticate.", 1);
ConnectionFailed("Failed to Authenticate.");
PlayerHudNotification.Instance.ShowText($"Failed to Authenticate.");
}
else
{
// Update connection state.
m_ConnectionState.Value = ConnectionState.Authenticated;
}
}
/// <summary>
/// See <see cref="MonoBehaviour"/>.
/// </summary>
protected virtual void Start()
{
NetworkManager.Singleton.OnClientStopped += OnLocalClientStopped;
}
/// <summary>
/// See <see cref="MonoBehaviour"/>.
/// </summary>
public override void OnDestroy()
{
base.OnDestroy();
ShutDown();
}
/// <summary>
/// See <see cref="MonoBehaviour"/>.
/// </summary>
private void OnApplicationQuit()
{
ShutDown();
}
async void ShutDown()
{
if (m_IsShuttingDown) return;
m_IsShuttingDown = true;
// Remove callbacks
if (NetworkManager.Singleton != null)
{
NetworkManager.Singleton.OnClientStopped -= OnLocalClientStopped;
}
// Shutdown lobby if owner, remove from lobby if not owner.
await m_LobbyManager.RemovePlayerFromLobby(AuthenicationId);
}
public async Task<bool> Authenticate()
{
return await m_AuthenticationManager.Authenticate();
}
public bool IsAuthenticated()
{
return AuthenticationManager.IsAuthenticated();
}
/// <summary>
/// Called from XRINetworkPlayer once they have spawned.
/// </summary>
/// <param name="localPlayerId">Sets based on <see cref="NetworkObject.OwnerClientId"/> from the local player</param>
public virtual void LocalPlayerConnected(ulong localPlayerId)
{
m_Connected.Value = true;
LocalId = localPlayerId;
PlayerHudNotification.Instance.ShowText($"<b>Status:</b> Connected");
}
/// <summary>
/// Called when disconnected from any networked game.
/// </summary>
/// <param name="id">
/// Local player id.
/// </param>
protected virtual void OnLocalClientStopped(bool id)
{
m_Connected.Value = false;
m_CurrentPlayerIDs.Clear();
PlayerHudNotification.Instance.ShowText($"<b>Status:</b> Disconnected");
// Check if authenticated on disconnect.
if (IsAuthenticated())
{
m_ConnectionState.Value = ConnectionState.Authenticated;
}
else
{
m_ConnectionState.Value = ConnectionState.None;
}
}
/// <summary>
/// Finds all <see cref="XRINetworkPlayer"/>'s existing in the scene and gets the <see cref="XRINetworkPlayer"/>
/// based on <see cref="NetworkObject.OwnerClientId"/> for that player.
/// </summary>
/// <param name="id">
/// <see cref="NetworkObject.OwnerClientId"/> of the player.
/// </param>
/// <param name="player">
/// Out <see cref="XRINetworkPlayer"/>.
/// </param>
/// <returns>
/// Returns true based on whether or not a player with that Id exists.
/// </returns>
public virtual bool GetPlayerByID(ulong id, out XRINetworkPlayer player)
{
// Find all existing players in scene. This is a workaround until NGO exposes client side player list (2.x I believe - JG).
XRINetworkPlayer[] allPlayers = FindObjectsByType<XRINetworkPlayer>(FindObjectsSortMode.None);
//Loops through existing players and returns true if player with id is found.
foreach (XRINetworkPlayer p in allPlayers)
{
if (p.NetworkObject.OwnerClientId == id)
{
player = p;
return true;
}
}
player = null;
return false;
}
[ContextMenu("Show All NetworkClients")]
void ShowAllNetworkClients()
{
foreach (var client in NetworkManager.Singleton.ConnectedClients)
{
Debug.Log($"Client: {client.Key}, {client.Value.PlayerObject.name}");
}
}
/// <summary>
/// This function will set the player ID in the list <see cref="m_CurrentPlayerIDs"/> and
/// invokes the callback <see cref="playerStateChanged"/>.
/// </summary>
/// <param name="playerID"><see cref="NetworkObject.OwnerClientId"/> of the joined player.</param>
/// <remarks>Called from <see cref="XRINetworkPlayer.CompleteSetup"/>.</remarks>
public virtual void PlayerJoined(ulong playerID)
{
// If playerID is not already registered, then add.
if (!m_CurrentPlayerIDs.Contains(playerID))
{
m_CurrentPlayerIDs.Add(playerID);
playerStateChanged?.Invoke(playerID, true);
}
else
{
Utils.Log($"{k_DebugPrepend}Trying to Add a player ID [{playerID}] that already exists", 1);
}
}
/// <summary>
/// Called from <see cref="XRINetworkPlayer.OnDestroy"/>.
/// </summary>
/// <param name="playerID"><see cref="NetworkObject.OwnerClientId"/> of the player who left.</param>
public virtual void PlayerLeft(ulong playerID)
{
// Check to make sure player has been registerd.
if (m_CurrentPlayerIDs.Contains(playerID))
{
m_CurrentPlayerIDs.Remove(playerID);
playerStateChanged?.Invoke(playerID, false);
}
else
{
Utils.Log($"{k_DebugPrepend}Trying to remove a player ID [{playerID}] that doesn't exist", 1);
}
}
/// <summary>
/// Called whenever there is a problem with connecting to game or lobby.
/// </summary>
/// <param name="reason">Failure message.</param>
public virtual void ConnectionFailed(string reason)
{
connectionFailedAction?.Invoke(reason);
m_ConnectionState.Value = AuthenticationManager.IsAuthenticated() ? ConnectionState.Authenticated : ConnectionState.None;
}
/// <summary>
/// Called whenever there is an update to connection status.
/// </summary>
/// <param name="update">Status update message.</param>
public virtual void ConnectionUpdated(string update)
{
connectionUpdated?.Invoke(update);
}
/// <summary>
/// Joins a random lobby. If no lobbies exist, it will create a new one.
/// </summary>
public virtual async void QuickJoinLobby()
{
Utils.Log($"{k_DebugPrepend}Joining Lobby by Quick Join.");
if (await AbleToConnect())
{
ConnectToLobby(await m_LobbyManager.QuickJoinLobby());
}
}
/// <summary>
/// Called when trying to join a Lobby by Room Code.
/// </summary>
/// <param name="lobby">Lobby to join.</param>
public virtual async void JoinLobbyByCode(string code)
{
Utils.Log($"{k_DebugPrepend}Joining Lobby by room code: {code}.");
if (await AbleToConnect())
{
ConnectToLobby(await m_LobbyManager.JoinLobby(roomCode: code));
}
}
/// <summary>
/// Called when trying to join a specific Lobby.
/// </summary>
/// <param name="lobby">Lobby to join.</param>
public virtual async void JoinLobbySpecific(Lobby lobby)
{
Utils.Log($"{k_DebugPrepend}Joining specific Lobby: {lobby.Name}.");
if (await AbleToConnect())
{
ConnectToLobby(await m_LobbyManager.JoinLobby(lobby: lobby));
}
}
/// <summary>
/// Creates a new Lobby.
/// </summary>
/// <param name="roomName">Name of the lobby.</param>
/// <param name="isPrivate">Whether or not the lobby is private.</param>
/// <param name="playerCount">Maximum allowed players.</param>
public virtual async void CreateNewLobby(string roomName = null, bool isPrivate = false, int playerCount = maxPlayers)
{
Utils.Log($"{k_DebugPrepend}Creating New Lobby: {roomName}.");
if (await AbleToConnect())
{
ConnectToLobby(await m_LobbyManager.CreateLobby(roomName, isPrivate, playerCount));
}
}
/// <summary>
/// Checks if a we are currently able to connect to a lobby.
/// If already connected it will disconnect in attempt to "Hot Join" a new lobby.
/// </summary>
/// <returns>Whether or not we are able to connect.</returns>
protected virtual async Task<bool> AbleToConnect()
{
// If in the process of trying to connect, send failure message and return false.
if (m_ConnectionState.Value == ConnectionState.Connecting)
{
string failureMessage = "Connection attempt still in progress.";
Utils.Log($"{k_DebugPrepend}{failureMessage}", 1);
ConnectionFailed(failureMessage);
return false;
}
// If already connected to a lobby, disconnect in attempt to "Hot Join".
if (Connected.Value || m_ConnectionState.Value == ConnectionState.Connected)
{
Utils.Log($"{k_DebugPrepend}Already Connected to a Lobby. Disconnecting.", 0);
await DisconnectAsync();
// Small wait while everything finishes disconnecting.
// This isn't technically needed, but makes the flow feel better.
await Task.Delay(100);
}
m_ConnectionState.Value = ConnectionState.Connecting;
return true;
}
/// <summary>
/// Connect to a lobby.
/// </summary>
/// <param name="lobby">Lobby to connect to.</param>
protected virtual void ConnectToLobby(Lobby lobby)
{
// Send failure message if we can't connect.
if (lobby == null || !ConnectedToLobby())
{
FailedToConnect();
}
}
/// <summary>
/// Checks if we successfully connected to a Lobby.
/// If <see cref="autoConnectOnLobbyJoin"/> is enabled, join networked game here.
/// </summary>
/// <returns>Whether or not we connected to a lobby and / or networked game.</returns>
protected virtual bool ConnectedToLobby()
{
bool connected;
if (autoConnectOnLobbyJoin)
{
ConnectedRoomRegion = m_LobbyManager.connectedLobby.Data[LobbyManager.k_RegionKeyIdentifier].Value;
ConnectedRoomCode = m_LobbyManager.connectedLobby.LobbyCode;
ConnectedRoomName.Value = m_LobbyManager.connectedLobby.Name;
if (m_LobbyManager.connectedLobby.HostId == AuthenicationId)
{
connected = NetworkManager.Singleton.StartHost();
}
else
{
connected = NetworkManager.Singleton.StartClient();
}
}
else
{
connected = true;
//Players are connected to the lobby, but have not started a Networked Game session.
}
if (connected)
{
Utils.Log($"{k_DebugPrepend}Connected to game session. Lobby: {m_LobbyManager.connectedLobby.Name}.");
m_ConnectionState.Value = ConnectionState.Connected;
SubscribeToLobbyEvents();
}
else
{
Utils.Log($"{k_DebugPrepend}Failed to connect to lobby {m_LobbyManager.connectedLobby.Name}.");
m_LobbyManager.OnLobbyFailed?.Invoke($"Failed to connect to lobby {m_LobbyManager.connectedLobby.Name}.");
}
return connected;
}
/// <summary>
/// Subscribe to lobby update events. This needed to be informed of Lobby changes (name, privacy, etc...).
/// </summary>
/// <remarks>See <see cref="OnLobbyChanged(ILobbyChanges)"/>.</remarks>
protected virtual async void SubscribeToLobbyEvents()
{
var callbacks = new LobbyEventCallbacks();
callbacks.LobbyChanged += OnLobbyChanged;
callbacks.LobbyEventConnectionStateChanged += OnLobbyEventConnectionStateChanged;
try
{
await LobbyService.Instance.SubscribeToLobbyEventsAsync(m_LobbyManager.connectedLobby.Id, callbacks);
}
catch (LobbyServiceException ex)
{
switch (ex.Reason)
{
case LobbyExceptionReason.AlreadySubscribedToLobby: Utils.Log($"{k_DebugPrepend}Already subscribed to lobby[{m_LobbyManager.connectedLobby.Id}]. We did not need to try and subscribe again. Exception Message: {ex.Message}", 1); break;
case LobbyExceptionReason.SubscriptionToLobbyLostWhileBusy: Utils.Log($"{k_DebugPrepend}Subscription to lobby events was lost while it was busy trying to subscribe. Exception Message: {ex.Message}", 2); throw;
case LobbyExceptionReason.LobbyEventServiceConnectionError: Utils.Log($"{k_DebugPrepend}Failed to connect to lobby events. Exception Message: {ex.Message}", 2); throw;
default: throw;
}
}
}
/// <summary>
/// Callabacks for anytime the lobby event connection state has changed.
/// </summary>
/// <param name="state"></param>
private void OnLobbyEventConnectionStateChanged(LobbyEventConnectionState state)
{
switch (state)
{
case LobbyEventConnectionState.Unsubscribed: Utils.Log($"{k_DebugPrepend}Lobby event now Unsubscribed"); break;
case LobbyEventConnectionState.Subscribing: Utils.Log($"{k_DebugPrepend}Attempting to subscribe to lobby events"); break;
case LobbyEventConnectionState.Subscribed: Utils.Log($"{k_DebugPrepend}Subscribing to lobby events now"); break;
case LobbyEventConnectionState.Unsynced:
m_LobbyManager.ReconnectToLobby();
Utils.Log($"{k_DebugPrepend}Lobby Events now unsynced.\n\n{state}", 1);
break;
case LobbyEventConnectionState.Error: Utils.Log($"{k_DebugPrepend}Lobby event error.\n\n{state}", 2); break;
}
}
/// <summary>
/// Callback for anytime a lobby is updated via <see cref="LobbyService.Instance.SubscribeToLobbyEventsAsync"/>.
/// </summary>
/// <param name="changes"></param>
protected virtual void OnLobbyChanged(ILobbyChanges changes)
{
// Check for lobby deletion.
if (!changes.LobbyDeleted)
{
changes.ApplyToLobby(m_LobbyManager.connectedLobby);
// Update values based on lobby changes.
if (changes.Name.Changed)
{
ConnectedRoomName.Value = m_LobbyManager.connectedLobby.Name;
}
}
}
/// <summary>
/// Generic failure message.
/// </summary>
protected virtual void FailedToConnect(string reason = null)
{
string failureMessage = "Failed to connect to lobby.";
if (reason != null)
{
failureMessage = $"{reason}";
}
Utils.Log($"{k_DebugPrepend}{failureMessage}", 1);
}
/// <summary>
/// Cancel current matchmaking.
/// Called from the Lobby UI.
/// </summary>
public virtual async void CancelMatchmaking()
{
if (IsAuthenticated())
{
m_ConnectionState.Value = ConnectionState.Authenticated;
}
await m_LobbyManager.RemovePlayerFromLobby(AuthenicationId);
}
/// <summary>
/// High Level Disconnect call.
/// </summary>
public virtual async void Disconnect()
{
await DisconnectAsync();
}
/// <summary>
/// Awaitable Disconnect call, used for Hot Joining.
/// </summary>
/// <returns></returns>
public virtual async Task<bool> DisconnectAsync()
{
bool fullyDisconnected = await m_LobbyManager.RemovePlayerFromLobby(AuthenicationId);
m_Connected.Value = false;
NetworkManager.Shutdown();
if (IsAuthenticated())
{
m_ConnectionState.Value = ConnectionState.Authenticated;
}
else
{
m_ConnectionState.Value = ConnectionState.None;
}
Utils.Log($"{k_DebugPrepend}Disconnected from Game.");
return fullyDisconnected;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ad97d272b98331644876b2289d56dd4a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: bfd350b190e77814db5d9da8fb666367
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,85 @@
using UnityEngine;
namespace XRMultiplayer
{
/// <summary>
/// This class is used to represent the Avatar IK system in both the <see cref="XRINetworkPlayer"/> and the Offline Player"/>.
/// </summary>
public class XRAvatarIK : MonoBehaviour
{
/// <summary>
/// Transform for the Network Player Head.
/// </summary>
[SerializeField, Tooltip("Transform for the Network Player Head.")] Transform m_HeadTransform;
/// <summary>
/// Torso Parent Transform.
/// </summary>
[SerializeField, Tooltip("Torso Parent Transform.")] Transform m_TorsoParentTransform;
/// <summary>
/// Root of the Head Visuals.
/// </summary>
[SerializeField, Tooltip("Root of the Head Visuals.")] Transform m_HeadVisualsRoot;
/// <summary>
/// Neck Transform.
/// </summary>
[SerializeField, Tooltip("Neck Transform.")] Transform m_Neck;
/// <summary>
/// Offset to be applied to the head height.
/// </summary>
[SerializeField, Tooltip("Offset to be applied to the head height.")] float m_HeadHeightOffset = .3f;
/// <summary>
/// Theshold to where body rotation appoximation is applied.
/// </summary>
[Range(0, 360.0f)]
[SerializeField, Tooltip("Theshold to where body rotation appoximation is applied.")] float m_RotateThreshold = 25.0f;
/// <summary>
/// Speed at which the body rotates.
/// </summary>
[SerializeField, Tooltip("Speed at which the body rotates.")] float m_RotateSpeed = 3.0f;
/// <summary>
/// Transform associated with this script.
/// </summary>
Transform m_Transform;
/// <summary>
/// Rotation destination for the Y euler value.
/// </summary>
float m_DestinationY;
/// <inheritdoc/>
private void Start()
{
m_Transform = GetComponent<Transform>();
m_DestinationY = m_HeadTransform.transform.eulerAngles.y;
}
/// <inheritdoc/>
private void Update()
{
// Update Head.
m_HeadVisualsRoot.position = m_HeadTransform.position;
m_HeadVisualsRoot.position -= m_HeadTransform.up * m_HeadHeightOffset;
m_Neck.rotation = m_HeadTransform.rotation;
// Update Body.
m_Transform.position = m_HeadTransform.position;
m_TorsoParentTransform.rotation = Quaternion.Slerp(m_TorsoParentTransform.rotation, Quaternion.Euler(new Vector3(0, m_DestinationY, 0)), Time.deltaTime * m_RotateSpeed);
// Rotate Body if past threshold.
if (Mathf.Abs(m_TorsoParentTransform.eulerAngles.y - m_HeadTransform.eulerAngles.y) >= m_RotateThreshold)
{
m_DestinationY = m_HeadTransform.transform.eulerAngles.y;
}
// Update scale.
m_HeadVisualsRoot.localScale = m_HeadTransform.localScale;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: aa392b809b089674e82ce792ca5f7dbb
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,127 @@
using System;
using UnityEngine;
namespace XRMultiplayer
{
[RequireComponent(typeof(XRINetworkPlayer))]
public class XRAvatarVisuals : MonoBehaviour
{
/// <summary>
/// Head Renderers to change rendering mode for local players.
/// </summary>
[Header("Renderer References"), SerializeField, Tooltip("Head Renderers to change rendering mode for local players.")]
protected Renderer[] m_HeadRends;
/// <summary>
/// Head Renderer to control the blend shape for mouth movement. Also updates shirt color based on <see cref="playerColor"/>.
/// </summary>
[SerializeField, Tooltip("Head Renderer to drive mouth movement blendshape and player shirt color.")]
protected SkinnedMeshRenderer m_headRend;
/// <summary>
/// GameObject to enable to show what player is the Room Host.
/// </summary>
[Header("Host Visuals"), SerializeField, Tooltip("GameObject that gets enabled for the Host only.")]
protected GameObject m_HostVisuals;
/// <summary>
/// GameObject to enable to show what player is the Room Host.
/// </summary>
[SerializeField, Tooltip("Show Host Visuals.")]
protected bool m_ShowHostVisuals = true;
/// <summary>
/// Materials to swap for the local player.
/// </summary>
[Header("Local Player Material Swap"), SerializeField]
protected LocalPlayerMaterialSwap m_LocalPlayerMaterialSwap;
/// <summary>
/// Reference to the attached XRINetworkPlayerAvatar component.
/// </summary>
protected XRINetworkPlayer m_NetworkPlayerAvatar;
public virtual void Awake()
{
if (!TryGetComponent(out m_NetworkPlayerAvatar))
{
Utils.LogError("XRAvatarVisuals requires a XRINetworkPlayerAvatar component to be attached to the same GameObject. Disabling this component now.");
enabled = false;
return;
}
m_NetworkPlayerAvatar.onSpawnedLocal += PlayerSpawnedLocal;
m_NetworkPlayerAvatar.onSpawnedAll += PlayerSpawnedAll;
m_NetworkPlayerAvatar.onColorUpdated += SetPlayerColor;
}
public virtual void OnDestroy()
{
m_NetworkPlayerAvatar.onSpawnedLocal -= PlayerSpawnedLocal;
m_NetworkPlayerAvatar.onSpawnedAll -= PlayerSpawnedAll;
m_NetworkPlayerAvatar.onColorUpdated -= SetPlayerColor;
}
public virtual void Update()
{
UpdateMouth();
}
public virtual void UpdateMouth()
{
if (m_headRend != null)
m_headRend.SetBlendShapeWeight(0, 100 - (m_NetworkPlayerAvatar.playerVoiceAmp * 100));
}
public virtual void PlayerSpawnedLocal()
{
m_LocalPlayerMaterialSwap.SwapMaterials();
int layer = LayerMask.NameToLayer("Mirror");
foreach (var r in m_HeadRends)
{
r.gameObject.layer = layer;
r.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.On;
}
}
public virtual void PlayerSpawnedAll()
{
m_HostVisuals.SetActive(m_ShowHostVisuals && m_NetworkPlayerAvatar.IsOwnedByServer);
}
public virtual void SetPlayerColor(Color newColor)
{
m_headRend.materials[2].SetColor("_BaseColor", newColor);
}
}
}
[Serializable]
/// <summary>
/// Helper class for swapping the local player to standard materials from the dithering materials.
/// </summary>
public class LocalPlayerMaterialSwap
{
public Renderer headRend;
public Renderer hmdRend;
public Renderer hostRend;
public Renderer[] hands;
public Material[] headMaterials;
public Material[] hmdMaterials;
public Material hostMaterial;
public Material handMaterial;
public void SwapMaterials()
{
for (int i = 0; i < hands.Length; i++)
{
hands[i].material = handMaterial;
}
hmdRend.materials = hmdMaterials;
headRend.materials = headMaterials;
hostRend.material = hostMaterial;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 054a6b4527d98b446aa238d86cd24270
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,592 @@
using System.Collections.Generic;
using Unity.Netcode;
using Unity.XR.CoreUtils;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.XR.Hands;
using UnityEngine.XR.Interaction.Toolkit.Inputs;
namespace XRMultiplayer
{
/// <summary>
/// This class will synchronize the hand poses over the network.
/// It will also allow for the user to control the fidelity of the hand poses and how much data is being sent over the network.
/// This class is going to have major changes in the near future based on the Hand Pose work being done.
/// </summary>
public class XRHandPoseReplicator : NetworkBehaviour
{
/// <summary>
/// Controls the level of fidelity and how much data is being sent over the network.
/// 0 is highest level of fidelity and the most bandwidth, 2 is the lowest and the least bandwidth.
/// </summary>
[Header("Hands and Fingers"), Tooltip("0 is highest, 2 is lowest")]
[Range(0, 2), SerializeField] int m_FidelityLevel;
[SerializeField, Tooltip("Determines minimum value threshold for updating finger rotations")] float m_MinUpdateDelta = .1f;
[SerializeField] JointBasedHand[] m_HandCurler;
[SerializeField] float m_FingerLerpSpeed = 20.0f;
[SerializeField] bool m_UpdateHandsLocally;
[Header("Controller Inputs")]
[SerializeField] InputActionProperty[] m_GripInputProperties;
[SerializeField] InputActionProperty[] m_TriggerInputProperties;
[SerializeField] InputActionProperty[] m_ThumbTouchProperties;
NetworkList<Vector3> m_FingerRotationsLeft;
NetworkList<Vector3> m_FingerRotationsRight;
NetworkList<float> m_FingerCurlLeft;
NetworkList<float> m_FingerCurlRight;
NetworkVariable<bool> m_IsInitialized = new NetworkVariable<bool>(false, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner);
Pose[] m_handTrackedStartPose = new Pose[2];
[Header("Offsets")]
[SerializeField] Vector3[] m_HandControllerOffsets;
[SerializeField] Vector3[] m_HandControllerEulerOffsets;
HandFidelityOption[] m_LocalHandFidelityOptions;
public XRInputModalityManager.InputMode trackingType { get => m_TrackingType.Value; }
readonly NetworkVariable<XRInputModalityManager.InputMode> m_TrackingType = new(XRInputModalityManager.InputMode.None, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner);
XRInputModalityManager m_XRModalityManager;
XROrigin m_XROrigin;
Transform m_LeftHandTransformReference;
Transform m_RightHandTransformReference;
Transform m_LeftControllerTransformReference;
Transform m_RightControllerTransformReference;
/// <summary>
/// Internal references to the Local Player Transforms.
/// </summary>
protected Transform m_LeftHandOrigin, m_RightHandOrigin;
private void Awake()
{
m_FingerRotationsLeft = new NetworkList<Vector3>(new List<Vector3>(), NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner);
m_FingerRotationsRight = new NetworkList<Vector3>(new List<Vector3>(), NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner);
m_FingerCurlLeft = new NetworkList<float>(new List<float>(), NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner);
m_FingerCurlRight = new NetworkList<float>(new List<float>(), NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner);
for (int i = 0; i < m_HandCurler.Length; i++)
{
m_handTrackedStartPose[i].position = m_HandCurler[i].transform.localPosition;
m_handTrackedStartPose[i].rotation = m_HandCurler[i].transform.localRotation;
}
}
private void OnEnable()
{
m_TrackingType.OnValueChanged += UpdateTrackingType;
}
private void OnDisable()
{
m_TrackingType.OnValueChanged -= UpdateTrackingType;
}
public override void OnDestroy()
{
base.OnDestroy();
if (IsOwner)
{
m_XRModalityManager.trackedHandModeStarted.RemoveListener(SwapToHands);
m_XRModalityManager.motionControllerModeStarted.RemoveListener(SwapToControllers);
}
}
private void Update()
{
if (!m_IsInitialized.Value || !NetworkManager.IsConnectedClient || NetworkManager.ShutdownInProgress) return;
if (trackingType == XRInputModalityManager.InputMode.TrackedHand)
{
switch (m_FidelityLevel)
{
case 0:
SyncAllFingerData();
break;
case 1:
SyncFingerCurl();
break;
case 2:
SyncFingerCurlLimited();
break;
}
}
else
{
SyncControllerTracking();
}
}
public override void OnNetworkSpawn()
{
base.OnNetworkSpawn();
if (IsOwner)
{
m_XROrigin = FindFirstObjectByType<XROrigin>();
if (m_XROrigin.TryGetComponent(out m_XRModalityManager))
{
SetupLocalHands();
}
SetupLocalFingerReferences();
}
else
{
ChangeControllerType(m_TrackingType.Value);
}
}
void UpdateTrackingType(XRInputModalityManager.InputMode old, XRInputModalityManager.InputMode current)
{
ChangeControllerType(current);
}
void SetupLocalHands()
{
m_LeftControllerTransformReference = m_XRModalityManager.leftController.transform;
m_RightControllerTransformReference = m_XRModalityManager.rightController.transform;
if (m_XRModalityManager.leftHand == null) //Rig doesn't have hands setup
{
m_LeftHandTransformReference = m_XRModalityManager.leftController.transform;
m_RightHandTransformReference = m_XRModalityManager.rightController.transform;
SetTrackingType(XRInputModalityManager.InputMode.MotionController);
}
else //Setup Hands and modality change listeners
{
m_LeftHandTransformReference = m_XRModalityManager.leftHand.GetComponentInChildren<XRHandSkeletonDriver>().rootTransform;
m_RightHandTransformReference = m_XRModalityManager.rightHand.GetComponentInChildren<XRHandSkeletonDriver>().rootTransform;
SetTrackingType(XRInputModalityManager.currentInputMode.Value);
m_XRModalityManager.trackedHandModeStarted.AddListener(SwapToHands);
m_XRModalityManager.motionControllerModeStarted.AddListener(SwapToControllers);
}
}
void SetupLocalFingerReferences()
{
XRInputModalityManager modalityMangager = FindFirstObjectByType<XRInputModalityManager>();
// Early out if hands are not setup
if (modalityMangager.leftHand == null)
{
// Set Default values for finger rotations
for (int i = 0; i < 3; i++)
{
m_FingerCurlLeft.Add(0.0f);
m_FingerCurlRight.Add(0.0f);
}
m_IsInitialized.Value = true;
ChangeControllerType(XRInputModalityManager.InputMode.MotionController);
return;
}
XRHandSkeletonDriver localLeftHandSkeleton = modalityMangager.leftHand.GetComponentInChildren<XRHandSkeletonDriver>();
XRHandSkeletonDriver localRightHandSkeleton = modalityMangager.rightHand.GetComponentInChildren<XRHandSkeletonDriver>();
m_LocalHandFidelityOptions = new HandFidelityOption[2];
for (int i = 0; i < m_LocalHandFidelityOptions.Length; i++)
{
m_LocalHandFidelityOptions[i].fingerJoints = new FingerJoints[5];
// Loop through each finger and setup name and joints
for (int j = 0; j < m_LocalHandFidelityOptions[i].fingerJoints.Length; j++)
{
m_LocalHandFidelityOptions[i].fingerJoints[j].fingerName = m_HandCurler[i].handFidelityOptions[0].fingerJoints[j].fingerName;
m_LocalHandFidelityOptions[i].fingerJoints[j].jointTransformReferences = new List<JointToTransformReference>();
// Loop through each joint in the finger and setup the joint references
for (int k = 0; k < m_HandCurler[i].handFidelityOptions[0].fingerJoints[j].jointTransformReferences.Count; k++)
{
XRHandSkeletonDriver currentHandSkeletonDriver = i % 2 == 0 ? localLeftHandSkeleton : localRightHandSkeleton;
// Loop through each local hand and look up the joint reference
foreach (var localJoint in currentHandSkeletonDriver.jointTransformReferences)
{
if (m_HandCurler[i].handFidelityOptions[0].fingerJoints[j].jointTransformReferences[k].xrHandJointID == localJoint.xrHandJointID)
{
m_LocalHandFidelityOptions[i].fingerJoints[j].jointTransformReferences.Add(localJoint);
break;
}
}
}
}
}
foreach (var fingerSync in m_LocalHandFidelityOptions[0].fingerJoints)
{
foreach (var joint in fingerSync.jointTransformReferences)
{
m_FingerRotationsLeft.Add(joint.jointTransform.eulerAngles);
}
}
foreach (var fingerSync in m_LocalHandFidelityOptions[1].fingerJoints)
{
foreach (var joint in fingerSync.jointTransformReferences)
{
m_FingerRotationsRight.Add(joint.jointTransform.eulerAngles);
}
}
// Set Default values for finger curl
foreach (var fingerSync in m_LocalHandFidelityOptions[0].fingerJoints)
{
m_FingerCurlLeft.Add(0.0f);
}
foreach (var fingerSync in m_LocalHandFidelityOptions[1].fingerJoints)
{
m_FingerCurlRight.Add(0.0f);
}
m_IsInitialized.Value = true;
SetFidelity(m_FidelityLevel);
}
public void ChangeControllerType(XRInputModalityManager.InputMode inputMode)
{
if (inputMode == XRInputModalityManager.InputMode.MotionController)
{
SetFidelity(2);
SetHandsToControllerOffset();
}
else
{
SetFidelity(m_FidelityLevel);
ResetHandsToStart();
}
}
void SetFidelity(int fidelity)
{
fidelity = Mathf.Clamp(fidelity, 0, 2);
m_HandCurler[0].fidelityLevel = fidelity;
m_HandCurler[1].fidelityLevel = fidelity;
m_HandCurler[0].useCurl = fidelity > 0;
m_HandCurler[1].useCurl = fidelity > 0;
}
void SyncAllFingerData()
{
if (IsOwner)
{
SetNetworkFingerRotations();
if (m_UpdateHandsLocally)
{
GetNetworkFingerRotations();
}
}
else
{
GetNetworkFingerRotations();
}
}
void SetNetworkFingerRotations()
{
int currentIdx = 0;
for (int i = 0; i < m_LocalHandFidelityOptions[0].fingerJoints.Length; i++)
{
for (int j = 0; j < m_LocalHandFidelityOptions[0].fingerJoints[i].jointTransformReferences.Count; j++)
{
m_FingerRotationsLeft[currentIdx++] =
m_LocalHandFidelityOptions[0].fingerJoints[i].jointTransformReferences[j].jointTransform.eulerAngles;
}
}
currentIdx = 0;
for (int i = 0; i < m_LocalHandFidelityOptions[1].fingerJoints.Length; i++)
{
for (int j = 0; j < m_LocalHandFidelityOptions[1].fingerJoints[i].jointTransformReferences.Count; j++)
{
m_FingerRotationsRight[currentIdx++] =
m_LocalHandFidelityOptions[1].fingerJoints[i].jointTransformReferences[j].jointTransform.eulerAngles;
}
}
}
void GetNetworkFingerRotations()
{
int currentIdx = 0;
int hand = 0;
for (int i = 0; i < m_HandCurler[hand].handFidelityOptions[0].fingerJoints.Length; i++)
{
for (int j = 0; j < m_HandCurler[hand].handFidelityOptions[0].fingerJoints[i].jointTransformReferences.Count; j++)
{
m_HandCurler[hand].handFidelityOptions[0].fingerJoints[i].jointTransformReferences[j].jointTransform.rotation =
Quaternion.Slerp(m_HandCurler[hand].handFidelityOptions[0].fingerJoints[i].jointTransformReferences[j].jointTransform.rotation,
Quaternion.Euler(m_FingerRotationsLeft[currentIdx++]),
Time.deltaTime * m_FingerLerpSpeed);
}
}
currentIdx = 0;
hand = 1;
for (int i = 0; i < m_HandCurler[hand].handFidelityOptions[0].fingerJoints.Length; i++)
{
for (int j = 0; j < m_HandCurler[hand].handFidelityOptions[0].fingerJoints[i].jointTransformReferences.Count; j++)
{
m_HandCurler[hand].handFidelityOptions[0].fingerJoints[i].jointTransformReferences[j].jointTransform.rotation =
Quaternion.Slerp(m_HandCurler[hand].handFidelityOptions[0].fingerJoints[i].jointTransformReferences[j].jointTransform.rotation,
Quaternion.Euler(m_FingerRotationsRight[currentIdx++]),
Time.deltaTime * m_FingerLerpSpeed);
}
}
}
void SyncFingerCurl()
{
if (IsOwner)
{
SetNetworkCurl();
if (m_UpdateHandsLocally)
{
GetNetworkCurl();
}
}
else
{
GetNetworkCurl();
}
}
void SetNetworkCurl()
{
for (int i = 0; i < m_LocalHandFidelityOptions[0].fingerJoints.Length; i++)
{
SetLocalNetworkFingerCurl(0, i, GetAverageX(0, i));
}
for (int i = 0; i < m_LocalHandFidelityOptions[1].fingerJoints.Length; i++)
{
SetLocalNetworkFingerCurl(1, i, GetAverageX(1, i));
}
}
void GetNetworkCurl()
{
for (int i = 0; i < m_HandCurler[0].handFidelityOptions[0].fingerJoints.Length; i++)
{
m_HandCurler[0].SetCurl(i, m_FingerCurlLeft[i]);
}
for (int i = 0; i < m_HandCurler[1].handFidelityOptions[0].fingerJoints.Length; i++)
{
m_HandCurler[1].SetCurl(i, m_FingerCurlRight[i]);
}
}
float GetAverageX(int hand, int finger)
{
float x = 0;
int digitCount = 4;
if (finger == 0) //Thumbs have 1 less joint
{
digitCount--;
}
for (int i = 1; i < digitCount; i++)
{
float currentX = m_LocalHandFidelityOptions[hand].fingerJoints[finger].jointTransformReferences[i].jointTransform.localEulerAngles.x;
if (currentX < 0 || currentX > 180)
{
currentX = 0;
}
x += currentX;
}
float avg = Mathf.Clamp(x / (digitCount - 1), 0, 100);
return avg / 100;
}
void SyncFingerCurlLimited()
{
if (IsOwner)
{
SetNetworkCurlLimited();
if (m_UpdateHandsLocally)
{
GetNetworkCurlLimited();
}
}
else
{
GetNetworkCurlLimited();
}
}
void SetNetworkCurlLimited()
{
for (int i = 0; i < 2; i++)
{
SetLocalNetworkFingerCurl(0, i, GetAverageX(0, i));
}
for (int i = 0; i < 2; i++)
{
SetLocalNetworkFingerCurl(1, i, GetAverageX(1, i));
}
SetLocalNetworkFingerCurl(0, 2, GetAverageXCombined(0));
SetLocalNetworkFingerCurl(1, 2, GetAverageXCombined(1));
}
void GetNetworkCurlLimited()
{
for (int i = 0; i < 3; i++)
{
m_HandCurler[0].SetCurl(i, m_FingerCurlLeft[i]);
}
for (int i = 0; i < 3; i++)
{
m_HandCurler[1].SetCurl(i, m_FingerCurlRight[i]);
}
}
float GetAverageXCombined(int hand)
{
float x = 0;
int digitCount = 4;
int startFinger = 2;
int endFinger = 5;
int count = 0;
for (int i = startFinger; i < endFinger; i++)
{
for (int j = 1; j < digitCount; j++)
{
float currentX = m_LocalHandFidelityOptions[hand].fingerJoints[i].jointTransformReferences[j].jointTransform.localEulerAngles.x;
if (currentX < 0 || currentX > 180)
{
currentX = 0;
}
x += currentX;
count++;
}
}
float avg = Mathf.Clamp(x / count, 0, 100);
return avg / 100;
}
void SyncControllerTracking()
{
//TODO: Sync Controller Input and map to hand poses
if (IsOwner)
{
SetNetworkControllerFingerSync();
if (m_UpdateHandsLocally)
{
GetNetworkedControllerFingerSync();
}
}
else
{
GetNetworkedControllerFingerSync();
}
}
void SetNetworkControllerFingerSync()
{
SetLocalNetworkFingerCurl(0, 0, m_ThumbTouchProperties[0].action?.ReadValue<float>() ?? 0.0f);
SetLocalNetworkFingerCurl(0, 1, m_TriggerInputProperties[0].action?.ReadValue<float>() ?? 0.0f);
SetLocalNetworkFingerCurl(0, 2, m_GripInputProperties[0].action?.ReadValue<float>() ?? 0.0f);
SetLocalNetworkFingerCurl(1, 0, m_ThumbTouchProperties[1].action?.ReadValue<float>() ?? 0.0f);
SetLocalNetworkFingerCurl(1, 1, m_TriggerInputProperties[1].action?.ReadValue<float>() ?? 0.0f);
SetLocalNetworkFingerCurl(1, 2, m_GripInputProperties[1].action?.ReadValue<float>() ?? 0.0f);
}
void SetLocalNetworkFingerCurl(int hand, int finger, float value)
{
if (hand == 0)
{
if (Mathf.Abs(m_FingerCurlLeft[finger] - value) > m_MinUpdateDelta)
m_FingerCurlLeft[finger] = value;
}
else
{
if (Mathf.Abs(m_FingerCurlRight[finger] - value) > m_MinUpdateDelta)
m_FingerCurlRight[finger] = value;
}
}
void GetNetworkedControllerFingerSync()
{
GetNetworkCurlLimited();
}
void SwapToHands()
{
SetTrackingType(XRInputModalityManager.InputMode.TrackedHand);
}
void SwapToControllers()
{
SetTrackingType(XRInputModalityManager.InputMode.MotionController);
}
public void SetTrackingType(XRInputModalityManager.InputMode trackingType)
{
m_TrackingType.Value = trackingType;
if (trackingType == XRInputModalityManager.InputMode.MotionController)
{
m_LeftHandOrigin = m_LeftControllerTransformReference;
m_RightHandOrigin = m_RightControllerTransformReference;
}
else
{
m_LeftHandOrigin = m_LeftHandTransformReference;
m_RightHandOrigin = m_RightHandTransformReference;
}
XRINetworkPlayer.LocalPlayer.SetHandOrigins(m_LeftHandOrigin, m_RightHandOrigin);
}
void ResetHandsToStart()
{
for (int i = 0; i < m_HandCurler.Length; i++)
{
m_HandCurler[i].transform.localPosition = m_handTrackedStartPose[i].position;
m_HandCurler[i].transform.localRotation = m_handTrackedStartPose[i].rotation;
}
}
void SetHandsToControllerOffset()
{
for (int i = 0; i < m_HandCurler.Length; i++)
{
m_HandCurler[i].transform.localPosition = m_HandControllerOffsets[i];
m_HandCurler[i].transform.localRotation = Quaternion.Euler(m_HandControllerEulerOffsets[i]);
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 79af78f95e61cdd458d2f24b1a8356fb
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,449 @@
using UnityEngine;
using Unity.Netcode;
using Unity.XR.CoreUtils;
using Unity.Collections;
using System;
using Unity.Services.Vivox;
using Unity.XR.CoreUtils.Bindings.Variables;
namespace XRMultiplayer
{
/// <summary>
/// XRINetworkPlayer class used for simple interactions.
/// </summary>
public class XRINetworkPlayer : NetworkBehaviour
{
/// <summary>
/// Speed at which voice amplitude changes.
/// </summary>
const float k_VoiceAmplitudeSpeed = 15.0f;
/// <summary>
/// Singleton Reference for the Local Player.
/// </summary>
public static XRINetworkPlayer LocalPlayer;
[Header("Avatar Transform References"), Tooltip("Assign to local avatar transform.")]
/// <summary>
/// Non-Local player transforms.
/// </summary>
public Transform head;
/// <summary>
/// Non-Local player transforms.
/// </summary>
public Transform leftHand;
/// <summary>
/// Non-Local player transforms.
/// </summary>
public Transform rightHand;
/// <summary>
/// Action called when the player name is updated.
/// </summary>
public Action<string> onNameUpdated;
/// <summary>
/// Action called when the player color is updated.
/// </summary>
public Action<Color> onColorUpdated;
/// <summary>
/// Action called when the Local Player is finished spawning in.
/// </summary>
public Action onSpawnedLocal;
/// <summary>
/// Action called when the Local Player is finished spawning in.
/// </summary>
public Action onSpawnedAll;
/// <summary>
/// Action called when the player color is updated.
/// </summary>
public Action<XRINetworkPlayer> onDisconnected;
/// <summary>
/// Bindable Variable used for other clients to mute this user locally.
/// </summary>
public BindableVariable<bool> squelched = new BindableVariable<bool>(false);
/// <summary>
/// Current Voice Amplitude driven from Vivox.
/// </summary>
public float playerVoiceAmp { get => m_VoiceAmplitudeCurrent; }
float m_VoiceAmplitudeCurrent;
/// <summary>
/// Player Voice Id string that reads from the internal NetworkVariable for the Player Voice Id.
/// </summary>
public string playerVoiceId { get => m_PlayerVoiceId.Value.ToString(); }
readonly NetworkVariable<FixedString128Bytes> m_PlayerVoiceId = new("", NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner);
/// <summary>
/// Player Name string that reads from the internal NetworkVariable for the Player Name.
/// </summary>
public string playerName { get => m_PlayerName.Value.ToString(); }
readonly NetworkVariable<FixedString128Bytes> m_PlayerName = new("", NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner);
/// <summary>
/// Player Color that reads from the internal NetworkVariable for the Player Color.
/// </summary>
public Color playerColor { get => m_PlayerColor.Value; }
readonly NetworkVariable<Color> m_PlayerColor = new(Color.white, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner);
[HideInInspector] public readonly NetworkVariable<bool> selfMuted = new(false, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner);
/// <summary>
/// Player Name Tag.
/// </summary>
[Header("Player Name Tag"), SerializeField, Tooltip("Player Name Tag.")] protected bool m_UpdateObjectName = true;
// /// <summary>
// /// Head Renderers to change rendering mode for local players.
// /// </summary>
// [SerializeField, Tooltip("Head Renderers to change rendering mode for local players.")] protected Renderer[] m_HeadRends;
/// <summary>
/// Hand Objects to be disabled for the local player.
/// </summary>
[Header("Networked Hands"), SerializeField, Tooltip("Hand Objects to be disabled for the local player.")] protected GameObject[] m_handsObjects;
/// <summary>
/// Player Name Tag.
/// </summary>
[Header("Player Name Tag"), SerializeField, Tooltip("Player Name Tag.")] protected PlayerNameTag m_PlayerNameTag;
/// <summary>
/// Internal references to the Local Player Transforms.
/// </summary>
protected Transform m_LeftHandOrigin, m_RightHandOrigin, m_HeadOrigin;
/// <summary>
/// Reference to the local player XR Origin
/// </summary>
protected XROrigin m_XROrigin;
/// <summary>
/// If the player has been connected to the the game.
/// </summary>
protected bool m_InitialConnected = false;
/// <summary>
/// Reference to the VoiceChatManager.
/// </summary>
protected VoiceChatManager m_VoiceChat;
/// <summary>
/// Reference to the VivoxParticipant.
/// </summary>
protected VivoxParticipant m_VivoxParticipant;
/// <summary>
/// Time to update the voice position.
/// </summary>
protected float m_VoicePositionUpdateTime = .1f, m_VoiceUpdatePosotionDelta = .05f;
/// <summary>
/// Destination for the voice amplitude.
/// </summary>
protected float m_VoiceAmplitudeDestination;
/// <summary>
/// Timer to check the voice position.
/// </summary>
protected float m_VoicePositionCheckTimer;
/// <summary>
/// Previous position of the head.
/// </summary>
protected Vector3 m_PrevHeadPos;
protected void Awake()
{
m_VoiceChat = FindFirstObjectByType<VoiceChatManager>();
m_VoicePositionCheckTimer = m_VoicePositionUpdateTime;
}
///<inheritdoc/>
protected virtual void OnEnable()
{
m_PlayerName.OnValueChanged += UpdatePlayerName;
m_PlayerColor.OnValueChanged += UpdatePlayerColor;
}
///<inheritdoc/>
protected virtual void OnDisable()
{
m_PlayerName.OnValueChanged -= UpdatePlayerName;
m_PlayerColor.OnValueChanged -= UpdatePlayerColor;
}
///<inheritdoc/>
protected virtual void Update()
{
if (IsOwner && XRINetworkGameManager.Instance.positionalVoiceChat)
{
if (Time.time > m_VoicePositionCheckTimer)
{
m_VoicePositionCheckTimer += m_VoicePositionUpdateTime;
if (Vector3.Distance(m_PrevHeadPos, m_HeadOrigin.position) > m_VoiceUpdatePosotionDelta)
{
m_PrevHeadPos = m_HeadOrigin.position;
if (XRINetworkGameManager.Instance.positionalVoiceChat)
{
m_VoiceChat.Set3DAudio(m_HeadOrigin);
}
}
}
}
m_VoiceAmplitudeCurrent = Mathf.Lerp(m_VoiceAmplitudeCurrent, m_VoiceAmplitudeDestination, Time.deltaTime * k_VoiceAmplitudeSpeed);
}
///<inheritdoc/>
protected virtual void LateUpdate()
{
if (!IsOwner) return;
// Set transforms to be replicated with ClientNetworkTransforms
leftHand.SetPositionAndRotation(m_LeftHandOrigin.position, m_LeftHandOrigin.rotation);
rightHand.SetPositionAndRotation(m_RightHandOrigin.position, m_RightHandOrigin.rotation);
head.SetPositionAndRotation(m_HeadOrigin.position, m_HeadOrigin.rotation);
}
///<inheritdoc/>
public override void OnDestroy()
{
base.OnDestroy();
if (IsOwner)
{
// Local Name unsubscribe.
XRINetworkGameManager.LocalPlayerName.Unsubscribe(UpdateLocalPlayerName);
XRINetworkGameManager.LocalPlayerColor.Unsubscribe(UpdateLocalPlayerColor);
m_VoiceChat.selfMuted.Unsubscribe(SelfMutedChanged);
}
else if (NetworkManager.Singleton != null && NetworkManager.Singleton.IsConnectedClient)
{
// Inform Network Manager that player left current session.
XRINetworkGameManager.Instance.PlayerLeft(NetworkObject.OwnerClientId);
}
// Unsubscribe from color updating.
m_PlayerColor.OnValueChanged -= UpdatePlayerColor;
}
///<inheritdoc/>
public override void OnNetworkSpawn()
{
base.OnNetworkSpawn();
if (IsOwner)
{
// Set Local Player.
LocalPlayer = this;
XRINetworkGameManager.Instance.LocalPlayerConnected(NetworkObject.OwnerClientId);
// Get Origin and set head.
m_XROrigin = FindFirstObjectByType<XROrigin>();
if (m_XROrigin != null)
{
m_HeadOrigin = m_XROrigin.Camera.transform;
}
else
{
Utils.Log("No XR Rig Available", 1);
}
SetupLocalPlayer();
}
CompleteSetup();
}
public override void OnNetworkDespawn()
{
base.OnNetworkDespawn();
PlayerHudNotification.Instance.ShowText($"<b>{m_PlayerName.Value}</b> left");
onDisconnected?.Invoke(this);
}
/// <summary>
/// Called from <see cref="XRHandPoseReplicator"/> when swapping between hand tracking and controllers.
/// </summary>
/// <param name="left">Transform for Left Hand.</param>
/// <param name="right">Transform for Right Hand.</param>
public void SetHandOrigins(Transform left, Transform right)
{
m_LeftHandOrigin = left;
m_RightHandOrigin = right;
}
/// <summary>
/// Hides and disables Renderers and GameObjects on the Local Player.
/// Also sets the initial values for <see cref="m_PlayerColor"/> and <see cref="m_PlayerName"/>.
/// Finally we subscribe to any updates for Color and Name.
/// </summary>
/// <remarks>Only called on the Local Player.</remarks>
protected virtual void SetupLocalPlayer()
{
foreach (var hand in m_handsObjects)
{
hand.SetActive(false);
}
m_PlayerColor.Value = XRINetworkGameManager.LocalPlayerColor.Value;
m_PlayerName.Value = new FixedString128Bytes(XRINetworkGameManager.LocalPlayerName.Value);
XRINetworkGameManager.LocalPlayerColor.Subscribe(UpdateLocalPlayerColor);
XRINetworkGameManager.LocalPlayerName.Subscribe(UpdateLocalPlayerName);
m_VoiceChat.selfMuted.Subscribe(SelfMutedChanged);
m_VoiceChat.ToggleSelfMute(true, true);
onSpawnedLocal?.Invoke();
}
/// <summary>
/// Called from the local player only
/// </summary>
/// <param name="muted"></param>
void SelfMutedChanged(bool muted)
{
selfMuted.Value = muted;
}
/// <summary>
/// Callback for the bindable variable <see cref="XRINetworkGameManager.LocalPlayerColor"/>.
/// </summary>
/// <param name="color">New Color for player.</param>
/// <remarks>Only called on Local Player.</remarks>
protected virtual void UpdateLocalPlayerColor(Color color)
{
m_PlayerColor.Value = XRINetworkGameManager.LocalPlayerColor.Value;
}
/// <summary>
/// Callback for the bindable variable <see cref="XRINetworkGameManager.LocalPlayerName"/>.
/// </summary>
/// <param name="name">New Name for player.</param>
/// <remarks>Only called on Local Player.</remarks>
protected virtual void UpdateLocalPlayerName(string name)
{
m_PlayerName.Value = new FixedString128Bytes(XRINetworkGameManager.LocalPlayerName.Value);
}
/// <summary>
/// Called when the player object is finished being setup.
/// </summary>
void CompleteSetup()
{
// Add player to XRINetworkManager.
XRINetworkGameManager.Instance.PlayerJoined(NetworkObject.OwnerClientId);
// Update Color and Name.
UpdatePlayerColor(Color.white, m_PlayerColor.Value);
UpdatePlayerName(new FixedString128Bytes(""), m_PlayerName.Value);
// Check if WorldCanvas exists
WorldCanvas worldCanvas = FindFirstObjectByType<WorldCanvas>();
if (worldCanvas != null)
{
// If we are using a World Canvas, reparent name tag and destroy local canvas.
Canvas localCanvas = m_PlayerNameTag.GetComponentInParent<Canvas>();
worldCanvas.SetupPlayerNameTag(this, m_PlayerNameTag);
Destroy(localCanvas.gameObject);
}
else
{
// If we are not using a World Canvas, setup the name tag for local use.
m_PlayerNameTag.SetupNameTag(this);
}
onSpawnedAll?.Invoke();
}
/// <summary>
/// Callback anytime the local player sets <see cref="m_PlayerName"/>.
/// </summary><remarks>Invokes the callback <see cref="onNameUpdated"/>.</remarks>
void UpdatePlayerName(FixedString128Bytes oldName, FixedString128Bytes currentName)
{
onNameUpdated?.Invoke(currentName.ToString());
if (!m_InitialConnected & !string.IsNullOrEmpty(currentName.ToString()))
{
m_InitialConnected = true;
if (!IsLocalPlayer)
PlayerHudNotification.Instance.ShowText($"<b>{playerName}</b> joined");
}
if (m_UpdateObjectName)
gameObject.name = currentName.ToString();
}
/// <summary>
/// Callback when the local player sets <see cref="m_PlayerColor"/>.
/// </summary><remarks>Invokes the callback <see cref="onColorUpdated"/>.</remarks>
void UpdatePlayerColor(Color oldColor, Color newColor)
{
onColorUpdated?.Invoke(newColor);
}
void UpdatePlayerVoiceEnergy(float current)
{
m_VoiceAmplitudeDestination = Mathf.Clamp01(current);
}
/// <summary>
/// Called when new players connect to the game and set their initial <see cref="m_PlayerVoiceId"/>
/// and when <see cref="VoiceChatManager.OnParticipantAdded(VivoxParticipant)"/> is called for existing players.
/// </summary>
public void SetupVoicePlayer()
{
m_VivoxParticipant = m_VoiceChat.GetVivoxParticipantById(playerVoiceId);
if (m_VivoxParticipant != null)
{
m_VivoxParticipant.ParticipantAudioEnergyChanged += ParticipantAudioEnergyChanged;
}
else
{
Utils.Log($"No Participant with id: {playerVoiceId}", 1);
}
if (!VoiceChatManager.m_PlayersDictionary.ContainsKey(playerVoiceId))
{
VoiceChatManager.AddNewVivoxPlayer(playerVoiceId, this);
}
}
private void ParticipantAudioEnergyChanged()
{
UpdatePlayerVoiceEnergy((float)m_VivoxParticipant.AudioEnergy);
}
public void SetVoiceId(string voiceId)
{
if (!IsOwner) return;
m_PlayerVoiceId.Value = new FixedString128Bytes(voiceId);
SetupVoicePlayer();
if (XRINetworkGameManager.Instance.positionalVoiceChat)
{
m_VoiceChat.Set3DAudio(m_HeadOrigin);
}
}
/// <summary>
/// Called from clients to mute this player locally for that client.
/// </summary>
public void ToggleSquelch()
{
if (m_VivoxParticipant != null)
{
squelched.Value = !squelched.Value;
if (squelched.Value)
m_VivoxParticipant.MutePlayerLocally();
else
m_VivoxParticipant.UnmutePlayerLocally();
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c156e0102a8d0514b91acc63faadec62
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,121 @@
using System.Collections.Generic;
using Unity.Netcode;
using UnityEngine;
using XRMultiplayer;
/// <summary>
/// Networked Projectile Launcher.
/// </summary>
public class NetworkProjectileLauncher : NetworkBehaviour
{
[SerializeField]
[Tooltip("The point that the project is created")]
Transform m_StartPoint = null;
// [SerializeField]
// [Tooltip("The projectile that's created")]
// GameObject m_ProjectilePrefab = null;
[SerializeField]
[Tooltip("The speed at which the projectile is launched")]
float m_LaunchSpeed = 1000f;
[SerializeField]
[Tooltip("The speed at which the projectile is launched")]
int m_MaxProjectilesAllowed = 15;
readonly List<Projectile> m_ProjectileQueue = new();
[Header("Audio")]
[SerializeField] AudioSource m_AudioSource;
[SerializeField] AudioClip m_AudioClip;
/// <summary>
/// Networked Color. This value gets set when ownership is gained.
/// </summary>
readonly NetworkVariable<Color> m_ProjectileColor = new(default, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner);
/// <summary>
/// Backup color to use for the local player if ownership has not been established when firing the launcher.
/// </summary>
/// <remarks>
/// This will only be used if the player picks up the launcher and fires immediately.
/// This Color is not synchronized over the network and will result in inconsistency between players when used.
/// </remarks>
Color m_BackupColor;
PoolerProjectiles m_ProjectilePooler;
void Awake()
{
m_ProjectilePooler = FindFirstObjectByType<PoolerProjectiles>();
}
/// <inheritdoc/>
public override void OnNetworkSpawn()
{
base.OnNetworkSpawn();
if (IsOwner)
{
m_ProjectileColor.Value = XRINetworkGameManager.LocalPlayerColor.Value;
}
}
/// <summary>
/// Synchronize the firing of the projectile.
/// </summary>
/// <param name="activate"></param>
public void FireLauncher(bool activate)
{
if (activate)
{
Color fireColor = m_BackupColor;
if (m_ProjectileColor.Value != default)
{
fireColor = m_ProjectileColor.Value;
}
GameObject newObject = m_ProjectilePooler.GetItem();
if (!newObject.TryGetComponent(out Projectile projectile))
{
Utils.Log("Projectile component not found on projectile object.", 1);
return;
}
projectile.transform.SetPositionAndRotation(m_StartPoint.position, m_StartPoint.rotation);
projectile.Setup(IsOwner, fireColor, OnProjectileDestroy);
m_AudioSource.PlayOneShot(m_AudioClip);
if (newObject.TryGetComponent(out Rigidbody rigidBody))
{
rigidBody.isKinematic = true;
rigidBody.isKinematic = false;
Vector3 force = m_StartPoint.forward * m_LaunchSpeed;
rigidBody.AddForce(force);
}
m_ProjectileQueue.Add(projectile);
if (m_ProjectileQueue.Count > m_MaxProjectilesAllowed)
{
m_ProjectileQueue[0].ResetProjectile();
}
}
}
void OnProjectileDestroy(Projectile projectile)
{
if (m_ProjectileQueue.Contains(projectile))
{
m_ProjectileQueue.Remove(projectile);
}
m_ProjectilePooler.ReturnItem(projectile.gameObject);
}
/// <inheritdoc/>
public override void OnGainedOwnership()
{
base.OnGainedOwnership();
if (IsOwner)
{
m_ProjectileColor.Value = XRINetworkGameManager.LocalPlayerColor.Value;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 398fc2e45b93f4b479a8520759861ac7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 983c4ee7d4d229f43bdd1744cf4e8b67
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,24 @@
using UnityEngine;
using Unity.Netcode.Components;
namespace XRMultiplayer
{
/// <summary>
/// ClientNetworkTransform class is responsible for updating the
/// <see cref="NetworkTransform"/> from the local owner perspective.
/// </summary>
[DisallowMultipleComponent]
public class ClientNetworkTransform : NetworkTransform
{
/// <summary>
/// If true, only the Server can update the transform of the object.
/// </summary>
[SerializeField, Tooltip("Determines Local or Server transform updating.")] bool isServerAuthoritative = false;
///<inheritdoc/>
protected override bool OnIsServerAuthoritative()
{
return isServerAuthoritative;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 25f7d6f3d19b0524d9be4bc50efbc19a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,26 @@
using UnityEngine;
using Unity.Netcode;
using Unity.XR.CoreUtils;
using UnityEngine.Events;
/// <summary>
/// Simple Network Trigger syncing the events over the network when the local player enters the trigger collider.
/// </summary>
public class NetworkTrigger : NetworkBehaviour
{
[SerializeField, Tooltip("This event is triggered when the Local Player walks into this.")] protected UnityEvent<ulong> m_NetworkedTriggerUnityEvent;
void OnTriggerEnter(Collider other)
{
// Local Player Triggered
if (other.TryGetComponent(out XROrigin origin))
{
TriggerRpc(NetworkManager.Singleton.LocalClientId);
}
}
[Rpc(SendTo.Everyone)]
void TriggerRpc(ulong clientId)
{
m_NetworkedTriggerUnityEvent?.Invoke(clientId);
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 70a0910909cd7314eb658649e9e0375d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: db5f8a51ece76e84492ef6b20cddc4b3
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,60 @@
using UnityEngine;
using Unity.Netcode;
using UnityEngine.UI;
namespace XRMultiplayer
{
/// <summary>
/// Simple implementation of a Networked button.
/// </summary>
[RequireComponent(typeof(Button))]
public class NetworkedButton : NetworkBehaviour
{
/// <summary>
/// Button associated with this component.
/// </summary>
Button m_Button;
///<inheritdoc/>
private void Awake()
{
m_Button = GetComponent<Button>();
m_Button.onClick.AddListener(ButtonClicked);
}
/// <summary>
/// Called when the button is clicked by the Local user.
/// </summary>
void ButtonClicked()
{
ClickButtonServerRpc(NetworkManager.Singleton.LocalClientId);
}
/// <summary>
/// Called from the local user to the Server when the local user has clicked the button.
/// </summary>
/// <param name="clientId">Local user Id.</param>
[ServerRpc(RequireOwnership = false)]
void ClickButtonServerRpc(ulong clientId)
{
ClickButtonClientRpc(clientId);
}
/// <summary>
/// Called from the Server on all clients after a local user has clicked the button.
/// </summary>
/// <param name="clientId">Local user Id.</param>
[ClientRpc]
void ClickButtonClientRpc(ulong clientId)
{
// Don't update on the local client if they sent the call.
if (NetworkManager.Singleton.LocalClientId != clientId)
{
//Remove listener here before Invoking to prevent continuous looping
m_Button.onClick.RemoveListener(ButtonClicked);
m_Button.onClick.Invoke();
m_Button.onClick.AddListener(ButtonClicked);
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 01970be37ab42be46b7bd0691f6f970d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,88 @@
using UnityEngine;
using Unity.Netcode;
using TMPro;
namespace XRMultiplayer
{
/// <summary>
/// Simple implementation of a Networked Dropdown.
/// </summary>
[RequireComponent(typeof(TMP_Dropdown))]
public class NetworkedDropdown : NetworkBehaviour
{
[SerializeField, Tooltip("Broadcast the value of the dropdown to all clients when a new client joins.")]
bool m_BroadcastValueOnJoin = false;
/// <summary>
/// Networked Variable to sync the state of the dropdown on new clients joining.
/// </summary>
NetworkVariable<int> m_CurrentDropdownNetworkValue;
/// <summary>
/// Dropdown associated with this component.
/// </summary>
TMP_Dropdown m_Dropdown;
///<inheritdoc/>
private void Awake()
{
m_Dropdown = GetComponent<TMP_Dropdown>();
m_CurrentDropdownNetworkValue = new NetworkVariable<int>(m_Dropdown.value, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server);
m_Dropdown.onValueChanged.AddListener(UpdateDropdown);
}
///<inheritdoc/>
public override void OnNetworkSpawn()
{
base.OnNetworkSpawn();
if (m_BroadcastValueOnJoin)
{
// Sync the value of the dropdown.
m_Dropdown.value = m_CurrentDropdownNetworkValue.Value;
}
else
{
m_Dropdown.SetValueWithoutNotify(m_CurrentDropdownNetworkValue.Value);
}
}
/// <summary>
/// Called when the Dropdown is updated by the local user.
/// </summary>
/// <param name="dropdownValue">Value of the dropdown.</param>
void UpdateDropdown(int dropdownValue)
{
UpdateDropdownServerRpc(dropdownValue, NetworkManager.Singleton.LocalClientId);
}
/// <summary>
/// Called from the local user to the Server whe the local user has updated the slider.
/// </summary>
/// <param name="dropdownValue">Value of the dropdown.</param>
/// <param name="clientId">Local user Id.</param>
[ServerRpc(RequireOwnership = false)]
void UpdateDropdownServerRpc(int dropdownValue, ulong clientId)
{
UpdateDropdownClientRpc(dropdownValue, clientId);
}
/// <summary>
/// Called from the Server on all clients when a local user has updated the dropdown.
/// </summary>
/// <param name="dropdownValue">Value of the dropdown.</param>
/// <param name="clientId">Local user Id.</param>
[ClientRpc]
void UpdateDropdownClientRpc(int dropdownValue, ulong clientId)
{
// Don't update on the local client if they sent the call.
if (NetworkManager.Singleton.LocalClientId != clientId)
{
//Remove listener here before updating value to prevent continuous looping
m_Dropdown.onValueChanged.RemoveListener(UpdateDropdown);
m_Dropdown.value = dropdownValue;
m_Dropdown.onValueChanged.AddListener(UpdateDropdown);
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 65eb984d014f1934da0f38f32923b9e8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,105 @@
using UnityEngine;
using Unity.Netcode;
using UnityEngine.UI;
namespace XRMultiplayer
{
/// <summary>
/// Simple implmentation of a Networked Slider.
/// </summary>
[RequireComponent(typeof(Slider))]
public class NetworkedSlider : NetworkBehaviour
{
[SerializeField, Tooltip("Broadcast the value of the dropdown to all clients when a new client joins.")]
bool m_BroadcastValueOnJoin = false;
[SerializeField, Tooltip("Reset current value when despawning.")]
bool m_ResetValueOnDespawn = true;
/// <summary>
/// Networked Variable to sync the state of the Slider on new clients joining.
/// </summary>
NetworkVariable<float> m_NetworkSliderValue;
/// <summary>
/// Slider associated with this component.
/// </summary>
Slider m_Slider;
float m_StartValue;
///<inheritdoc/>
private void Awake()
{
m_Slider = GetComponent<Slider>();
m_Slider.onValueChanged.AddListener(SliderChanged);
m_StartValue = m_Slider.value;
m_NetworkSliderValue = new NetworkVariable<float>(m_StartValue, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server);
}
///<inheritdoc/>
public override void OnNetworkSpawn()
{
base.OnNetworkSpawn();
if (m_BroadcastValueOnJoin)
{
// Sync the value of the dropdown.
m_Slider.value = m_NetworkSliderValue.Value;
}
else
{
m_Slider.SetValueWithoutNotify(m_NetworkSliderValue.Value);
}
}
public override void OnNetworkDespawn()
{
base.OnNetworkDespawn();
if (IsServer && m_ResetValueOnDespawn)
{
if (m_NetworkSliderValue != null)
m_NetworkSliderValue.Value = m_StartValue;
}
}
/// <summary>
/// Called when the Slider is updated by the local user.
/// </summary>
/// <param name="newValue">Value of the slider.</param>
void SliderChanged(float newValue)
{
SliderChangedServerRpc(newValue, NetworkManager.Singleton.LocalClientId);
}
/// <summary>
/// Called from the local user to the Server when the local user has updated the slider.
/// </summary>
/// <param name="newValue">Value of the slider.</param>
/// <param name="clientId">Local user Id.</param>
[ServerRpc(RequireOwnership = false)]
void SliderChangedServerRpc(float newValue, ulong clientId)
{
m_NetworkSliderValue.Value = newValue;
SliderChangedClientRpc(newValue, clientId);
}
/// <summary>
/// Called from the Server on all clients after a local user has updated the slider.
/// </summary>
/// <param name="newValue">Value of the slider.</param>
/// <param name="clientId">Local user Id.</param>
[ClientRpc]
void SliderChangedClientRpc(float newValue, ulong clientId)
{
// Don't update on the local client if they sent the call.
if (NetworkManager.Singleton.LocalClientId != clientId)
{
//Remove listener here before updating value to prevent continuous looping
m_Slider.onValueChanged.RemoveListener(SliderChanged);
m_Slider.value = newValue;
m_Slider.onValueChanged.AddListener(SliderChanged);
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5a5524c997ae7af4780b0aaa31b8ccc7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,89 @@
using UnityEngine;
using Unity.Netcode;
using UnityEngine.UI;
namespace XRMultiplayer
{
/// <summary>
/// Simple implementation of a Networked Toggle.
/// </summary>
[RequireComponent(typeof(Toggle))]
public class NetworkedToggle : NetworkBehaviour
{
[SerializeField, Tooltip("Broadcast the value of the dropdown to all clients when a new client joins.")]
bool m_BroadcastValueOnJoin = false;
/// <summary>
/// Networked variable to sync the state of the toggle on new clients joining.
/// </summary>
NetworkVariable<bool> m_NetworkToggleValue;
/// <summary>
/// Toggle associated with this component.
/// </summary>
Toggle m_Toggle;
///<inheritdoc/>
private void Awake()
{
m_Toggle = GetComponent<Toggle>();
m_NetworkToggleValue = new NetworkVariable<bool>(m_Toggle.isOn, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server);
m_Toggle.onValueChanged.AddListener(UpdateToggleValue);
}
///<inheritdoc/>
public override void OnNetworkSpawn()
{
base.OnNetworkSpawn();
if (m_BroadcastValueOnJoin)
{
// Sync the value of the dropdown.
m_Toggle.isOn = m_NetworkToggleValue.Value;
}
else
{
m_Toggle.SetIsOnWithoutNotify(m_NetworkToggleValue.Value);
}
}
/// <summary>
/// Called when the Toggle is updated by the local user.
/// </summary>
/// <param name="value">Value of the toggle.</param>
void UpdateToggleValue(bool value)
{
UpdateToggleServerRpc(value, NetworkManager.Singleton.LocalClientId);
}
/// <summary>
/// Called from the local user to the Server when the local user has updated the toggle.
/// </summary>
/// <param name="value">Value of the toggle.</param>
/// <param name="clientId">Local user Id.</param>
[ServerRpc(RequireOwnership = false)]
void UpdateToggleServerRpc(bool value, ulong clientId)
{
UpdateToggleClientRpc(value, clientId);
}
/// <summary>
/// Called from the Server on all clients when a local user has updated the toggle.
/// </summary>
/// <param name="value">Value of the toggle.</param>
/// <param name="clientId">Local user Id.</param>
[ClientRpc]
void UpdateToggleClientRpc(bool value, ulong clientId)
{
// Don't update on the local client if they sent the call.
if (NetworkManager.Singleton.LocalClientId != clientId)
{
m_Toggle.onValueChanged.RemoveListener(UpdateToggleValue);
m_Toggle.isOn = value;
m_Toggle.onValueChanged.AddListener(UpdateToggleValue);
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3673f01027db6384abcf3934934e5a4a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,94 @@
using Unity.Netcode;
using UnityEngine;
using UnityEngine.XR.Content.Interaction;
namespace XRMultiplayer
{
/// <summary>
/// Represents a networked XR knob interactable.
/// </summary>
[RequireComponent(typeof(XRKnob))]
public class NetworkXRKnob : NetworkBehaviour
{
/// <summary>
/// The networked knob value.
/// </summary>
NetworkVariable<float> m_NetworkedKnobValue = new NetworkVariable<float>(0f, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server);
/// <summary>
/// The XR knob component.
/// </summary>
XRKnob m_XRKnob;
/// <inheritdoc/>
public void Awake()
{
// Get associated components
if (!TryGetComponent(out m_XRKnob))
{
Utils.Log("Missing Components! Disabling Now.", 2);
enabled = false;
return;
}
}
/// <inheritdoc/>
public override void OnNetworkSpawn()
{
base.OnNetworkSpawn();
m_XRKnob.onValueChange.AddListener(KnobChanged);
if (IsServer)
{
m_NetworkedKnobValue.Value = m_XRKnob.value;
}
else
{
m_XRKnob.value = m_NetworkedKnobValue.Value;
}
}
public override void OnNetworkDespawn()
{
base.OnNetworkDespawn();
m_XRKnob.onValueChange.RemoveListener(KnobChanged);
}
/// <summary>
/// Called when the knob value is changed.
/// </summary>
/// <param name="newValue">The new value of the knob.</param>
private void KnobChanged(float newValue)
{
KnobChangedServerRpc(newValue, NetworkManager.Singleton.LocalClientId);
}
/// <summary>
/// Server RPC called when the knob value is changed.
/// </summary>
/// <param name="newValue">The new value of the knob.</param>
/// <param name="clientId">The client ID of the player who changed the knob value.</param>
[ServerRpc(RequireOwnership = false)]
void KnobChangedServerRpc(float newValue, ulong clientId)
{
m_NetworkedKnobValue.Value = newValue;
KnobChangedClientRpc(newValue, clientId);
}
/// <summary>
/// Client RPC called when the knob value is changed.
/// </summary>
/// <param name="newValue">The new value of the knob.</param>
/// <param name="clientId">The client ID of the player who changed the knob value.</param>
[ClientRpc]
void KnobChangedClientRpc(float newValue, ulong clientId)
{
if (clientId != NetworkManager.Singleton.LocalClientId)
{
m_XRKnob.onValueChange.RemoveListener(KnobChanged);
m_XRKnob.value = newValue;
m_XRKnob.onValueChange.AddListener(KnobChanged);
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c470054f5fb2da149a35fe23c48a3055
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,77 @@
using Unity.Netcode;
using UnityEngine;
using UnityEngine.XR.Content.Interaction;
namespace XRMultiplayer
{
[RequireComponent(typeof(XRLever))]
public class NetworkXRLever : NetworkBehaviour
{
/// <summary>
/// The networked knob value.
/// </summary>
NetworkVariable<bool> m_NetworkedLeverValue = new NetworkVariable<bool>(false, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server);
XRLever m_XRLever;
void Awake()
{
// Get associated components
if (!TryGetComponent(out m_XRLever))
{
Utils.Log("Missing Components! Disabling Now.", 2);
enabled = false;
return;
}
}
/// <inheritdoc/>
public override void OnNetworkSpawn()
{
base.OnNetworkSpawn();
m_XRLever.onLeverActivate.AddListener(LeverChanged);
m_XRLever.onLeverDeactivate.AddListener(LeverChanged);
if (IsServer)
{
m_NetworkedLeverValue.Value = m_XRLever.value;
}
else
{
m_XRLever.value = m_NetworkedLeverValue.Value;
}
}
public override void OnNetworkDespawn()
{
base.OnNetworkDespawn();
m_XRLever.onLeverActivate.RemoveListener(LeverChanged);
m_XRLever.onLeverDeactivate.RemoveListener(LeverChanged);
}
void LeverChanged()
{
LeverChangedServerRpc(m_XRLever.value, NetworkManager.Singleton.LocalClientId);
}
[ServerRpc(RequireOwnership = false)]
void LeverChangedServerRpc(bool newValue, ulong clientId)
{
m_NetworkedLeverValue.Value = newValue;
LeverChangedClientRpc(newValue, clientId);
}
[ClientRpc]
void LeverChangedClientRpc(bool newValue, ulong clientId)
{
if (clientId != NetworkManager.Singleton.LocalClientId)
{
m_XRLever.onLeverActivate.RemoveListener(LeverChanged);
m_XRLever.onLeverDeactivate.RemoveListener(LeverChanged);
m_XRLever.value = newValue;
m_XRLever.onLeverActivate.AddListener(LeverChanged);
m_XRLever.onLeverDeactivate.AddListener(LeverChanged);
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1af5a1a5d9d769f4cb8c5209829fe788
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: