450 lines
16 KiB
C#
450 lines
16 KiB
C#
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();
|
|
}
|
|
}
|
|
}
|
|
}
|