Initial commit
This commit is contained in:
@@ -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:
|
||||
Reference in New Issue
Block a user