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 { /// /// XRINetworkPlayer class used for simple interactions. /// public class XRINetworkPlayer : NetworkBehaviour { /// /// Speed at which voice amplitude changes. /// const float k_VoiceAmplitudeSpeed = 15.0f; /// /// Singleton Reference for the Local Player. /// public static XRINetworkPlayer LocalPlayer; [Header("Avatar Transform References"), Tooltip("Assign to local avatar transform.")] /// /// Non-Local player transforms. /// public Transform head; /// /// Non-Local player transforms. /// public Transform leftHand; /// /// Non-Local player transforms. /// public Transform rightHand; /// /// Action called when the player name is updated. /// public Action onNameUpdated; /// /// Action called when the player color is updated. /// public Action onColorUpdated; /// /// Action called when the Local Player is finished spawning in. /// public Action onSpawnedLocal; /// /// Action called when the Local Player is finished spawning in. /// public Action onSpawnedAll; /// /// Action called when the player color is updated. /// public Action onDisconnected; /// /// Bindable Variable used for other clients to mute this user locally. /// public BindableVariable squelched = new BindableVariable(false); /// /// Current Voice Amplitude driven from Vivox. /// public float playerVoiceAmp { get => m_VoiceAmplitudeCurrent; } float m_VoiceAmplitudeCurrent; /// /// Player Voice Id string that reads from the internal NetworkVariable for the Player Voice Id. /// public string playerVoiceId { get => m_PlayerVoiceId.Value.ToString(); } readonly NetworkVariable m_PlayerVoiceId = new("", NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner); /// /// Player Name string that reads from the internal NetworkVariable for the Player Name. /// public string playerName { get => m_PlayerName.Value.ToString(); } readonly NetworkVariable m_PlayerName = new("", NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner); /// /// Player Color that reads from the internal NetworkVariable for the Player Color. /// public Color playerColor { get => m_PlayerColor.Value; } readonly NetworkVariable m_PlayerColor = new(Color.white, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner); [HideInInspector] public readonly NetworkVariable selfMuted = new(false, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner); /// /// Player Name Tag. /// [Header("Player Name Tag"), SerializeField, Tooltip("Player Name Tag.")] protected bool m_UpdateObjectName = true; // /// // /// Head Renderers to change rendering mode for local players. // /// // [SerializeField, Tooltip("Head Renderers to change rendering mode for local players.")] protected Renderer[] m_HeadRends; /// /// Hand Objects to be disabled for the local player. /// [Header("Networked Hands"), SerializeField, Tooltip("Hand Objects to be disabled for the local player.")] protected GameObject[] m_handsObjects; /// /// Player Name Tag. /// [Header("Player Name Tag"), SerializeField, Tooltip("Player Name Tag.")] protected PlayerNameTag m_PlayerNameTag; /// /// Internal references to the Local Player Transforms. /// protected Transform m_LeftHandOrigin, m_RightHandOrigin, m_HeadOrigin; /// /// Reference to the local player XR Origin /// protected XROrigin m_XROrigin; /// /// If the player has been connected to the the game. /// protected bool m_InitialConnected = false; /// /// Reference to the VoiceChatManager. /// protected VoiceChatManager m_VoiceChat; /// /// Reference to the VivoxParticipant. /// protected VivoxParticipant m_VivoxParticipant; /// /// Time to update the voice position. /// protected float m_VoicePositionUpdateTime = .1f, m_VoiceUpdatePosotionDelta = .05f; /// /// Destination for the voice amplitude. /// protected float m_VoiceAmplitudeDestination; /// /// Timer to check the voice position. /// protected float m_VoicePositionCheckTimer; /// /// Previous position of the head. /// protected Vector3 m_PrevHeadPos; protected void Awake() { m_VoiceChat = FindFirstObjectByType(); m_VoicePositionCheckTimer = m_VoicePositionUpdateTime; } /// protected virtual void OnEnable() { m_PlayerName.OnValueChanged += UpdatePlayerName; m_PlayerColor.OnValueChanged += UpdatePlayerColor; } /// protected virtual void OnDisable() { m_PlayerName.OnValueChanged -= UpdatePlayerName; m_PlayerColor.OnValueChanged -= UpdatePlayerColor; } /// 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); } /// 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); } /// 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; } /// 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(); 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($"{m_PlayerName.Value} left"); onDisconnected?.Invoke(this); } /// /// Called from when swapping between hand tracking and controllers. /// /// Transform for Left Hand. /// Transform for Right Hand. public void SetHandOrigins(Transform left, Transform right) { m_LeftHandOrigin = left; m_RightHandOrigin = right; } /// /// Hides and disables Renderers and GameObjects on the Local Player. /// Also sets the initial values for and . /// Finally we subscribe to any updates for Color and Name. /// /// Only called on the Local Player. 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(); } /// /// Called from the local player only /// /// void SelfMutedChanged(bool muted) { selfMuted.Value = muted; } /// /// Callback for the bindable variable . /// /// New Color for player. /// Only called on Local Player. protected virtual void UpdateLocalPlayerColor(Color color) { m_PlayerColor.Value = XRINetworkGameManager.LocalPlayerColor.Value; } /// /// Callback for the bindable variable . /// /// New Name for player. /// Only called on Local Player. protected virtual void UpdateLocalPlayerName(string name) { m_PlayerName.Value = new FixedString128Bytes(XRINetworkGameManager.LocalPlayerName.Value); } /// /// Called when the player object is finished being setup. /// 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(); if (worldCanvas != null) { // If we are using a World Canvas, reparent name tag and destroy local canvas. Canvas localCanvas = m_PlayerNameTag.GetComponentInParent(); 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(); } /// /// Callback anytime the local player sets . /// Invokes the callback . 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($"{playerName} joined"); } if (m_UpdateObjectName) gameObject.name = currentName.ToString(); } /// /// Callback when the local player sets . /// Invokes the callback . void UpdatePlayerColor(Color oldColor, Color newColor) { onColorUpdated?.Invoke(newColor); } void UpdatePlayerVoiceEnergy(float current) { m_VoiceAmplitudeDestination = Mathf.Clamp01(current); } /// /// Called when new players connect to the game and set their initial /// and when is called for existing players. /// 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); } } /// /// Called from clients to mute this player locally for that client. /// public void ToggleSquelch() { if (m_VivoxParticipant != null) { squelched.Value = !squelched.Value; if (squelched.Value) m_VivoxParticipant.MutePlayerLocally(); else m_VivoxParticipant.UnmutePlayerLocally(); } } } }