Initial commit

This commit is contained in:
Thorbjoern
2025-05-26 00:46:28 +02:00
commit e5bca03433
3896 changed files with 1434297 additions and 0 deletions
@@ -0,0 +1,117 @@
using System;
using UnityEngine;
using UnityEngine.Events;
namespace XRMultiplayer
{
/// <summary>
/// Fires events when this object is is within the field of view of the gaze transform. This is currently used to
/// hide and show tooltip callouts on the controllers when the controllers are within the field of view.
/// </summary>
public class CalloutGazeController : MonoBehaviour
{
[SerializeField, Tooltip("The transform which the forward direction will be used to evaluate as the gaze direction.")]
protected Transform m_GazeTransform;
[SerializeField, Tooltip("Threshold for the dot product when determining if the Gaze Transform is facing this object. The lower the threshold, the wider the field of view."), Range(0.0f, 1.0f)]
protected float m_FacingThreshold = 0.85f;
[SerializeField, Tooltip("Events fired when the Gaze Transform begins facing this game object")]
protected UnityEvent m_FacingEntered;
[SerializeField, Tooltip("Events fired when the Gaze Transform stops facing this game object")]
protected UnityEvent m_FacingExited;
[SerializeField, Tooltip("Distance threshold for movement in a single frame that determines a large movement that will trigger Facing Exited events.")]
float m_LargeMovementDistanceThreshold = 0.05f;
[SerializeField, Tooltip("Cool down time after a large movement for Facing Entered events to fire again.")]
float m_LargeMovementCoolDownTime = 0.25f;
[SerializeField, Tooltip("Use Distance Threshold")]
bool m_UseDistanceThreshold = false;
[SerializeField, Tooltip("Distance threshold to stop applying Facing Events")]
float m_MaxDistanceThreshold = 10.0f;
bool m_IsFacing;
float m_LargeMovementCoolDown;
Vector3 m_LastPosition;
void Start()
{
if (!m_GazeTransform)
m_GazeTransform = Camera.main.transform;
}
protected virtual void Update()
{
CheckLargeMovement();
if (m_LargeMovementCoolDown < m_LargeMovementCoolDownTime)
return;
CheckFacing();
}
void CheckFacing()
{
if (!m_GazeTransform)
return;
if (m_UseDistanceThreshold)
{
float currentDistance = Vector3.Distance(m_GazeTransform.position, transform.position);
if (currentDistance > m_MaxDistanceThreshold)
{
if (m_IsFacing)
{
FacingExited();
}
return;
}
}
var dotProduct = Vector3.Dot(m_GazeTransform.forward, (transform.position - m_GazeTransform.position).normalized);
if (dotProduct > m_FacingThreshold && !m_IsFacing)
FacingEntered();
else if (dotProduct < m_FacingThreshold && m_IsFacing)
FacingExited();
}
void CheckLargeMovement()
{
// Check if there is large movement
var currentPosition = transform.position;
var positionDelta = Mathf.Abs(Vector3.Distance(m_LastPosition, currentPosition));
if (positionDelta > m_LargeMovementDistanceThreshold)
{
m_LargeMovementCoolDown = 0.0f;
FacingExited();
}
m_LargeMovementCoolDown += Time.deltaTime;
m_LastPosition = currentPosition;
}
void FacingEntered()
{
m_IsFacing = true;
m_FacingEntered.Invoke();
}
void FacingExited()
{
m_IsFacing = false;
m_FacingExited.Invoke();
}
public void CheckPointerExit()
{
var dotProduct = Vector3.Dot(m_GazeTransform.forward, (transform.position - m_GazeTransform.position).normalized);
float currentDistance = Vector3.Distance(m_GazeTransform.position, transform.position);
if (dotProduct < m_FacingThreshold || (m_UseDistanceThreshold && currentDistance > m_MaxDistanceThreshold))
{
FacingExited();
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 84f6509f3c7fa7b4899c9c767f49e622
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,73 @@
using UnityEngine;
namespace XRMultiplayer
{
/// <summary>
/// A very simple script that will enable or disable objects based on the Network Connection State.
/// </summary>
public class ConnectionToggler : MonoBehaviour
{
/// <summary>
/// Enables all objects on connect.
/// Disables all objects on disconnect.
/// </summary>
[SerializeField] GameObject[] objectsToEnableOnline;
/// <summary>
/// Enables all objects on disconnect.
/// Disables all objects on connect.
/// </summary>
[SerializeField] GameObject[] objectsToEnableOffline;
/// <inheritdoc/>
void OnEnable()
{
XRINetworkGameManager.Connected.Subscribe(ToggleNetworkObjects);
ToggleNetworkObjects(XRINetworkGameManager.Connected.Value);
}
void Start()
{
XRINetworkGameManager.Instance.connectionFailedAction += (reason) =>
{
ToggleNetworkObjects(false);
};
}
void OnDestroy()
{
XRINetworkGameManager.Instance.connectionFailedAction -= (reason) =>
{
ToggleNetworkObjects(false);
};
}
/// <inheritdoc/>
void OnDisable()
{
XRINetworkGameManager.Connected.Unsubscribe(ToggleNetworkObjects);
}
/// <summary>
/// Toggles objects on or off based on whether or not connected.
/// <see cref="m_Connected"/>
/// </summary>
/// <param name="online">
/// Whether or not players are connected to a networked game.
/// </param>
protected virtual void ToggleNetworkObjects(bool online)
{
foreach (GameObject g in objectsToEnableOnline)
{
if (g == null) continue;
g.SetActive(online);
}
foreach (GameObject g in objectsToEnableOffline)
{
if (g == null) continue;
g.SetActive(!online);
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 18c4930fb76401f4980a7b6da9f2aecd
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,246 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.XR.Hands;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace XRMultiplayer
{
/// <summary>
/// This class controls the curl of the fingers based on hand tracking or controller tracking.
/// </summary>
public class JointBasedHand : MonoBehaviour
{
/// <summary>
/// Controls how the fingers are updated.
/// Curl allows for a much more lightweight approximation of finger movements.
/// </summary>
public bool useCurl
{
get => m_UseCurl;
set => m_UseCurl = value;
}
bool m_UseCurl;
/// <summary>
/// Controls the level of fidelity for fingers.
/// </summary>
/// <remarks>Set from <see cref="XRHandPoseReplicator.SetFidelity(int)"/>.</remarks>
public int fidelityLevel
{
get => m_FidelityLevel;
set => m_FidelityLevel = value;
}
int m_FidelityLevel;
[Header("Setup Settings")]
/// <summary>
/// Specify where the root of the hand is.
/// </summary>
[SerializeField, Tooltip("Specify where the root of the hand is.")]
protected Transform m_HandRoot;
/// <summary>
/// Specify the names of the fingers.
/// </summary>
[SerializeField, Tooltip("Specify the names of the fingers.")]
protected string[] m_FingerNames = { "Thumb", "Index", "Middle", "Ring", "Little" };
/// <summary>
/// Specify the start index of the finger joints.
/// </summary>
[SerializeField, Tooltip("Specify the start index of the finger joints.")]
protected XRHandJointID[] m_FingerStartJointIds = { XRHandJointID.ThumbMetacarpal, XRHandJointID.IndexMetacarpal, XRHandJointID.MiddleMetacarpal, XRHandJointID.RingMetacarpal, XRHandJointID.LittleMetacarpal };
[Header("Hand Fidelity Options")]
/// <summary>
/// Groups of <see cref="JointToTransformReference"/>.
/// </summary>
[Tooltip("Groups of Joint To Transform References. Use Context Menu for auto generation.")]
public HandFidelityOption[] handFidelityOptions;
/// <summary>
/// Sets the Min/Max euler rotation of the fingers.
/// </summary>
[SerializeField, Tooltip("Sets the Min/Max euler rotation of the fingers.")]
protected Vector2 m_MinMaxEulerX = new Vector2(0, 100);
//3, 4, 5 -- Thumb
//7, 8, 9, 10 -- Index
//12, 13, 14, 15 -- Middle
//17, 18, 19, 20 -- Ring
//22, 23, 24, 25 -- Little
/// <inheritdoc/>
private void Update()
{
if (!m_UseCurl) return;
m_FidelityLevel = Mathf.Clamp(m_FidelityLevel, 0, handFidelityOptions.Length);
foreach (var joint in handFidelityOptions[m_FidelityLevel].fingerJoints)
{
foreach (var finger in joint.jointTransformReferences)
{
Vector3 rot = Vector3.zero;
rot.x = Mathf.Lerp(m_MinMaxEulerX.x, m_MinMaxEulerX.y, joint.curlAmount);
finger.jointTransform.localRotation = Quaternion.Euler(rot);
}
}
}
/// <summary>
/// Controls the Curl level of fingers.
/// </summary>
/// <remarks>Called from <see cref="XRHandPoseReplicator.GetNetworkCurl()"/>.</remarks>
/// <param name="fingerID">ID of the specific finger.</param>
/// <param name="curlAmount">Amount to curl the finger.</param>
public void SetCurl(int fingerID, float curlAmount)
{
handFidelityOptions[m_FidelityLevel].fingerJoints[fingerID].curlAmount = curlAmount;
}
#if UNITY_EDITOR
/// <summary>
/// Clears the hand references.
/// </summary>
public void ClearHandReferences()
{
handFidelityOptions = new HandFidelityOption[3];
}
/// <summary>
/// Attempts to find and automatically assign the hand references.
/// </summary>
[ContextMenu("Setup Hand References")]
public void SetupHandReferences()
{
try
{
handFidelityOptions = new HandFidelityOption[3];
for (int i = 0; i < handFidelityOptions.Length; i++)
{
handFidelityOptions[i].fingerJoints = new FingerJoints[i <= 1 ? 5 : 3];
for (int j = 0; j < handFidelityOptions[i].fingerJoints.Length; j++)
{
handFidelityOptions[i].fingerJoints[j].fingerName = m_FingerNames[j];
handFidelityOptions[i].fingerJoints[j].jointTransformReferences = new List<JointToTransformReference>();
int jointDepth = i == 0 ? 4 : 3;
if(j == 0) jointDepth -= 1; // Thumb has 1 less joint than the other fingers
int startDepth = i == 0 ? 0 : 1;
handFidelityOptions[i].fingerJoints[j].jointTransformReferences = GetFingerJoints(m_FingerNames[j], startDepth, jointDepth, m_FingerStartJointIds[j]);
}
//Get extra fingers as mittens
if(i == 2)
{
handFidelityOptions[i].fingerJoints[2].jointTransformReferences.AddRange(GetFingerJoints(m_FingerNames[3], 1, 3, m_FingerStartJointIds[3]));
handFidelityOptions[i].fingerJoints[2].jointTransformReferences.AddRange(GetFingerJoints(m_FingerNames[4], 1, 3, m_FingerStartJointIds[4]));
}
}
}
catch (Exception e)
{
Utils.LogError($"Error in FindNetworkHandReferences: {e}");
}
}
List<JointToTransformReference> GetFingerJoints(string fingerName, int startDepth, int jointDepth, XRHandJointID fingerStartJointId)
{
try
{
List<JointToTransformReference> fingerJoints = new();
JointToTransformReference currentJoint = new();
foreach (Transform child in m_HandRoot)
{
if (child.name.Contains(fingerName))
{
Transform currentChild = child;
//Navigate to the starting joint based on the startDepth
for(int i = 0; i < startDepth; i++)
{
currentChild = currentChild.GetChild(0);
}
// Get all joints in the finger and add them to the list based on the jointDepth
for (int i = 0; i < jointDepth; i++)
{
currentJoint.jointTransform = currentChild;
int currentHandJointId = (int)fingerStartJointId + i + startDepth;
currentJoint.xrHandJointID = (XRHandJointID)currentHandJointId;
fingerJoints.Add(currentJoint);
currentChild = currentChild.GetChild(0);
}
}
}
return fingerJoints;
}
catch (Exception e)
{
Utils.LogError($"Error in GetFingerJoints: {e}");
return null;
}
}
#endif
}
[Serializable]
public struct HandFidelityOption
{
public FingerJoints[] fingerJoints;
}
[Serializable]
public struct FingerJoints
{
/// <summary>
/// Finger Name.
/// </summary>
public string fingerName;
/// <summary>
/// The current curl amount of the finger.
/// </summary>
[Range(0.0f, 1.0f)]
public float curlAmount;
/// <summary>
/// <see cref="JointToTransformReference"/> List.
/// </summary>
public List<JointToTransformReference> jointTransformReferences;
}
#if UNITY_EDITOR
[CustomEditor(typeof(JointBasedHand))]
public class HandCurlerEditor : Editor
{
public override void OnInspectorGUI()
{
DrawDefaultInspector();
JointBasedHand myScript = (JointBasedHand)target;
if (GUILayout.Button("Setup References"))
{
myScript.SetupHandReferences();
}
if (GUILayout.Button("Clear Hand References"))
{
myScript.ClearHandReferences();
}
}
}
#endif
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 08e01ea3f1f3e964b9f0ccf246ec3155
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,202 @@
using Unity.XR.CoreUtils;
using Unity.XR.CoreUtils.Bindings.Variables;
using UnityEngine;
using UnityEngine.Android;
namespace XRMultiplayer
{
/// <summary>
/// Represents the offline player avatar.
/// </summary>
public class OfflinePlayerAvatar : MonoBehaviour
{
public static BindableVariable<float> voiceAmp = new BindableVariable<float>();
/// <summary>
/// Gets or sets a value indicating whether the player is muted.
/// </summary>
public static bool muted
{
get => s_Muted;
set
{
if (Permission.HasUserAuthorizedPermission(Permission.Microphone))
s_Muted = value;
}
}
/// <summary>
/// A value indicating whether the player is muted.
/// </summary>
static bool s_Muted;
/// <summary>
/// The head transform.
/// </summary>
[SerializeField] Transform m_HeadTransform;
/// <summary>
/// The head renderer.
/// </summary>
[SerializeField] SkinnedMeshRenderer m_HeadRend;
/// <summary>
/// The voice amplitude curve.
/// </summary>
[SerializeField] AnimationCurve m_VoiceCurve;
/// <summary>
/// The head origin.
/// </summary>
Transform m_HeadOrigin;
/// <summary>
/// The mouth blend smoothing.
/// </summary>
[SerializeField] float m_MouthBlendSmoothing = 5.0f;
/// <summary>
/// The microphone loudness.
/// </summary>
float m_MicLoudness;
/// <summary>
/// The microphone device name.
/// </summary>
string m_Device;
/// <summary>
/// The sample window.
/// </summary>
int m_SampleWindow = 128;
/// <summary>
/// The clip record.
/// </summary>
AudioClip m_ClipRecord;
/// <summary>
/// The voice destination volume.
/// </summary>
float m_VoiceDestinationVolume;
bool m_MicInitialized = false;
/// <inheritdoc/>
void Start()
{
XROrigin rig = FindFirstObjectByType<XROrigin>();
m_HeadOrigin = rig.Camera.transform;
}
void OnEnable()
{
XRINetworkGameManager.LocalPlayerColor.Subscribe(UpdatePlayerColor);
VoiceChatManager.s_HasMicrophonePermission.Subscribe(MicrophonePermissionGranted);
XRINetworkGameManager.Connected.Subscribe(connected =>
{
gameObject.SetActive(!connected);
});
}
void OnDisable()
{
XRINetworkGameManager.LocalPlayerColor.Unsubscribe(UpdatePlayerColor);
VoiceChatManager.s_HasMicrophonePermission.Subscribe(MicrophonePermissionGranted);
StopMicrophone();
XRINetworkGameManager.Connected.Unsubscribe(connected =>
{
gameObject.SetActive(!connected);
});
}
/// <inheritdoc/>
private void LateUpdate()
{
m_HeadTransform.SetPositionAndRotation(m_HeadOrigin.position, m_HeadOrigin.rotation);
}
/// <inheritdoc/>
void Update()
{
if (!s_Muted)
{
m_MicLoudness = LevelMax();
m_VoiceDestinationVolume = Mathf.Clamp01(Mathf.Lerp(m_VoiceDestinationVolume, m_MicLoudness, Time.deltaTime * m_MouthBlendSmoothing));
float appliedCurve = m_VoiceCurve.Evaluate(m_VoiceDestinationVolume);
voiceAmp.Value = appliedCurve;
m_HeadRend.SetBlendShapeWeight(0, 100 - appliedCurve * 100);
}
else
{
voiceAmp.Value = 0.0f;
}
}
void MicrophonePermissionGranted(bool granted)
{
if (granted)
{
InitMic();
}
}
void UpdatePlayerColor(Color color)
{
m_HeadRend.materials[2].color = color;
}
/// <summary>
/// Initializes the microphone, called from <see cref="VoiceChatManager.s_HasMicrophonePermission" callback/>.
/// </summary>
void InitMic()
{
m_MicInitialized = true;
m_Device ??= Microphone.devices[0];
m_ClipRecord = Microphone.Start(m_Device, true, 999, 44100);
}
/// <summary>
/// Stops the microphone.
/// </summary>
void StopMicrophone()
{
m_MicInitialized = false;
if (Permission.HasUserAuthorizedPermission(Permission.Microphone))
{
Microphone.End(m_Device);
}
else
{
s_Muted = true;
}
}
/// <summary>
/// Gets the maximum level of the microphone input.
/// </summary>
/// <returns>The maximum level of the microphone input.</returns>
float LevelMax()
{
if (!m_MicInitialized) return 0;
float levelMax = 0;
float[] waveData = new float[m_SampleWindow];
int micPosition = Microphone.GetPosition(null) - (m_SampleWindow + 1); // null means the first microphone
if (micPosition < 0) return 0;
m_ClipRecord.GetData(waveData, micPosition);
// Getting a peak on the last 128 samples
for (int i = 0; i < m_SampleWindow; i++)
{
float wavePeak = waveData[i] * waveData[i];
if (levelMax < wavePeak)
{
levelMax = wavePeak;
}
}
return levelMax;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6d8dc9872c05f1d47ae75941496f3d00
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,73 @@
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
namespace XRMultiplayer
{
/// <summary>
/// A simple example of how to setup a player appearance menu and utilize the bindable variables.
/// </summary>
public class PlayerAppearanceMenu : MonoBehaviour
{
[SerializeField] Color[] m_PlayerColors;
[SerializeField] TMP_InputField m_PlayerNameInputField;
[SerializeField] Image m_PlayerIconColor;
void Awake()
{
XRINetworkGameManager.LocalPlayerName.Subscribe(SetPlayerName);
XRINetworkGameManager.LocalPlayerColor.Subscribe(SetPlayerColor);
}
void Start()
{
SetPlayerColor(XRINetworkGameManager.LocalPlayerColor.Value);
SetPlayerName(XRINetworkGameManager.LocalPlayerName.Value);
}
void OnDestroy()
{
XRINetworkGameManager.LocalPlayerName.Unsubscribe(SetPlayerName);
XRINetworkGameManager.LocalPlayerColor.Unsubscribe(SetPlayerColor);
}
/// <summary>
/// Use this to set the player's name so it triggers the bindable variable
/// </summary>
/// <param name="text"></param>
public void SubmitNewPlayerName(string text)
{
XRINetworkGameManager.LocalPlayerName.Value = text;
}
/// <summary>
/// Use this to set the player's color so it triggers the bindable variable
/// </summary>
/// <param name="text"></param>
public void SetRandomColor()
{
List<Color> availableColors = new(m_PlayerColors);
if (availableColors.Remove(XRINetworkGameManager.LocalPlayerColor.Value))
{
XRINetworkGameManager.LocalPlayerColor.Value = availableColors[Random.Range(0, availableColors.Count)];
}
else
{
XRINetworkGameManager.LocalPlayerColor.Value = m_PlayerColors[Random.Range(0, m_PlayerColors.Length)];
}
}
void SetPlayerName(string newName)
{
m_PlayerNameInputField.text = newName;
}
void SetPlayerColor(Color color)
{
m_PlayerIconColor.color = color;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 82939a096682b114495548d75991172a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,164 @@
using System.Collections;
using TMPro;
using UnityEngine;
namespace XRMultiplayer
{
/// <summary>
/// This class controls the display of the Player HUD Notification aka the Toast.
/// </summary>
public class PlayerHudNotification : MonoBehaviour
{
/// <summary>
/// The singleton instance of this class.
/// </summary>
public static PlayerHudNotification Instance;
[Header("Display Options")]
[SerializeField] bool m_LockPitch = true;
[SerializeField] bool m_LockRoll = true;
/// <summary>
/// The speed at which the toast follows the camera.
/// </summary>
[SerializeField] float m_FollowSpeed = 5.0f;
/// <summary>
/// The amount of time to display the toast.
/// </summary>
[SerializeField] float m_DisplayTime = 3.0f;
/// <summary>
/// The speed at which the toast fades in and out.
/// </summary>
[SerializeField] float m_ShowHideSpeed = 5.0f;
[Header("Display References")]
/// <summary>
/// Text component to display the toast.
/// </summary>
[SerializeField] TMP_Text m_Text;
/// <summary>
/// The layout group transform that contains the toast.
/// </summary>
[SerializeField] Transform m_LayoutGroupTransform;
/// <summary>
/// The canvas group that contains the toast.
/// </summary>
[SerializeField] CanvasGroup m_CanvasGroup;
/// <summary>
/// The main camera.
/// </summary>
Camera m_Camera;
/// <summary>
/// The transform of this object.
/// </summary>
Transform m_Transform;
///<inheritdoc/>
private void Awake()
{
if (Instance != null)
{
Utils.Log("Instance is not null for PlayerHudNotification.", 2);
enabled = false;
return;
}
Instance = this;
}
/// <inheritdoc/>
private void Start()
{
m_Camera = Camera.main;
m_Transform = transform;
if (m_CanvasGroup == null)
m_CanvasGroup = GetComponentInChildren<CanvasGroup>();
m_CanvasGroup.alpha = 0.0f;
m_LayoutGroupTransform.gameObject.SetActive(false);
}
[ContextMenu("Show Text Test")]
void ShowTextTest()
{
ShowText("Test Text", m_DisplayTime);
}
/// <summary>
/// Shows the toast with the given text.
/// </summary>
public void ShowText(string textToShow, float displayTime = 3.0f)
{
m_DisplayTime = displayTime;
m_Text.text = textToShow;
m_LayoutGroupTransform.gameObject.SetActive(true);
StopAllCoroutines();
StartCoroutine(ShowRoutine());
}
/// <inheritdoc/>
private void LateUpdate()
{
m_Transform.position = m_Camera.transform.position;
Quaternion lookRot = Quaternion.LookRotation(m_Camera.transform.forward);
Vector3 offset = lookRot.eulerAngles;
if (m_LockPitch)
offset.x = 0;
if (m_LockRoll)
offset.z = 0;
lookRot = Quaternion.Euler(offset);
m_Transform.rotation = Quaternion.Slerp(m_Transform.rotation, lookRot, Time.deltaTime * m_FollowSpeed);
}
/// <summary>
/// Coroutine to show the toast.
/// </summary>
/// <returns></returns>
IEnumerator ShowRoutine()
{
while (m_CanvasGroup.alpha < 1.0f)
{
m_CanvasGroup.alpha += Time.deltaTime * m_ShowHideSpeed;
yield return null;
}
StartCoroutine(DisplayRoutine());
}
/// <summary>
/// Coroutine to display the toast.
/// </summary>
/// <returns></returns>
IEnumerator DisplayRoutine()
{
yield return new WaitForSeconds(m_DisplayTime);
StartCoroutine(HideTime());
}
/// <summary>
/// Coroutine to hide the toast.
/// </summary>
/// <returns></returns>
IEnumerator HideTime()
{
while (m_CanvasGroup.alpha > 0.0f)
{
m_CanvasGroup.alpha -= Time.deltaTime * m_ShowHideSpeed;
yield return null;
}
m_LayoutGroupTransform.gameObject.SetActive(false);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f76e89bf08e0ba8498751fc725c916de
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,217 @@
using TMPro;
using UnityEngine;
using UnityEngine.UI;
namespace XRMultiplayer
{
public class PlayerNameTag : MonoBehaviour
{
public ulong playerId { get => m_PlayerId; }
ulong m_PlayerId;
[SerializeField] bool m_WorldUp;
[SerializeField] TMP_Text m_NameTagText;
[SerializeField] TMP_Text m_InitialsText;
[SerializeField] Image m_ColoredImage;
[SerializeField] float m_NameTextScale = .25f;
[Header("Voice Chat")]
[SerializeField] Button m_MuteButton;
[SerializeField] Image m_VoiceChatFillImage;
[SerializeField] Image m_MicIcon;
[SerializeField] Image m_SquelchedIcon;
[SerializeField] Sprite m_MutedSprite;
[SerializeField] Sprite m_UnmutedSprite;
[SerializeField] ParticleSystem[] m_voiceParticles;
[Header("Name Tag LOD Settings")]
[SerializeField, Tooltip("If the avatar is further than this distance (in meters), the name tag details will be deactivated.")]
float m_MaxDistanceThreshold = 3f;
[SerializeField, Tooltip("If the avatar is closer than this distance (in meters), the entire name tag will be deactivated.")]
float m_MinDistanceThreshold = 1f;
[SerializeField, Tooltip("The GameObject that will be deactivated when the avatar is closer than the Min distance threshold.")]
GameObject m_GameObjectToHide;
[SerializeField, Tooltip("The GameObjects that will be deactivated when the avatar is beyond the Max distance threshold.")]
GameObject[] m_GameObjectDetailsToDeactivate;
XRINetworkPlayer m_Player;
protected Camera m_Camera;
bool m_EmittingVoice = false;
bool m_IsMinimized = false;
bool m_IsHidden = false;
bool m_IsFocusedOn = false;
private void Awake()
{
m_Camera = Camera.main;
}
void LateUpdate()
{
UpdateRotation();
UpdateMinimizedState();
}
private void OnDestroy()
{
m_Player.onColorUpdated -= UpdateColor;
m_Player.onNameUpdated -= UpdateName;
m_Player.selfMuted.OnValueChanged -= UpdateSelfMutedState;
m_Player.squelched.Unsubscribe(UpdateSquelchedState);
m_MuteButton.onClick.RemoveListener(SquelchPressed);
}
public void SetupNameTag(XRINetworkPlayer player)
{
m_PlayerId = player.OwnerClientId;
m_Player = player;
UpdateName(player.playerName);
m_ColoredImage.color = m_Player.playerColor;
m_Player.onColorUpdated += UpdateColor;
m_Player.onNameUpdated += UpdateName;
m_Player.selfMuted.OnValueChanged += UpdateSelfMutedState;
m_Player.squelched.Subscribe(UpdateSquelchedState);
m_MuteButton.onClick.AddListener(SquelchPressed);
m_SquelchedIcon.enabled = false;
}
void UpdateRotation()
{
Quaternion lookRot = Quaternion.LookRotation(m_Camera.transform.position - transform.position).normalized;
if (m_WorldUp)
{
Vector3 offset = lookRot.eulerAngles;
offset.x = 0;
offset.z = 0;
lookRot = Quaternion.Euler(offset);
}
transform.rotation = lookRot;
}
void UpdateMinimizedState()
{
var viewerDistance = Vector3.Distance(transform.position, m_Camera.transform.position);
if (viewerDistance < m_MinDistanceThreshold)
{
ToggleHiddenState(true);
}
else
{
ToggleHiddenState(false);
if (m_IsFocusedOn) return;
if (viewerDistance > m_MaxDistanceThreshold)
{
ToggleMinimizeState(true);
}
else
{
ToggleMinimizeState(false);
}
}
}
void ToggleHiddenState(bool toggle)
{
if (m_IsHidden == toggle) return;
m_IsHidden = toggle;
m_GameObjectToHide.SetActive(!toggle);
}
void ToggleMinimizeState(bool toggle)
{
if (m_IsMinimized == toggle) return;
m_IsMinimized = toggle;
foreach (var go in m_GameObjectDetailsToDeactivate)
{
go.SetActive(!toggle);
}
}
/// <summary>
/// Called from the Callout Gaze Controller and Event Trigger.
/// </summary>
public void ToggleFocused(bool toggle)
{
if (m_IsFocusedOn == toggle) return;
m_IsFocusedOn = toggle;
if (m_IsFocusedOn)
{
ToggleMinimizeState(false);
}
}
void UpdateColor(Color newColor)
{
m_ColoredImage.color = newColor;
}
void UpdateName(string newName)
{
if (string.IsNullOrEmpty(newName)) return;
m_NameTagText.text = newName;
m_InitialsText.text = newName.Substring(0, 1);
UpdateNameTagSize();
}
[ContextMenu("Update Name Tag Size")]
void UpdateNameTagSize()
{
m_NameTagText.rectTransform.sizeDelta = new Vector2(m_NameTagText.preferredWidth * m_NameTextScale, m_NameTagText.rectTransform.sizeDelta.y);
}
public void UpdateVoice(float energy)
{
m_VoiceChatFillImage.fillAmount = energy;
if (energy >= 0.001f & !m_EmittingVoice)
{
m_EmittingVoice = true;
foreach (var particle in m_voiceParticles)
{
var emission = particle.emission;
emission.rateOverTime = Mathf.Lerp(1, 2, energy);
particle.Emit(1);
particle.Play();
}
}
else if (energy <= 0.001f && m_EmittingVoice)
{
m_EmittingVoice = false;
foreach (var particle in m_voiceParticles)
{
particle.Stop(false, ParticleSystemStopBehavior.StopEmitting);
}
}
}
#region Muting
void SquelchPressed()
{
m_Player.ToggleSquelch();
}
public void UpdateSelfMutedState(bool old, bool current)
{
m_MicIcon.sprite = current ? m_MutedSprite : m_UnmutedSprite;
}
void UpdateSquelchedState(bool squelched)
{
m_SquelchedIcon.enabled = squelched;
}
#endregion
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7086c2e8e1b2a924185edf8409a34dca
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: