Initial commit
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -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 speaker’s 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 speaker’s 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -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:
|
||||
+60
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 01970be37ab42be46b7bd0691f6f970d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+88
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 65eb984d014f1934da0f38f32923b9e8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+105
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5a5524c997ae7af4780b0aaa31b8ccc7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+89
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -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:
|
||||
Reference in New Issue
Block a user