Initial commit
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c5a0aaa7a164741498d68a6afe10f5aa
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,351 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
using UnityEngine.XR.Interaction.Toolkit.Samples.StarterAssets;
|
||||
|
||||
namespace XRMultiplayer
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents an anti-gravity zone in the game world.
|
||||
/// Objects and characters within the zone experience anti-gravity effects.
|
||||
/// </summary>
|
||||
public class AntiGravityZone : MonoBehaviour
|
||||
{
|
||||
[Header("Anti-Gravity Zone Settings")]
|
||||
/// <summary>
|
||||
/// The minimum range of float speeds for objects within the zone.
|
||||
/// </summary>
|
||||
[SerializeField] Vector2 m_ObjectFloatSpeedRangeMin = new(4.5f, 12.0f);
|
||||
|
||||
/// <summary>
|
||||
/// The maximum range of float speeds for objects within the zone.
|
||||
/// </summary>
|
||||
[SerializeField] Vector2 m_ObjectFloatSpeedRangeMax = new Vector2(13.0f, 15.0f);
|
||||
|
||||
/// <summary>
|
||||
/// The minimum range of float speeds for the player within the zone.
|
||||
/// </summary>
|
||||
[SerializeField] Vector2 m_PlayerFloatSpeedRangeMin = new Vector2(0, 0);
|
||||
|
||||
/// <summary>
|
||||
/// The maximum range of float speeds for the player within the zone.
|
||||
/// </summary>
|
||||
[SerializeField] Vector2 m_PlayerFloatSpeedRangeMax = new Vector2(.025f, .075f);
|
||||
|
||||
/// <summary>
|
||||
/// The range of float speeds for the particle system.
|
||||
/// </summary>
|
||||
[SerializeField] Vector2 m_ParticleFloatSpeedMinMax = new Vector2(0, 3);
|
||||
|
||||
/// <summary>
|
||||
/// The maximum velocity for objects within the zone.
|
||||
/// </summary>
|
||||
[SerializeField] float m_MaxObjectVelocityThreshold = 4.0f;
|
||||
|
||||
/// <summary>
|
||||
/// The particle system for the zone.
|
||||
/// </summary>
|
||||
[SerializeField] ParticleSystem[] m_Particles;
|
||||
|
||||
/// <summary>
|
||||
/// The collider for the zone.
|
||||
/// </summary>
|
||||
[SerializeField] SubTrigger m_AntiGravitySubTrigger;
|
||||
|
||||
/// <summary>
|
||||
/// The renderer for the zone.
|
||||
/// </summary>
|
||||
[SerializeField] Renderer m_Renderer;
|
||||
|
||||
[Header("Powered Settings")]
|
||||
/// <summary>
|
||||
/// The renderers for the zone that get colored when powered on.
|
||||
/// </summary>
|
||||
[SerializeField] Renderer[] m_PoweredOnRends;
|
||||
|
||||
/// <summary>
|
||||
/// The audio source for the zone.
|
||||
/// </summary>
|
||||
[SerializeField] AudioSource m_PoweredOnAudio;
|
||||
|
||||
/// <summary>
|
||||
/// The material for the zone when powered on.
|
||||
/// </summary>
|
||||
[SerializeField] Material m_PoweredOnMaterial;
|
||||
|
||||
/// <summary>
|
||||
/// The material for the zone when powered off.
|
||||
/// </summary>
|
||||
[SerializeField] Material m_PoweredOffMaterial;
|
||||
|
||||
[Header("Black Hole Settings")]
|
||||
|
||||
[SerializeField] GameObject[] m_BlackHoleObjects;
|
||||
|
||||
float currentSpeed;
|
||||
|
||||
/// <summary>
|
||||
/// The float speed range for objects within the zone.
|
||||
/// </summary>
|
||||
Vector2 m_FloatSpeedRange;
|
||||
|
||||
/// <summary>
|
||||
/// The float speed range for the player within the zone.
|
||||
/// </summary>
|
||||
Vector2 m_PlayerFloatSpeedRange;
|
||||
|
||||
/// <summary>
|
||||
/// The list of rigidbodies within the zone.
|
||||
/// </summary>
|
||||
CharacterController m_CharacterController;
|
||||
|
||||
/// <summary>
|
||||
/// The list of rigidbodies within the zone.
|
||||
/// </summary>
|
||||
List<Rigidbody> m_RigidbodyList = new List<Rigidbody>();
|
||||
|
||||
bool m_IsBlackHole = false;
|
||||
|
||||
bool m_IsPowered = false;
|
||||
|
||||
/// <inheritdoc/>
|
||||
private void Awake()
|
||||
{
|
||||
PowerOff();
|
||||
UpdateSpeed(0);
|
||||
m_AntiGravitySubTrigger.OnTriggerAction += SubTriggered;
|
||||
ToggleBlackHoles(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
void Update()
|
||||
{
|
||||
if (m_IsBlackHole) return;
|
||||
|
||||
if (m_CharacterController != null)
|
||||
{
|
||||
m_CharacterController.transform.position += Vector3.up * Random.Range(m_PlayerFloatSpeedRange.x, m_PlayerFloatSpeedRange.y);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
void FixedUpdate()
|
||||
{
|
||||
if (m_IsBlackHole) return;
|
||||
|
||||
foreach (var rigidbody in m_RigidbodyList)
|
||||
{
|
||||
if (rigidbody == null) continue;
|
||||
if (!rigidbody.isKinematic)
|
||||
{
|
||||
// If the rigidbody is not kinematic and it's Y velocity is under the threshold, apply a force to simulate floating.
|
||||
if (rigidbody.linearVelocity.y < m_MaxObjectVelocityThreshold)
|
||||
{
|
||||
currentSpeed = Random.Range(m_FloatSpeedRange.x, m_FloatSpeedRange.y);
|
||||
rigidbody.AddForce(Vector3.up * currentSpeed);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Turns off the anti-gravity zone.
|
||||
/// </summary>
|
||||
///<remarks> Called from the Socket Interactor.</remarks>
|
||||
public void PowerOff()
|
||||
{
|
||||
m_IsPowered = false;
|
||||
m_AntiGravitySubTrigger.subTriggerCollider.enabled = false;
|
||||
m_Renderer.enabled = false;
|
||||
m_PoweredOnAudio.enabled = false;
|
||||
m_RigidbodyList.Clear();
|
||||
|
||||
foreach (var p in m_Particles)
|
||||
{
|
||||
p.Stop(false, ParticleSystemStopBehavior.StopEmitting);
|
||||
}
|
||||
|
||||
foreach (Renderer r in m_PoweredOnRends)
|
||||
{
|
||||
if (r == null) continue;
|
||||
r.material = m_PoweredOffMaterial;
|
||||
}
|
||||
|
||||
if (m_CharacterController != null)
|
||||
{
|
||||
m_CharacterController.GetComponentInChildren<DynamicMoveProvider>().useGravity = true;
|
||||
m_CharacterController = null;
|
||||
}
|
||||
|
||||
ToggleBlackHoles(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Turns on the anti-gravity zone.
|
||||
/// </summary>
|
||||
///<remarks> Called from the Socket Interactor.</remarks>
|
||||
public void PowerOn()
|
||||
{
|
||||
m_IsPowered = true;
|
||||
m_PoweredOnAudio.enabled = true;
|
||||
|
||||
foreach (Renderer r in m_PoweredOnRends)
|
||||
{
|
||||
r.material = m_PoweredOnMaterial;
|
||||
}
|
||||
|
||||
if (m_IsBlackHole)
|
||||
{
|
||||
m_Renderer.enabled = false;
|
||||
ToggleBlackHoles(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_Renderer.enabled = true;
|
||||
m_AntiGravitySubTrigger.subTriggerCollider.enabled = true;
|
||||
foreach (var p in m_Particles)
|
||||
{
|
||||
p.Play();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the speed of the anti-gravity effects based on the given value.
|
||||
/// </summary>
|
||||
/// <param name="value">The value used to interpolate the speed range.</param>
|
||||
public void UpdateSpeed(float value)
|
||||
{
|
||||
m_FloatSpeedRange = new Vector2(Mathf.Lerp(m_ObjectFloatSpeedRangeMin.x, m_ObjectFloatSpeedRangeMax.x, value), Mathf.Lerp(m_ObjectFloatSpeedRangeMin.y, m_ObjectFloatSpeedRangeMax.y, value));
|
||||
m_PlayerFloatSpeedRange = new Vector2(Mathf.Lerp(m_PlayerFloatSpeedRangeMin.x, m_PlayerFloatSpeedRangeMax.x, value), Mathf.Lerp(m_PlayerFloatSpeedRangeMin.y, m_PlayerFloatSpeedRangeMax.y, value));
|
||||
|
||||
for (int i = 0; i < m_Particles.Length; i++)
|
||||
|
||||
{
|
||||
var main = m_Particles[i].main;
|
||||
main.startSpeed = Mathf.Lerp(m_ParticleFloatSpeedMinMax.x / (i + 1), m_ParticleFloatSpeedMinMax.y / (i + 1), value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Callback for the SubTrigger action.
|
||||
/// </summary>
|
||||
/// <param name="other"></param>
|
||||
/// <param name="entered"></param>
|
||||
void SubTriggered(Collider other, bool entered)
|
||||
{
|
||||
if (entered)
|
||||
{
|
||||
SubTriggerEntered(other);
|
||||
}
|
||||
else
|
||||
{
|
||||
SubTriggerExited(other);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Callback for the SubTrigger OnTriggerEnter.
|
||||
/// </summary>
|
||||
/// <param name="other"></param>
|
||||
private void SubTriggerEntered(Collider other)
|
||||
{
|
||||
Rigidbody body = other.GetComponentInParent<Rigidbody>();
|
||||
if (body != null)
|
||||
{
|
||||
if (m_RigidbodyList.Contains(body)) return;
|
||||
// If the object is an interactable and it throws on detach, add it to the list.
|
||||
if (body.TryGetComponent(out UnityEngine.XR.Interaction.Toolkit.Interactables.XRGrabInteractable interactable))
|
||||
{
|
||||
if (interactable.throwOnDetach)
|
||||
{
|
||||
m_RigidbodyList.Add(body);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
m_RigidbodyList.Add(body);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// If the object is a character controller, add it to the list.
|
||||
if (other.TryGetComponent(out CharacterController controller))
|
||||
{
|
||||
m_CharacterController = controller;
|
||||
m_CharacterController.GetComponentInChildren<DynamicMoveProvider>().useGravity = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Callback for the SubTrigger OnTriggerExit.
|
||||
/// </summary>
|
||||
/// <param name="other"></param>
|
||||
private void SubTriggerExited(Collider other)
|
||||
{
|
||||
Rigidbody body = other.GetComponentInParent<Rigidbody>();
|
||||
if (body != null)
|
||||
{
|
||||
if (m_RigidbodyList.Contains(body))
|
||||
{
|
||||
m_RigidbodyList.Remove(body);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (other.TryGetComponent(out CharacterController controller))
|
||||
{
|
||||
controller.GetComponentInChildren<DynamicMoveProvider>().useGravity = true;
|
||||
m_CharacterController = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Toggles the black hole effect.
|
||||
/// This is used to destroy objects within the zone.
|
||||
/// </summary>
|
||||
/// <param name="toggle"></param>
|
||||
public void ToggleBlackHole(bool toggle)
|
||||
{
|
||||
m_IsBlackHole = toggle;
|
||||
if (m_IsBlackHole)
|
||||
{
|
||||
m_Renderer.enabled = false;
|
||||
if (m_IsPowered)
|
||||
{
|
||||
m_AntiGravitySubTrigger.subTriggerCollider.enabled = false;
|
||||
foreach (var p in m_Particles)
|
||||
{
|
||||
p.Stop();
|
||||
}
|
||||
|
||||
ToggleBlackHoles(true);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (m_IsPowered)
|
||||
{
|
||||
m_Renderer.enabled = true;
|
||||
m_AntiGravitySubTrigger.subTriggerCollider.enabled = true;
|
||||
foreach (var p in m_Particles)
|
||||
{
|
||||
p.Play();
|
||||
}
|
||||
|
||||
ToggleBlackHoles(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ToggleBlackHoles(bool toggle)
|
||||
{
|
||||
foreach (GameObject g in m_BlackHoleObjects)
|
||||
{
|
||||
g.SetActive(toggle);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6f3cc003d5d460e4d9d8ccb1291bcd16
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ca09e8d0255658846b8e79a331ecfc74
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,86 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.XR.Interaction.Toolkit;
|
||||
using UnityEngine.XR.Interaction.Toolkit.Interactables;
|
||||
|
||||
namespace XRMultiplayer
|
||||
{
|
||||
[RequireComponent(typeof(TrailRenderer))]
|
||||
public class PenTrail : MonoBehaviour
|
||||
{
|
||||
public TrailRenderer trailRenderer => m_TrailRenderer;
|
||||
TrailRenderer m_TrailRenderer;
|
||||
|
||||
[SerializeField] bool m_UseLifetime = false;
|
||||
[SerializeField] float m_ObjectLifetimeInSeconds = 900.0f;
|
||||
|
||||
GameObject m_SpawnedInteractableObject;
|
||||
Color m_StartColor;
|
||||
float m_StartWidth;
|
||||
// Start is called before the first frame update
|
||||
void Awake()
|
||||
{
|
||||
if (!TryGetComponent(out m_TrailRenderer))
|
||||
{
|
||||
Utils.Log("Missing Components! Disabling Now.", 2);
|
||||
enabled = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetColor(Color color)
|
||||
{
|
||||
m_StartColor = color;
|
||||
UpdateColor(m_StartColor);
|
||||
}
|
||||
|
||||
void UpdateColor(Color color)
|
||||
{
|
||||
m_TrailRenderer.material.color = color;
|
||||
m_TrailRenderer.startColor = color;
|
||||
m_TrailRenderer.endColor = color;
|
||||
}
|
||||
|
||||
public void CreateInteractableTrail()
|
||||
{
|
||||
m_StartWidth = m_TrailRenderer.startWidth;
|
||||
|
||||
Mesh mesh = new();
|
||||
m_TrailRenderer.BakeMesh(mesh, true);
|
||||
|
||||
m_SpawnedInteractableObject = new();
|
||||
m_SpawnedInteractableObject.transform.position = Vector3.zero;
|
||||
m_SpawnedInteractableObject.AddComponent<MeshFilter>().mesh = mesh;
|
||||
m_SpawnedInteractableObject.AddComponent<MeshCollider>().sharedMesh = mesh;
|
||||
|
||||
m_SpawnedInteractableObject.transform.parent = transform;
|
||||
|
||||
XRSimpleInteractable interactable = m_SpawnedInteractableObject.AddComponent<XRSimpleInteractable>();
|
||||
|
||||
interactable.activated.AddListener(DestroyTrail);
|
||||
interactable.hoverEntered.AddListener(HoverEntered);
|
||||
interactable.hoverExited.AddListener(HoverExited);
|
||||
|
||||
if (m_UseLifetime)
|
||||
Destroy(gameObject, m_ObjectLifetimeInSeconds);
|
||||
}
|
||||
|
||||
void HoverEntered(HoverEnterEventArgs args)
|
||||
{
|
||||
m_TrailRenderer.startWidth = m_StartWidth * 3.0f;
|
||||
m_TrailRenderer.endWidth = m_StartWidth * 3.0f;
|
||||
UpdateColor(Color.red);
|
||||
}
|
||||
|
||||
void HoverExited(HoverExitEventArgs args)
|
||||
{
|
||||
m_TrailRenderer.startWidth = m_StartWidth;
|
||||
m_TrailRenderer.endWidth = m_StartWidth;
|
||||
UpdateColor(m_StartColor);
|
||||
}
|
||||
|
||||
void DestroyTrail(ActivateEventArgs args)
|
||||
{
|
||||
Destroy(gameObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 07a342a134654fc4094a7fc45b3105b9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,75 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using XRMultiplayer;
|
||||
|
||||
public class SimplePen : MonoBehaviour
|
||||
{
|
||||
[SerializeField] PenTrail m_TrailRendererPrefab;
|
||||
[SerializeField] Transform m_PenTipTransform;
|
||||
[SerializeField] Renderer m_PenTipRenderer;
|
||||
|
||||
NetworkPhysicsInteractable m_NetworkInteractable;
|
||||
|
||||
PenTrail m_CurrentTrailRenderer;
|
||||
|
||||
Color m_CurrentColor;
|
||||
|
||||
List<PenTrail> m_PenTrails = new();
|
||||
|
||||
void Awake()
|
||||
{
|
||||
TryGetComponent(out m_NetworkInteractable);
|
||||
}
|
||||
|
||||
void Start()
|
||||
{
|
||||
SetColor();
|
||||
XRINetworkGameManager.Connected.Subscribe(ConnectedToNetworkGame);
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
XRINetworkGameManager.Connected.Unsubscribe(ConnectedToNetworkGame);
|
||||
}
|
||||
|
||||
void ConnectedToNetworkGame(bool connected)
|
||||
{
|
||||
if (!connected)
|
||||
{
|
||||
foreach (var trail in m_PenTrails)
|
||||
{
|
||||
Destroy(trail.gameObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void ToggleDrawing(bool toggle)
|
||||
{
|
||||
if (toggle && m_CurrentTrailRenderer == null)
|
||||
{
|
||||
m_CurrentTrailRenderer = Instantiate(m_TrailRendererPrefab, m_PenTipTransform.position, m_PenTipTransform.rotation, m_PenTipTransform);
|
||||
m_CurrentTrailRenderer.SetColor(m_CurrentColor);
|
||||
}
|
||||
else if (!toggle && m_CurrentTrailRenderer != null)
|
||||
{
|
||||
m_CurrentTrailRenderer.transform.SetParent(null);
|
||||
m_CurrentTrailRenderer.CreateInteractableTrail();
|
||||
m_CurrentTrailRenderer = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetColor()
|
||||
{
|
||||
if (XRINetworkGameManager.Instance.GetPlayerByID(m_NetworkInteractable.OwnerClientId, out XRINetworkPlayer player))
|
||||
{
|
||||
m_CurrentColor = player.playerColor;
|
||||
}
|
||||
// Failed to get player, might be offline
|
||||
else
|
||||
{
|
||||
m_CurrentColor = XRINetworkGameManager.LocalPlayerColor.Value;
|
||||
}
|
||||
m_PenTipRenderer.material.color = m_CurrentColor;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b950049051c4d15468f2446a40a6c40e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2e4db9aa154079b45a7ed5304be9232a
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,49 @@
|
||||
using UnityEngine;
|
||||
using TMPro;
|
||||
|
||||
namespace XRMultiplayer
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a message text on a message board.
|
||||
/// </summary>
|
||||
public class MessageText : MonoBehaviour
|
||||
{
|
||||
/// <summary>
|
||||
/// The text component to display the message.
|
||||
/// </summary>
|
||||
[SerializeField] private TMP_Text text;
|
||||
|
||||
/// <summary>
|
||||
/// The text component to display the time.
|
||||
/// </summary>
|
||||
[SerializeField] private TMP_Text timeText;
|
||||
|
||||
/// <summary>
|
||||
/// Use this value for any headers or offsets that need to be added to the message text when calculating height.
|
||||
/// </summary>
|
||||
[SerializeField, Tooltip("Use this value for any headers or offsets that need to be added to the message text when calculating height.")]
|
||||
float m_BonusScale = 1.0f;
|
||||
|
||||
[SerializeField] RectTransform m_RectTransform;
|
||||
|
||||
/// <summary>
|
||||
/// Sets the message and time to be displayed.
|
||||
/// </summary>
|
||||
/// <param name="message">The message to be displayed.</param>
|
||||
/// <param name="time">The time to be displayed.</param>
|
||||
public void SetMessage(string message, string time)
|
||||
{
|
||||
text.SetText(message);
|
||||
timeText.SetText(time);
|
||||
Canvas.ForceUpdateCanvases();
|
||||
SnapHeight();
|
||||
}
|
||||
|
||||
[ContextMenu("Snap Height")]
|
||||
void SnapHeight()
|
||||
{
|
||||
m_RectTransform.sizeDelta = new Vector2(m_RectTransform.sizeDelta.x, text.preferredHeight * m_BonusScale /* * text.fontSize*/);
|
||||
Canvas.ForceUpdateCanvases();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2bc4ec75d1dc40c4e8da2c62f9789aff
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,173 @@
|
||||
using UnityEngine;
|
||||
using Unity.Netcode;
|
||||
using Unity.Collections;
|
||||
using System;
|
||||
using UnityEngine.XR.Interaction.Toolkit.Samples.SpatialKeyboard;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor;
|
||||
#endif
|
||||
|
||||
namespace XRMultiplayer
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a message board that allows players to submit and display messages in a networked environment.
|
||||
/// </summary>
|
||||
public class NetworkMessageBoard : NetworkBehaviour
|
||||
{
|
||||
/// <summary>
|
||||
/// The prefab for the message text.
|
||||
/// </summary>
|
||||
[SerializeField] GameObject m_MessagePrefab;
|
||||
|
||||
/// <summary>
|
||||
/// The transform that contains the viewport for the messages.
|
||||
/// </summary>
|
||||
[SerializeField] Transform m_ContentViewport;
|
||||
|
||||
/// <summary>
|
||||
/// The maximum number of messages that can be displayed.
|
||||
/// </summary>
|
||||
[SerializeField] int m_MaxMessageCount = 100;
|
||||
|
||||
/// <summary>
|
||||
/// The maximum number of characters that can be displayed in a message.
|
||||
/// </summary>
|
||||
[SerializeField] int m_MaxCharacterCount = 256;
|
||||
|
||||
/// <summary>
|
||||
/// The list of current messages.
|
||||
/// </summary>
|
||||
NetworkList<FixedString512Bytes> messageList;
|
||||
|
||||
/// <inheritdoc/>
|
||||
void Start()
|
||||
{
|
||||
XRINetworkGameManager.Connected.Subscribe(ConnectedToNetwork);
|
||||
messageList = new NetworkList<FixedString512Bytes>(default, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the network connection status changes.
|
||||
/// </summary>
|
||||
/// <param name="connected">Indicates whether the player is connected to the network.</param>
|
||||
void ConnectedToNetwork(bool connected)
|
||||
{
|
||||
if (!connected)
|
||||
{
|
||||
foreach (Transform t in m_ContentViewport)
|
||||
{
|
||||
Destroy(t.gameObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Called from XRIKeyboardDisplay
|
||||
public void ToggleKeyboardOpen(bool toggle)
|
||||
{
|
||||
GlobalNonNativeKeyboard.instance.keyboard.closeOnSubmit = !toggle;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void OnNetworkSpawn()
|
||||
{
|
||||
base.OnNetworkSpawn();
|
||||
if (IsServer)
|
||||
{
|
||||
messageList.Clear();
|
||||
}
|
||||
foreach (FixedString512Bytes message in messageList)
|
||||
{
|
||||
CreateText(message.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Submits a text message locally.
|
||||
/// </summary>
|
||||
/// <param name="text">The text message to submit.</param>
|
||||
public void SubmitTextLocal(string text)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text) || string.IsNullOrWhiteSpace(text)) return;
|
||||
string textToSend = $"<b>{XRINetworkPlayer.LocalPlayer.playerName}</b>:<br><br>{text}";
|
||||
|
||||
if (textToSend.Length > m_MaxCharacterCount)
|
||||
{
|
||||
textToSend = textToSend.Substring(0, m_MaxCharacterCount);
|
||||
}
|
||||
|
||||
FixedString512Bytes newText = new FixedString512Bytes(textToSend);
|
||||
SubmitMessageServerRpc(newText);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Submits a message to the server.
|
||||
/// </summary>
|
||||
/// <param name="text">The message to submit.</param>
|
||||
[ServerRpc(RequireOwnership = false)]
|
||||
void SubmitMessageServerRpc(FixedString512Bytes text)
|
||||
{
|
||||
messageList.Add(text);
|
||||
if (messageList.Count > m_MaxMessageCount)
|
||||
{
|
||||
messageList.RemoveAt(0);
|
||||
}
|
||||
SubmitMessageClientRpc(text);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Submits a message to the clients.
|
||||
/// </summary>
|
||||
/// <param name="text">The message to submit.</param>
|
||||
[ClientRpc]
|
||||
void SubmitMessageClientRpc(FixedString512Bytes text)
|
||||
{
|
||||
CreateText(text.ToString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a text message and adds it to the message board.
|
||||
/// </summary>
|
||||
/// <param name="text">The text of the message.</param>
|
||||
void CreateText(string text)
|
||||
{
|
||||
Instantiate(m_MessagePrefab, m_ContentViewport).GetComponent<MessageText>().SetMessage(text, DateTime.Now.ToString("h:mm tt"));
|
||||
// message.SetMessage(text, DateTime.Now.ToString("h:mm tt"));
|
||||
|
||||
if (m_ContentViewport.childCount > m_MaxMessageCount)
|
||||
{
|
||||
Destroy(m_ContentViewport.GetChild(0).gameObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
[CustomEditor(typeof(NetworkMessageBoard), true), CanEditMultipleObjects]
|
||||
public class NetworkMessageBoardEditor : Editor
|
||||
{
|
||||
|
||||
[SerializeField, TextArea(10, 15)] string m_DebugText;
|
||||
public override void OnInspectorGUI()
|
||||
{
|
||||
base.OnInspectorGUI();
|
||||
GUILayout.Space(10);
|
||||
GUILayout.Label("Debug Area", EditorStyles.boldLabel);
|
||||
GUI.enabled = XRINetworkGameManager.Connected.Value;
|
||||
if(!XRINetworkGameManager.Connected.Value)
|
||||
{
|
||||
GUILayout.Label("Connect to a network to submit messages.", EditorStyles.helpBox);
|
||||
}
|
||||
else
|
||||
{
|
||||
GUILayout.Label("Debug Text");
|
||||
m_DebugText = GUILayout.TextArea(m_DebugText);
|
||||
}
|
||||
if (GUILayout.Button("Submit Text Debug"))
|
||||
{
|
||||
((NetworkMessageBoard)target).SubmitTextLocal(m_DebugText);
|
||||
}
|
||||
GUI.enabled = true;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0ec9afa5a52ab1f49ba7d6429da6a00d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,157 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
|
||||
namespace XRMultiplayer
|
||||
{
|
||||
/// <summary>
|
||||
/// This class is responsible for handling the networked despawning of objects based within
|
||||
/// a trigger volume. It also resets the player controller if they are within the bounds.
|
||||
/// It also handles the spawning of particles when an object is despawned.
|
||||
/// </summary>
|
||||
public class NetworkObjectDestroyer : NetworkBehaviour
|
||||
{
|
||||
/// <summary>
|
||||
/// The sub-trigger that triggers the action.
|
||||
/// </summary>
|
||||
[SerializeField] SubTrigger m_SubTrigger;
|
||||
|
||||
/// <summary>
|
||||
/// The Y offset for the particles that are spawned.
|
||||
/// </summary>
|
||||
[SerializeField] float m_YOffset = .1f;
|
||||
|
||||
/// <summary>
|
||||
/// The list of scene interactables that cannot be destroyed.
|
||||
/// </summary>
|
||||
[SerializeField] List<NetworkBaseInteractable> m_UndestroyableInteractables;
|
||||
|
||||
readonly List<NetworkBaseInteractable> m_DestroyedInteractables = new();
|
||||
|
||||
Pooler m_ParticlePooler;
|
||||
|
||||
/// <summary>
|
||||
/// Awake is called when the script instance is being loaded.
|
||||
/// </summary>
|
||||
void Awake()
|
||||
{
|
||||
if (!TryGetComponent(out m_ParticlePooler))
|
||||
{
|
||||
Utils.LogError("NetworkObjectDestroyer requires a Pooler component to be attached to the same GameObject.");
|
||||
return;
|
||||
};
|
||||
m_SubTrigger.OnTriggerAction += Triggered;
|
||||
}
|
||||
|
||||
public override void OnDestroy()
|
||||
{
|
||||
base.OnDestroy();
|
||||
m_SubTrigger.OnTriggerAction -= Triggered;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event handler for the trigger action.
|
||||
/// </summary>
|
||||
/// <param name="other">The collider that triggered the action.</param>
|
||||
/// <param name="entered">A flag indicating if the collider entered or exited the trigger.</param>
|
||||
void Triggered(Collider other, bool entered)
|
||||
{
|
||||
if (!entered) return;
|
||||
NetworkBaseInteractable networkBaseInteractable = other.GetComponentInParent<NetworkBaseInteractable>();
|
||||
|
||||
if (networkBaseInteractable != null)
|
||||
{
|
||||
if (m_UndestroyableInteractables.Contains(networkBaseInteractable) || networkBaseInteractable.isInteracting) return;
|
||||
|
||||
// This will prevent objects with multiple colliders from being destroyed multiple times.
|
||||
if (m_DestroyedInteractables.Contains(networkBaseInteractable)) return;
|
||||
m_DestroyedInteractables.Add(networkBaseInteractable);
|
||||
|
||||
Vector3 position = networkBaseInteractable.transform.position + Vector3.up * m_YOffset;
|
||||
if (!networkBaseInteractable.IsSpawned)
|
||||
{
|
||||
Destroy(networkBaseInteractable);
|
||||
PlayDestroyEffect(position);
|
||||
// PlayDestroyEffectRpc(position);
|
||||
}
|
||||
else if (IsServer)
|
||||
{
|
||||
networkBaseInteractable.NetworkObject.Despawn();
|
||||
PlayDestroyEffectRpc(position);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (other.TryGetComponent(out CharacterResetter playerResetter))
|
||||
{
|
||||
PlayDestroyEffectRpc(playerResetter.transform.position);
|
||||
// PlayDestroyEffectServerRpc(playerResetter.transform.position, NetworkManager.Singleton.LocalClientId);
|
||||
playerResetter.ResetPlayer();
|
||||
}
|
||||
|
||||
if (other.TryGetComponent(out Projectile projectile))
|
||||
{
|
||||
PlayDestroyEffectRpc(projectile.transform.position);
|
||||
// Destroy(Instantiate(m_DestroyParticles, projectile.transform.position, Quaternion.identity), 1.0f);
|
||||
projectile.ResetProjectile();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plays the destroy effect at the specified position.
|
||||
/// </summary>
|
||||
/// <param name="position">The position at which to play the destroy effect.</param>
|
||||
void PlayDestroyEffect(Vector3 position)
|
||||
{
|
||||
GameObject particleObject = m_ParticlePooler.GetItem();
|
||||
particleObject.transform.position = position;
|
||||
StartCoroutine(ReturnParticleToPool(particleObject));
|
||||
// var particles = particleObject.GetComponent<ParticleSystem>();
|
||||
// particles.Play();
|
||||
|
||||
// var main = particles.main;
|
||||
// main.stopAction = m_ParticlePooler.ReturnItem(particleObject);
|
||||
// Destroy(Instantiate(m_DestroyParticles, position, Quaternion.identity), 1.0f);
|
||||
// PlayDestroyEffectServerRpc(position, NetworkManager.Singleton.LocalClientId);
|
||||
}
|
||||
|
||||
IEnumerator ReturnParticleToPool(GameObject particleObject)
|
||||
{
|
||||
yield return new WaitForSeconds(1.0f);
|
||||
m_ParticlePooler.ReturnItem(particleObject);
|
||||
}
|
||||
|
||||
// /// <summary>
|
||||
// /// Server RPC method for playing the destroy effect on the server.
|
||||
// /// </summary>
|
||||
// /// <param name="position">The position at which to play the destroy effect.</param>
|
||||
// /// <param name="clientId">The client ID of the player triggering the effect.</param>
|
||||
// [ServerRpc(RequireOwnership = false)]
|
||||
// void PlayDestroyEffectServerRpc(Vector3 position, ulong clientId)
|
||||
// {
|
||||
// PlayDestroyEffectClientRpc(position, clientId);
|
||||
// }
|
||||
|
||||
// /// <summary>
|
||||
// /// Client RPC method for playing the destroy effect on the clients.
|
||||
// /// </summary>
|
||||
// /// <param name="position">The position at which to play the destroy effect.</param>
|
||||
// /// <param name="clientId">The client ID of the player triggering the effect.</param>
|
||||
// [ClientRpc]
|
||||
// void PlayDestroyEffectClientRpc(Vector3 position, ulong clientId)
|
||||
// {
|
||||
// if (clientId != NetworkManager.Singleton.LocalClientId)
|
||||
// Destroy(Instantiate(m_DestroyParticles, position, Quaternion.identity), 1.0f);
|
||||
// }
|
||||
|
||||
[Rpc(SendTo.Everyone)]
|
||||
void PlayDestroyEffectRpc(Vector3 position)
|
||||
{
|
||||
PlayDestroyEffect(position);
|
||||
// if (clientId != NetworkManager.Singleton.LocalClientId)
|
||||
// Destroy(Instantiate(m_DestroyParticles, position, Quaternion.identity), 1.0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7fd7afb96cdea764f8dcd3c1e4807183
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b95cdd7e24bd7174ebd321cb8ecf4da0
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fc458c0abfe5b264d96cd9ee6413f12c
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+288
@@ -0,0 +1,288 @@
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor;
|
||||
using UnityEditor.UIElements;
|
||||
using UnityEngine.UIElements;
|
||||
#endif
|
||||
|
||||
namespace XRMultiplayer
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
[CustomEditor(typeof(NetworkObjectDispenser))]
|
||||
public class NetworkObjectDispenserEditor : Editor
|
||||
{
|
||||
public override VisualElement CreateInspectorGUI()
|
||||
{
|
||||
return DrawBaseContainer();
|
||||
}
|
||||
|
||||
VisualElement DrawBaseContainer()
|
||||
{
|
||||
var root = new VisualElement();
|
||||
|
||||
Box backgroundBox = new();
|
||||
Foldout foldout = new Foldout
|
||||
{
|
||||
name = "Dispenser Panels Foldout",
|
||||
text = "Dispenser Spawn Groups",
|
||||
};
|
||||
|
||||
// Creating a second nested foldout to help with the layout
|
||||
Foldout persistentFoldout = new Foldout
|
||||
{
|
||||
name = "Persistent Panel Foldout",
|
||||
text = "Persistent Spawn Group",
|
||||
};
|
||||
|
||||
persistentFoldout.style.paddingLeft = 11.0f;
|
||||
|
||||
|
||||
persistentFoldout.Add(new PropertyField(serializedObject.FindProperty("m_PersistentPanel")));
|
||||
foldout.Add(persistentFoldout);
|
||||
|
||||
var togglePanels = new PropertyField(serializedObject.FindProperty("m_Panels"), "Toggleable Spawn Groups");
|
||||
foldout.Add(togglePanels);
|
||||
|
||||
backgroundBox.Add(foldout);
|
||||
root.Add(backgroundBox);
|
||||
|
||||
var capacityContainer = GetCapacityContainer();
|
||||
root.Add(capacityContainer);
|
||||
capacityContainer.PlaceBehind(backgroundBox);
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
VisualElement GetCapacityContainer()
|
||||
{
|
||||
var container = new VisualElement();
|
||||
container.Add(new PropertyField(serializedObject.FindProperty("m_ClearButton")));
|
||||
container.Add(new PropertyField(serializedObject.FindProperty("m_Capacity")));
|
||||
container.Add(new PropertyField(serializedObject.FindProperty("m_CountText")));
|
||||
container.Add(new PropertyField(serializedObject.FindProperty("m_DistanceCheckTimeInterval")));
|
||||
return container;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
[CustomPropertyDrawer(typeof(DispenserPanel))]
|
||||
public class DispenserPanelDrawer : PropertyDrawer
|
||||
{
|
||||
NetworkObjectDispenser m_target = null;
|
||||
public override VisualElement CreatePropertyGUI(SerializedProperty property)
|
||||
{
|
||||
if(Selection.activeGameObject != null && Selection.activeGameObject.GetComponent<NetworkObjectDispenser>() != null)
|
||||
{
|
||||
m_target = (NetworkObjectDispenser)Selection.activeGameObject.GetComponent(typeof(NetworkObjectDispenser));
|
||||
}
|
||||
var container = new VisualElement();
|
||||
|
||||
int panelId = property.FindPropertyRelative("panelId").intValue;
|
||||
|
||||
Box backgroundBox = new();
|
||||
backgroundBox.style.backgroundColor = new UnityEngine.Color(0.1f, 0.1f, 0.1f, .5f);
|
||||
backgroundBox.Add(new PropertyField(property.FindPropertyRelative("panelName"), "Group Name"));
|
||||
backgroundBox.Add(new PropertyField(property.FindPropertyRelative("panel"), "Group GameObject"));
|
||||
|
||||
|
||||
if(m_target == null)
|
||||
{
|
||||
Label errorLabel = new Label("Please select a NetworkObjectDispenser GameObject to Spawn Previews.");
|
||||
backgroundBox.Add(errorLabel);
|
||||
container.Add(backgroundBox);
|
||||
return container;
|
||||
}
|
||||
|
||||
var buttonContainer = new VisualElement();
|
||||
buttonContainer.style.flexDirection = FlexDirection.Row;
|
||||
TextElement textElement = new()
|
||||
{
|
||||
style =
|
||||
{
|
||||
fontSize = 12,
|
||||
marginTop = 5.0f,
|
||||
marginLeft = 5.0f,
|
||||
marginBottom = 5.0f
|
||||
},
|
||||
text = "Group Preview"
|
||||
};
|
||||
buttonContainer.Add(textElement);
|
||||
var showButton = ObjectDispenserEditorButton.CreateDefaultButton("Show");
|
||||
showButton.style.marginLeft = 50.0f;
|
||||
showButton.style.alignSelf = Align.FlexEnd;
|
||||
var hideButton = ObjectDispenserEditorButton.CreateDefaultButton("Hide");
|
||||
hideButton.style.alignSelf = Align.FlexEnd;
|
||||
|
||||
PanelToggleGroup panelToggle = new(showButton, hideButton, panelId, -1);
|
||||
|
||||
showButton.clicked += () => SpawnPanel(panelToggle);
|
||||
hideButton.clicked += () => ClearPanel(panelToggle);
|
||||
m_target.OnProxiesUpdated += () => UpdateButtonsState(panelToggle);
|
||||
|
||||
UpdateButtonsState(panelToggle);
|
||||
|
||||
buttonContainer.Add(showButton);
|
||||
buttonContainer.Add(hideButton);
|
||||
|
||||
backgroundBox.Add(buttonContainer);
|
||||
|
||||
backgroundBox.Add(new PropertyField(property.FindPropertyRelative("dispenserSlots"), "Spawn Slots"));
|
||||
container.Add(backgroundBox);
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
void SpawnPanel(PanelToggleGroup panelToggle)
|
||||
{
|
||||
if(m_target != null)
|
||||
m_target.SpawnProxyPanel(panelToggle.panelId);
|
||||
UpdateButtonsState(panelToggle);
|
||||
}
|
||||
|
||||
void ClearPanel(PanelToggleGroup panelToggle)
|
||||
{
|
||||
if(m_target != null)
|
||||
m_target.ClearProxyPanel(panelToggle.panelId);
|
||||
UpdateButtonsState(panelToggle);
|
||||
}
|
||||
|
||||
void UpdateButtonsState(PanelToggleGroup panelToggle)
|
||||
{
|
||||
bool isShowing = m_target.IsProxyPanelShowing(panelToggle.panelId);
|
||||
bool IsFull = m_target.IsProxyPanelFull(panelToggle.panelId);
|
||||
panelToggle.showButton.SetEnabled(!IsFull);
|
||||
panelToggle.hideButton.SetEnabled(isShowing);
|
||||
}
|
||||
}
|
||||
|
||||
[CustomPropertyDrawer(typeof(DispenserSlot))]
|
||||
public class DispenserSlotDrawer : PropertyDrawer
|
||||
{
|
||||
NetworkObjectDispenser m_target = null;
|
||||
public override VisualElement CreatePropertyGUI(SerializedProperty property)
|
||||
{
|
||||
if(Selection.activeGameObject != null && Selection.activeGameObject.GetComponent<NetworkObjectDispenser>() != null)
|
||||
{
|
||||
m_target = (NetworkObjectDispenser)Selection.activeGameObject.GetComponent(typeof(NetworkObjectDispenser));
|
||||
}
|
||||
|
||||
var container = new VisualElement();
|
||||
|
||||
int slotId = property.FindPropertyRelative("slotId").intValue;
|
||||
int panelId = property.FindPropertyRelative("panelId").intValue;
|
||||
bool hasSpawnedProxy = property.FindPropertyRelative("hasSpawnedProxy").boolValue;
|
||||
|
||||
Box backgroundBox = new();
|
||||
backgroundBox.style.backgroundColor = new UnityEngine.Color(0.1f, 0.1f, 0.1f, .5f);
|
||||
backgroundBox.Add(new PropertyField(property.FindPropertyRelative("freezeOnSpawn"), "Freeze On Spawn"));
|
||||
backgroundBox.Add(new PropertyField(property.FindPropertyRelative("distanceToSpawnNew"), "Distance To Spawn New"));
|
||||
backgroundBox.Add(new PropertyField(property.FindPropertyRelative("spawnCooldown"), "Spawn Cooldown"));
|
||||
backgroundBox.Add(new PropertyField(property.FindPropertyRelative("dispenserSlotTransform"), "Spawn Transform"));
|
||||
backgroundBox.Add(new PropertyField(property.FindPropertyRelative("spawnableInteractablePrefab"), "Spawn Prefab"));
|
||||
|
||||
if(m_target == null)
|
||||
{
|
||||
Label errorLabel = new Label("Please select a NetworkObjectDispenser GameObject to Spawn Previews.");
|
||||
backgroundBox.Add(errorLabel);
|
||||
container.Add(backgroundBox);
|
||||
return container;
|
||||
}
|
||||
|
||||
|
||||
var buttonContainer = new VisualElement();
|
||||
buttonContainer.style.flexDirection = FlexDirection.Row;
|
||||
|
||||
|
||||
TextElement textElement = new()
|
||||
{
|
||||
style =
|
||||
{
|
||||
fontSize = 12,
|
||||
marginTop = 5.0f,
|
||||
marginLeft = 5.0f,
|
||||
marginBottom = 5.0f
|
||||
},
|
||||
text = "Object Preview"
|
||||
};
|
||||
buttonContainer.Add(textElement);
|
||||
var showButton = ObjectDispenserEditorButton.CreateDefaultButton("Show");
|
||||
showButton.style.marginLeft = 50.0f;
|
||||
var hideButton = ObjectDispenserEditorButton.CreateDefaultButton("Hide");
|
||||
|
||||
PanelToggleGroup panelToggle = new(showButton, hideButton, panelId, slotId);
|
||||
|
||||
showButton.clicked += () => SpawnButton(panelToggle);
|
||||
hideButton.clicked += () => ClearButton(panelToggle);
|
||||
m_target.OnProxiesUpdated += () => UpdateButtonsState(panelToggle);
|
||||
|
||||
UpdateButtonsState(panelToggle);
|
||||
|
||||
buttonContainer.Add(showButton);
|
||||
buttonContainer.Add(hideButton);
|
||||
|
||||
backgroundBox.Add(buttonContainer);
|
||||
|
||||
container.Add(backgroundBox);
|
||||
return container;
|
||||
}
|
||||
|
||||
void SpawnButton(PanelToggleGroup panelToggle)
|
||||
{
|
||||
if(m_target != null)
|
||||
{
|
||||
m_target.SpawnProxy(panelToggle.panelId, panelToggle.slotId);
|
||||
}
|
||||
UpdateButtonsState(panelToggle);
|
||||
}
|
||||
|
||||
void ClearButton(PanelToggleGroup panelToggle)
|
||||
{
|
||||
if(m_target != null)
|
||||
{
|
||||
m_target.ClearProxy(panelToggle.panelId, panelToggle.slotId);
|
||||
}
|
||||
UpdateButtonsState(panelToggle);
|
||||
}
|
||||
|
||||
void UpdateButtonsState(PanelToggleGroup panelToggle)
|
||||
{
|
||||
bool isProxyShowing = m_target.IsProxySlotShowing(panelToggle.panelId, panelToggle.slotId);
|
||||
panelToggle.showButton.SetEnabled(!isProxyShowing);
|
||||
panelToggle.hideButton.SetEnabled(isProxyShowing);
|
||||
}
|
||||
}
|
||||
|
||||
public static class ObjectDispenserEditorButton
|
||||
{
|
||||
public static Button CreateDefaultButton(string buttonText)
|
||||
{
|
||||
var button = new Button
|
||||
{
|
||||
text = buttonText,
|
||||
style =
|
||||
{
|
||||
height = 20.0f,
|
||||
width = 75.0f,
|
||||
}
|
||||
};
|
||||
|
||||
return button;
|
||||
}
|
||||
}
|
||||
|
||||
public struct PanelToggleGroup
|
||||
{
|
||||
public Button showButton;
|
||||
public Button hideButton;
|
||||
public int panelId;
|
||||
public int slotId;
|
||||
|
||||
public PanelToggleGroup(Button showButton, Button hideButton, int panelId, int slotId)
|
||||
{
|
||||
this.showButton = showButton;
|
||||
this.hideButton = hideButton;
|
||||
this.panelId = panelId;
|
||||
this.slotId = slotId;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9a054c1bb9cd10142bfd82ee243710a5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,614 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using Unity.Netcode;
|
||||
using System;
|
||||
using UnityEngine.Events;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor.SceneManagement;
|
||||
#endif
|
||||
|
||||
namespace XRMultiplayer
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a networked object dispenser that can spawn and despawn interactable objects.
|
||||
/// </summary>
|
||||
public class NetworkObjectDispenser : NetworkBehaviour
|
||||
{
|
||||
const int k_NonToggleablePanelId = -1;
|
||||
public Action OnProxiesUpdated;
|
||||
|
||||
/// <summary>
|
||||
/// The button used to clear the current interactables.
|
||||
/// </summary>
|
||||
[SerializeField] UnityEngine.UI.Button m_ClearButton;
|
||||
|
||||
// [SerializeField] bool m_UseCapacity = false;
|
||||
/// <summary>
|
||||
/// The maximum capacity of the dispenser.
|
||||
/// </summary>
|
||||
[SerializeField] int m_Capacity;
|
||||
|
||||
[SerializeField] float m_DistanceCheckTimeInterval = .5f;
|
||||
/// <summary>
|
||||
/// The text component displaying the current capacity.
|
||||
/// </summary>
|
||||
[SerializeField] TMP_Text m_CountText;
|
||||
|
||||
/// <summary>
|
||||
/// The network variable representing the current capacity.
|
||||
/// </summary>
|
||||
NetworkVariable<int> m_CurrentCapacityNetworked = new NetworkVariable<int>(0, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server);
|
||||
|
||||
/// <summary>
|
||||
/// The network variable representing the current panel ID.
|
||||
/// </summary>
|
||||
NetworkVariable<int> m_CurrentPanelIdNetworked = new NetworkVariable<int>(-1, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner);
|
||||
|
||||
/// <summary>
|
||||
/// The list of currently active interactables.
|
||||
/// </summary>
|
||||
List<NetworkBaseInteractable> m_ActiveInteractables = new List<NetworkBaseInteractable>();
|
||||
|
||||
// /// <summary>
|
||||
// /// The panels containing the dispenser slots.
|
||||
// /// </summary>
|
||||
[SerializeField] DispenserPanel[] m_Panels;
|
||||
|
||||
// /// <summary>
|
||||
// /// This panel is persistent and does not switch on or off.
|
||||
// /// /// </summary>
|
||||
[SerializeField] DispenserPanel m_PersistentPanel;
|
||||
|
||||
[SerializeField] Transform m_DefaultSpawnTransform;
|
||||
|
||||
///<inheritdoc/>
|
||||
private void Start()
|
||||
{
|
||||
CheckForProxies();
|
||||
m_CurrentCapacityNetworked.OnValueChanged += UpdateCapacity;
|
||||
}
|
||||
|
||||
///<inheritdoc/>
|
||||
public override void OnDestroy()
|
||||
{
|
||||
base.OnDestroy();
|
||||
m_CurrentCapacityNetworked.OnValueChanged -= UpdateCapacity;
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
void OnValidate()
|
||||
{
|
||||
CheckForProxies(false);
|
||||
|
||||
PrefabStage.prefabStageOpened -= (PrefabStage stage) => CheckForProxies();
|
||||
PrefabStage.prefabStageOpened += (PrefabStage stage) => CheckForProxies();
|
||||
|
||||
for(int i = 0; i < m_Panels.Length; i++)
|
||||
{
|
||||
m_Panels[i].panelId = i;
|
||||
for(int j = 0; j < m_Panels[i].dispenserSlots.Length; j++)
|
||||
{
|
||||
if(m_Panels[i].dispenserSlots[j] == null) continue;
|
||||
m_Panels[i].dispenserSlots[j].panelId = i;
|
||||
m_Panels[i].dispenserSlots[j].slotId = j;
|
||||
}
|
||||
}
|
||||
|
||||
m_PersistentPanel.panelId = k_NonToggleablePanelId;
|
||||
for(int j = 0; j < m_PersistentPanel.dispenserSlots.Length; j++)
|
||||
{
|
||||
if(m_PersistentPanel.dispenserSlots[j] == null) continue;
|
||||
m_PersistentPanel.dispenserSlots[j].panelId = k_NonToggleablePanelId;
|
||||
m_PersistentPanel.dispenserSlots[j].slotId = j;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
IEnumerator ServerSpawnCooldownRoutine()
|
||||
{
|
||||
float deltaTime;
|
||||
while (IsServer)
|
||||
{
|
||||
deltaTime = Time.deltaTime;
|
||||
foreach (DispenserPanel panel in m_Panels)
|
||||
{
|
||||
foreach (DispenserSlot slot in panel.dispenserSlots)
|
||||
{
|
||||
if (!slot.dispenserSlotTransform.gameObject.activeInHierarchy) continue;
|
||||
if (slot.CanSpawn(deltaTime))
|
||||
{
|
||||
AddInteractableToDispenser(panel, slot.slotId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (DispenserSlot slot in m_PersistentPanel.dispenserSlots)
|
||||
{
|
||||
if (!slot.dispenserSlotTransform.gameObject.activeInHierarchy) continue;
|
||||
if (slot.CanSpawn(deltaTime))
|
||||
{
|
||||
AddInteractableToDispenser(m_PersistentPanel, slot.slotId);
|
||||
}
|
||||
}
|
||||
|
||||
yield return new WaitForEndOfFrame();
|
||||
}
|
||||
}
|
||||
IEnumerator ServerDistanceCheckRoutine()
|
||||
{
|
||||
while (IsServer)
|
||||
{
|
||||
foreach (DispenserPanel panel in m_Panels)
|
||||
{
|
||||
foreach (DispenserSlot slot in panel.dispenserSlots)
|
||||
{
|
||||
if (slot.CheckInteractablePosition())
|
||||
{
|
||||
m_ActiveInteractables.Add(slot.currentInteractable);
|
||||
m_CurrentCapacityNetworked.Value = m_ActiveInteractables.Count;
|
||||
slot.currentInteractable = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (DispenserSlot slot in m_PersistentPanel.dispenserSlots)
|
||||
{
|
||||
if (slot.CheckInteractablePosition())
|
||||
{
|
||||
m_ActiveInteractables.Add(slot.currentInteractable);
|
||||
m_CurrentCapacityNetworked.Value = m_ActiveInteractables.Count;
|
||||
slot.currentInteractable = null;
|
||||
}
|
||||
}
|
||||
|
||||
yield return new WaitForSeconds(m_DistanceCheckTimeInterval);
|
||||
}
|
||||
}
|
||||
|
||||
///<inheritdoc/>
|
||||
public override void OnNetworkSpawn()
|
||||
{
|
||||
base.OnNetworkSpawn();
|
||||
|
||||
m_ActiveInteractables.Clear();
|
||||
if (IsServer)
|
||||
{
|
||||
EnablePanel(0);
|
||||
StartCoroutine(ServerDistanceCheckRoutine());
|
||||
StartCoroutine(ServerSpawnCooldownRoutine());
|
||||
m_CurrentCapacityNetworked.Value = 0;
|
||||
}
|
||||
else if (m_CurrentPanelIdNetworked.Value != -1)
|
||||
{
|
||||
EnablePanel(m_CurrentPanelIdNetworked.Value);
|
||||
if (m_CountText != null)
|
||||
m_CountText.text = $"Current Capacity: {m_CurrentCapacityNetworked.Value} / {m_Capacity}";
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnNetworkDespawn()
|
||||
{
|
||||
base.OnNetworkDespawn();
|
||||
StopAllCoroutines();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the capacity UI based on the current capacity value.
|
||||
/// </summary>
|
||||
/// <param name="old">The old capacity value.</param>
|
||||
/// <param name="current">The current capacity value.</param>
|
||||
void UpdateCapacity(int old, int current)
|
||||
{
|
||||
if (m_CountText != null)
|
||||
{
|
||||
m_CountText.text = $"Current Capacity: {m_CurrentCapacityNetworked.Value} / {m_Capacity}";
|
||||
m_CountText.color = m_CurrentCapacityNetworked.Value > m_Capacity ? Color.red : Color.white;
|
||||
}
|
||||
|
||||
if (m_ClearButton != null)
|
||||
m_ClearButton.interactable = m_CurrentCapacityNetworked.Value > 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears the current interactables on the server.
|
||||
/// </summary>
|
||||
public void ClearCurrentInteractables()
|
||||
{
|
||||
if (IsServer)
|
||||
{
|
||||
for (int i = m_ActiveInteractables.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (m_ActiveInteractables[i] != null & !m_ActiveInteractables[i].isInteracting)
|
||||
{
|
||||
m_ActiveInteractables[i].NetworkObject.Despawn();
|
||||
m_ActiveInteractables.Remove(m_ActiveInteractables[i]);
|
||||
}
|
||||
}
|
||||
m_CurrentCapacityNetworked.Value = m_ActiveInteractables.Count;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Picks up an object from a dispenser panel slot.
|
||||
/// </summary>
|
||||
/// <param name="panel">The dispenser panel.</param>
|
||||
/// <param name="slotId">The slot ID.</param>
|
||||
void PickupObject(UnityAction<bool> bindingAction, int panelId, int slotId)
|
||||
{
|
||||
var panel = GetPanelById(panelId);
|
||||
if (!IsServer) { Utils.Log("Trying to Spawn Object from Non-Server Client"); return; }
|
||||
NetworkBaseInteractable netInteractable = panel.dispenserSlots[slotId].currentInteractable;
|
||||
if (netInteractable == null) return;
|
||||
|
||||
netInteractable.OnInteractingChanged.RemoveListener(bindingAction);
|
||||
|
||||
if (netInteractable.TryGetComponent(out Rigidbody rb))
|
||||
{
|
||||
rb.constraints = RigidbodyConstraints.None;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds an interactable to a dispenser panel slot on the server.
|
||||
/// </summary>
|
||||
/// <param name="panel">The dispenser panel.</param>
|
||||
/// <param name="slotId">The slot ID.</param>
|
||||
/// <param name="prefabId">The prefab ID, if default (-1) it will spawn based on the index of the prefab list.</param>
|
||||
void AddInteractableToDispenser(DispenserPanel panel, int slotId)
|
||||
{
|
||||
if (panel.dispenserSlots[slotId].currentInteractable != null)
|
||||
{
|
||||
Utils.Log($"Cannot Spawn. Interactable already exists in Panel {panel.panelId}, slot {panel.dispenserSlots[slotId].slotId}");
|
||||
return;
|
||||
}
|
||||
|
||||
Transform spawnerTransform = panel.dispenserSlots[slotId].dispenserSlotTransform;
|
||||
panel.dispenserSlots[slotId].m_SpawnCooldownTimer = panel.dispenserSlots[slotId].spawnCooldown;
|
||||
NetworkBaseInteractable spawnedInteractable = panel.dispenserSlots[slotId].SpawnInteractablePrefab(spawnerTransform);
|
||||
|
||||
panel.dispenserSlots[slotId].currentInteractable = spawnedInteractable;
|
||||
panel.dispenserSlots[slotId].currentInteractable.NetworkObject.Spawn();
|
||||
|
||||
// Creates a UnityAction<bool> that calls PickupObject with the correct parameters
|
||||
void PickupBinding(bool arg0)
|
||||
{
|
||||
PickupObject(PickupBinding, panel.panelId, slotId);
|
||||
}
|
||||
|
||||
spawnedInteractable.OnInteractingChanged.AddListener(PickupBinding);
|
||||
|
||||
if (panel.dispenserSlots[slotId].freezeOnSpawn && spawnedInteractable.TryGetComponent(out Rigidbody rb))
|
||||
{
|
||||
rb.constraints = RigidbodyConstraints.FreezeAll;
|
||||
}
|
||||
}
|
||||
|
||||
DispenserPanel GetPanelById(int Id)
|
||||
{
|
||||
if (Id == k_NonToggleablePanelId) return m_PersistentPanel;
|
||||
return m_Panels[Id];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears the current panel on the server.
|
||||
/// </summary>
|
||||
void ClearPanel()
|
||||
{
|
||||
StopAllCoroutines();
|
||||
foreach (var slot in m_Panels[m_CurrentPanelIdNetworked.Value].dispenserSlots)
|
||||
{
|
||||
if (slot.currentInteractable != null)
|
||||
{
|
||||
slot.currentInteractable.NetworkObject.Despawn();
|
||||
slot.currentInteractable = null;
|
||||
}
|
||||
}
|
||||
StartCoroutine(ServerDistanceCheckRoutine());
|
||||
StartCoroutine(ServerSpawnCooldownRoutine());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enables a specific panel and adds interactables to its slots on the server.
|
||||
/// </summary>
|
||||
/// <param name="panelId">The panel ID.</param>
|
||||
public void EnablePanel(int panelId)
|
||||
{
|
||||
for (int i = 0; i < m_Panels.Length; i++)
|
||||
{
|
||||
m_Panels[i].panel.SetActive(i == panelId);
|
||||
}
|
||||
if (IsServer)
|
||||
{
|
||||
if (m_CurrentPanelIdNetworked.Value != -1)
|
||||
{
|
||||
ClearPanel();
|
||||
}
|
||||
|
||||
m_CurrentPanelIdNetworked.Value = panelId;
|
||||
}
|
||||
}
|
||||
|
||||
[ContextMenu("Spawn Random Object")]
|
||||
void SpawnRandomObject()
|
||||
{
|
||||
SpawnRandomRpc(m_DefaultSpawnTransform.position, m_DefaultSpawnTransform.rotation);
|
||||
}
|
||||
|
||||
[Rpc(SendTo.Server)]
|
||||
public void SpawnRandomRpc(Vector3 spawnPosition, Quaternion spawnRotation)
|
||||
{
|
||||
var spawnObject = m_PersistentPanel.dispenserSlots[UnityEngine.Random.Range(0, m_PersistentPanel.dispenserSlots.Length)].spawnableInteractablePrefab;
|
||||
if (UnityEngine.Random.value < .85f)
|
||||
{
|
||||
int randomPanel = UnityEngine.Random.Range(0, m_Panels.Length);
|
||||
int randomSlot = UnityEngine.Random.Range(0, m_Panels[randomPanel].dispenserSlots.Length);
|
||||
spawnObject = m_Panels[randomPanel].dispenserSlots[randomSlot].spawnableInteractablePrefab;
|
||||
}
|
||||
|
||||
NetworkPhysicsInteractable spawnedObject = Instantiate(spawnObject.gameObject, spawnPosition, spawnRotation).GetComponent<NetworkPhysicsInteractable>();
|
||||
spawnedObject.spawnLocked = false;
|
||||
spawnedObject.NetworkObject.Spawn();
|
||||
|
||||
m_ActiveInteractables.Add(spawnedObject.GetComponent<NetworkBaseInteractable>());
|
||||
m_CurrentCapacityNetworked.Value = m_ActiveInteractables.Count;
|
||||
}
|
||||
|
||||
[ContextMenu("Clear Proxies")]
|
||||
public void ClearProxies()
|
||||
{
|
||||
foreach (var panel in m_Panels)
|
||||
{
|
||||
foreach (var slot in panel.dispenserSlots)
|
||||
{
|
||||
slot.ClearProxy();
|
||||
}
|
||||
}
|
||||
foreach (var slot in m_PersistentPanel.dispenserSlots)
|
||||
{
|
||||
slot.ClearProxy();
|
||||
}
|
||||
}
|
||||
|
||||
public void ClearProxyPanel(int panelId)
|
||||
{
|
||||
(panelId == k_NonToggleablePanelId ? m_PersistentPanel : m_Panels[panelId]).ClearProxyPanel();
|
||||
OnProxiesUpdated?.Invoke();
|
||||
}
|
||||
|
||||
public void ClearProxy(int panelId, int slotId)
|
||||
{
|
||||
(panelId == k_NonToggleablePanelId ? m_PersistentPanel : m_Panels[panelId]).dispenserSlots[slotId].ClearProxy();
|
||||
OnProxiesUpdated?.Invoke();
|
||||
}
|
||||
|
||||
public void SpawnProxyPanel(int panelId)
|
||||
{
|
||||
if (panelId != k_NonToggleablePanelId)
|
||||
{
|
||||
for (int i = 0; i < m_Panels.Length; i++)
|
||||
{
|
||||
if (panelId != i)
|
||||
{
|
||||
m_Panels[i].ClearProxyPanel();
|
||||
}
|
||||
}
|
||||
}
|
||||
(panelId == k_NonToggleablePanelId ? m_PersistentPanel : m_Panels[panelId]).SpawnProxyPanel();
|
||||
|
||||
OnProxiesUpdated?.Invoke();
|
||||
}
|
||||
|
||||
public void SpawnProxy(int panelId, int slotId)
|
||||
{
|
||||
(panelId == k_NonToggleablePanelId ? m_PersistentPanel : m_Panels[panelId]).dispenserSlots[slotId].SpawnProxy();
|
||||
|
||||
NetworkBaseInteractable interactable = (panelId == k_NonToggleablePanelId ? m_PersistentPanel : m_Panels[panelId]).dispenserSlots[slotId].currentInteractable;
|
||||
OnProxiesUpdated?.Invoke();
|
||||
}
|
||||
|
||||
public bool IsProxyPanelShowing(int panelId)
|
||||
{
|
||||
if (m_Panels.Length == 0) return false;
|
||||
DispenserPanel panel = panelId == k_NonToggleablePanelId ? m_PersistentPanel : m_Panels[panelId];
|
||||
foreach (var slot in panel.dispenserSlots)
|
||||
{
|
||||
if (slot.hasSpawnedProxy) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool IsProxyPanelFull(int panelId)
|
||||
{
|
||||
if (m_Panels.Length == 0) return false;
|
||||
DispenserPanel panel = panelId == k_NonToggleablePanelId ? m_PersistentPanel : m_Panels[panelId];
|
||||
foreach (var slot in panel.dispenserSlots)
|
||||
{
|
||||
if (!slot.hasSpawnedProxy) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool IsProxySlotShowing(int panelId, int slotId)
|
||||
{
|
||||
if (m_Panels.Length == 0) return false;
|
||||
return (panelId == k_NonToggleablePanelId ? m_PersistentPanel : m_Panels[panelId]).dispenserSlots[slotId].hasSpawnedProxy;
|
||||
}
|
||||
|
||||
public void CheckForProxies(bool clearAllProxies = true)
|
||||
{
|
||||
foreach (var panel in m_Panels)
|
||||
{
|
||||
foreach (var slot in panel.dispenserSlots)
|
||||
{
|
||||
if (slot.currentInteractable == null && slot.dispenserSlotTransform != null)
|
||||
{
|
||||
if (slot.dispenserSlotTransform.parent.GetComponentInChildren<NetworkBaseInteractable>() != null)
|
||||
{
|
||||
slot.currentInteractable = slot.dispenserSlotTransform.parent.GetComponentInChildren<NetworkBaseInteractable>();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach (var slot in m_PersistentPanel.dispenserSlots)
|
||||
{
|
||||
if (slot.currentInteractable == null && slot.dispenserSlotTransform != null)
|
||||
{
|
||||
if (slot.dispenserSlotTransform.parent.GetComponentInChildren<NetworkBaseInteractable>() != null)
|
||||
{
|
||||
slot.currentInteractable = slot.dispenserSlotTransform.parent.GetComponentInChildren<NetworkBaseInteractable>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (clearAllProxies)
|
||||
ClearProxies();
|
||||
|
||||
OnProxiesUpdated?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
/// <summary>
|
||||
/// Represents a dispenser panel.
|
||||
/// </summary>
|
||||
public class DispenserPanel
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of physics used by this panel.
|
||||
/// </summary>
|
||||
public string panelName;
|
||||
|
||||
/// <summary>
|
||||
/// The panel game object associated with the object dispenser.
|
||||
/// </summary>
|
||||
public GameObject panel;
|
||||
|
||||
/// <summary>
|
||||
/// The array of dispenser slots used by the object dispenser.
|
||||
/// </summary>
|
||||
[SerializeField] public DispenserSlot[] dispenserSlots;
|
||||
|
||||
public int panelId;
|
||||
|
||||
public void SpawnProxyPanel()
|
||||
{
|
||||
for (int i = 0; i < dispenserSlots.Length; i++)
|
||||
{
|
||||
dispenserSlots[i].SpawnProxy();
|
||||
}
|
||||
}
|
||||
|
||||
public void ClearProxyPanel()
|
||||
{
|
||||
for (int i = 0; i < dispenserSlots.Length; i++)
|
||||
{
|
||||
dispenserSlots[i].ClearProxy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
/// <summary>
|
||||
/// Represents a dispenser slot.
|
||||
/// </summary>
|
||||
public class DispenserSlot
|
||||
{
|
||||
/// <summary>
|
||||
/// The transform of the dispenser slot.
|
||||
/// </summary>
|
||||
public Transform dispenserSlotTransform;
|
||||
|
||||
public NetworkBaseInteractable spawnableInteractablePrefab;
|
||||
|
||||
public NetworkObjectSpawner objectSpawner;
|
||||
public int panelId;
|
||||
public int slotId;
|
||||
|
||||
public bool freezeOnSpawn = true;
|
||||
public float distanceToSpawnNew = .5f;
|
||||
public float spawnCooldown = .5f;
|
||||
|
||||
internal float m_SpawnCooldownTimer = 0f;
|
||||
|
||||
|
||||
[SerializeField] public bool hasSpawnedProxy = false;
|
||||
/// <summary>
|
||||
/// The current network interactable object in the dispenser slot.
|
||||
/// </summary>
|
||||
[SerializeField] public NetworkBaseInteractable currentInteractable;
|
||||
public void ClearProxy()
|
||||
{
|
||||
if (currentInteractable == null) return;
|
||||
if (Application.isPlaying)
|
||||
{
|
||||
UnityEngine.Object.Destroy(currentInteractable.gameObject);
|
||||
}
|
||||
else
|
||||
{
|
||||
UnityEngine.Object.DestroyImmediate(currentInteractable.gameObject);
|
||||
}
|
||||
hasSpawnedProxy = false;
|
||||
}
|
||||
|
||||
public void SpawnProxy()
|
||||
{
|
||||
if (currentInteractable != null) ClearProxy();
|
||||
NetworkBaseInteractable spawnedInteractable = UnityEngine.Object.Instantiate(spawnableInteractablePrefab, dispenserSlotTransform.position, dispenserSlotTransform.rotation);
|
||||
spawnedInteractable.transform.localScale = dispenserSlotTransform.localScale;
|
||||
spawnedInteractable.transform.parent = dispenserSlotTransform.transform;
|
||||
currentInteractable = spawnedInteractable;
|
||||
hasSpawnedProxy = true;
|
||||
}
|
||||
|
||||
public bool CheckInteractablePosition()
|
||||
{
|
||||
if (currentInteractable == null)
|
||||
return false;
|
||||
|
||||
float currentDistance = Vector3.Distance(currentInteractable.transform.position, dispenserSlotTransform.position);
|
||||
if (objectSpawner != null && currentDistance > 0.001f)
|
||||
objectSpawner.OnSpawnDistanceUpdated.Invoke(Mathf.Clamp01(currentDistance / distanceToSpawnNew));
|
||||
|
||||
return currentDistance > distanceToSpawnNew;
|
||||
}
|
||||
|
||||
public bool CanSpawn(float deltaTime)
|
||||
{
|
||||
if (currentInteractable != null) return false;
|
||||
if (m_SpawnCooldownTimer > 0)
|
||||
{
|
||||
UpdateCooldown(m_SpawnCooldownTimer - deltaTime);
|
||||
return false;
|
||||
}
|
||||
|
||||
UpdateCooldown(spawnCooldown);
|
||||
return true;
|
||||
}
|
||||
|
||||
void UpdateCooldown(float newTime)
|
||||
{
|
||||
m_SpawnCooldownTimer = newTime;
|
||||
if (objectSpawner != null)
|
||||
objectSpawner.OnSpawnCooldownUpdated.Invoke(Mathf.Clamp01(1 - (m_SpawnCooldownTimer / spawnCooldown)));
|
||||
}
|
||||
|
||||
public NetworkBaseInteractable SpawnInteractablePrefab(Transform spawnerTransform)
|
||||
{
|
||||
UpdateCooldown(spawnCooldown);
|
||||
NetworkBaseInteractable spawnedInteractable = UnityEngine.Object.Instantiate
|
||||
(
|
||||
spawnableInteractablePrefab,
|
||||
spawnerTransform.position,
|
||||
spawnerTransform.rotation
|
||||
);
|
||||
spawnedInteractable.transform.localScale = spawnerTransform.localScale;
|
||||
if (objectSpawner != null)
|
||||
objectSpawner.OnObjectSpawned.Invoke();
|
||||
|
||||
return spawnedInteractable;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1fc88142560a94e45b25b25e04ed4b0b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,31 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
|
||||
namespace XRMultiplayer
|
||||
{
|
||||
public class NetworkObjectSpawner : MonoBehaviour
|
||||
{
|
||||
public UnityEvent<float> OnSpawnDistanceUpdated;
|
||||
public UnityEvent<float> OnSpawnCooldownUpdated;
|
||||
public UnityEvent OnObjectSpawned;
|
||||
|
||||
public Renderer fadeRenderer;
|
||||
|
||||
Vector2 minMax = new Vector2(-1.0f, -0.25f);
|
||||
|
||||
public void UpdateDistance(float distance)
|
||||
{
|
||||
fadeRenderer.material.SetFloat("_FadeOffset", Mathf.Lerp(minMax.x, minMax.y, distance));
|
||||
if (distance >= 1.0f)
|
||||
{
|
||||
fadeRenderer.gameObject.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
public void ResetDistance()
|
||||
{
|
||||
fadeRenderer.material.SetFloat("_FadeOffset", minMax.x);
|
||||
fadeRenderer.gameObject.SetActive(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4f7b90925e8011b46b35eebd00c5e38d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,425 @@
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using Unity.Netcode;
|
||||
|
||||
namespace XRMultiplayer
|
||||
{
|
||||
/// <summary>
|
||||
/// Controls the music playback in a room.
|
||||
/// </summary>
|
||||
public class RoomMusic : NetworkBehaviour
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicates whether the music should start playing automatically.
|
||||
/// </summary>
|
||||
[SerializeField] bool m_AutoPlay;
|
||||
|
||||
/// <summary>
|
||||
/// The list of music clips to play.
|
||||
/// </summary>
|
||||
[SerializeField, Tooltip("Format clip name to 'Song Title-Artist Name'")] AudioClip[] m_MusicClips;
|
||||
|
||||
/// <summary>
|
||||
/// The audio source to play the music.
|
||||
/// </summary>
|
||||
[SerializeField] AudioSource m_AudioSource;
|
||||
|
||||
/// <summary>
|
||||
/// The slider to control the timeline of the current playing track.
|
||||
/// </summary>
|
||||
[SerializeField] Slider m_TimelineSlider;
|
||||
|
||||
/// <summary>
|
||||
/// The slider to control the volume of the current playing track.
|
||||
/// </summary>
|
||||
[SerializeField] Slider m_VolumeSlider;
|
||||
|
||||
/// <summary>
|
||||
/// The dropdown to select the current track to play.
|
||||
/// </summary>
|
||||
[SerializeField] TMP_Dropdown m_Dropdown;
|
||||
|
||||
/// <summary>
|
||||
/// The text to display the current playing track.
|
||||
/// </summary>
|
||||
[SerializeField] TMP_Text[] m_CurrentSongText;
|
||||
|
||||
/// <summary>
|
||||
/// The toggle to play/pause the current track.
|
||||
/// </summary>
|
||||
[SerializeField] Toggle m_PlayPauseToggle;
|
||||
|
||||
/// <summary>
|
||||
/// The toggle to shuffle the music.
|
||||
/// </summary>
|
||||
[SerializeField] Toggle m_ShuffleToggle;
|
||||
|
||||
/// <summary>
|
||||
/// The button to play the next track.
|
||||
/// </summary>
|
||||
[SerializeField] Button m_NextButton;
|
||||
|
||||
/// <summary>
|
||||
/// The button to play the previous track.
|
||||
/// </summary>
|
||||
[SerializeField] Button m_PreviousButton;
|
||||
|
||||
/// <summary>
|
||||
/// The image to display the play/pause state.
|
||||
/// </summary>
|
||||
[SerializeField] Image m_PlayPauseImage;
|
||||
|
||||
/// <summary>
|
||||
/// The sprite to display the pause state.
|
||||
/// </summary>
|
||||
[SerializeField] Sprite m_PauseSprite;
|
||||
|
||||
/// <summary>
|
||||
/// The sprite to display the play state.
|
||||
/// </summary>
|
||||
[SerializeField] Sprite m_PlaySprite;
|
||||
|
||||
/// <summary>
|
||||
/// The network variable to store the current song ID.
|
||||
/// </summary>
|
||||
readonly NetworkVariable<int> m_CurrentSongIdNetworked = new NetworkVariable<int>(0, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server);
|
||||
|
||||
/// <summary>
|
||||
/// The network variable to store the state of whether or not we are actively playing music.
|
||||
/// </summary>
|
||||
readonly NetworkVariable<bool> m_IsPlayingNetworked = new NetworkVariable<bool>(false, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server);
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether the music should be shuffled.
|
||||
/// </summary>
|
||||
bool m_Shuffle;
|
||||
|
||||
/// <summary>
|
||||
/// The current song ID.
|
||||
/// </summary>
|
||||
int currentSongId = 0;
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
void Start()
|
||||
{
|
||||
// Clear the dropdown options
|
||||
m_Dropdown.ClearOptions();
|
||||
|
||||
if (m_MusicClips == null || m_MusicClips.Length == 0)
|
||||
{
|
||||
Utils.LogWarning("No music clips found in RoomMusic script. Please add some music clips to the RoomMusic script.");
|
||||
enabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Add the music clip names as options to the dropdown
|
||||
foreach (var c in m_MusicClips)
|
||||
{
|
||||
TMP_Dropdown.OptionData optionData = new TMP_Dropdown.OptionData(c.name);
|
||||
m_Dropdown.options.Add(optionData);
|
||||
}
|
||||
|
||||
// If auto play is enabled, pick a random song
|
||||
if (m_AutoPlay)
|
||||
{
|
||||
PickRandomSong();
|
||||
}
|
||||
|
||||
// Set the initial volume and clip for the audio source
|
||||
m_AudioSource.volume = m_VolumeSlider.value;
|
||||
m_AudioSource.clip = m_MusicClips[Random.Range(0, m_MusicClips.Length)];
|
||||
|
||||
SetupUIListeners();
|
||||
|
||||
// Update the song title text
|
||||
UpdateSongTitleText();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void OnNetworkSpawn()
|
||||
{
|
||||
base.OnNetworkSpawn();
|
||||
|
||||
// Set the volume of the audio source
|
||||
m_AudioSource.volume = m_VolumeSlider.value;
|
||||
|
||||
// Add listeners for the network variable change events
|
||||
m_CurrentSongIdNetworked.OnValueChanged += CurrentSongUpdated;
|
||||
m_IsPlayingNetworked.OnValueChanged += OnIsPlayingChanged;
|
||||
|
||||
if (IsServer)
|
||||
{
|
||||
m_IsPlayingNetworked.Value = m_AutoPlay;
|
||||
if (!m_AutoPlay)
|
||||
{
|
||||
m_CurrentSongIdNetworked.Value = 0;
|
||||
SetClipTime(0.0f);
|
||||
m_TimelineSlider.SetValueWithoutNotify(0.0f);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (m_IsPlayingNetworked.Value)
|
||||
{
|
||||
SetSong(m_CurrentSongIdNetworked.Value);
|
||||
GetCurrentSongPercFromServerRpc();
|
||||
}
|
||||
OnIsPlayingChanged(false, m_IsPlayingNetworked.Value);
|
||||
}
|
||||
CurrentSongUpdated(0, m_CurrentSongIdNetworked.Value);
|
||||
}
|
||||
|
||||
public override void OnNetworkDespawn()
|
||||
{
|
||||
base.OnNetworkDespawn();
|
||||
|
||||
OnIsPlayingChanged(false, false);
|
||||
|
||||
// Remove listeners for the network variable change events
|
||||
m_CurrentSongIdNetworked.OnValueChanged -= CurrentSongUpdated;
|
||||
m_IsPlayingNetworked.OnValueChanged -= OnIsPlayingChanged;
|
||||
|
||||
|
||||
SetClipTime(0.0f);
|
||||
m_TimelineSlider.SetValueWithoutNotify(0.0f);
|
||||
|
||||
if (IsServer)
|
||||
{
|
||||
if (m_CurrentSongIdNetworked != null)
|
||||
m_CurrentSongIdNetworked.Value = 0;
|
||||
if (m_IsPlayingNetworked != null)
|
||||
m_IsPlayingNetworked.Value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
private void Update()
|
||||
{
|
||||
if (!m_IsPlayingNetworked.Value || !NetworkManager.Singleton.IsConnectedClient)
|
||||
return;
|
||||
|
||||
// Calculate the current playback percentage
|
||||
float perc = m_AudioSource.time / m_AudioSource.clip.length;
|
||||
m_TimelineSlider.SetValueWithoutNotify(perc);
|
||||
|
||||
// If the playback is near the end, pick a new song
|
||||
if (perc >= .999f)
|
||||
{
|
||||
PickNewSong();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void OnDestroy()
|
||||
{
|
||||
base.OnDestroy();
|
||||
RemoveUIListeners();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets up the listeners for UI events.
|
||||
/// </summary>
|
||||
void SetupUIListeners()
|
||||
{
|
||||
m_TimelineSlider.onValueChanged.AddListener(SetClipTime);
|
||||
m_VolumeSlider.onValueChanged.AddListener(UpdateVolume);
|
||||
m_Dropdown.onValueChanged.AddListener(PickSong);
|
||||
m_PlayPauseToggle.onValueChanged.AddListener(TogglePlay);
|
||||
m_ShuffleToggle.onValueChanged.AddListener(ToggleShuffle);
|
||||
|
||||
m_NextButton.onClick.AddListener(delegate { PickNewSong(1); });
|
||||
m_PreviousButton.onClick.AddListener(delegate { PickNewSong(-1); });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the listeners for UI events.
|
||||
/// </summary>
|
||||
void RemoveUIListeners()
|
||||
{
|
||||
m_TimelineSlider.onValueChanged.RemoveListener(SetClipTime);
|
||||
m_VolumeSlider.onValueChanged.RemoveListener(UpdateVolume);
|
||||
m_Dropdown.onValueChanged.RemoveListener(PickSong);
|
||||
m_PlayPauseToggle.onValueChanged.RemoveListener(TogglePlay);
|
||||
m_ShuffleToggle.onValueChanged.RemoveListener(ToggleShuffle);
|
||||
|
||||
m_NextButton.onClick.RemoveListener(delegate { PickNewSong(1); });
|
||||
m_PreviousButton.onClick.RemoveListener(delegate { PickNewSong(-1); });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the volume of the audio source.
|
||||
/// </summary>
|
||||
/// <param name="volume">The new volume value.</param>
|
||||
void UpdateVolume(float volume)
|
||||
{
|
||||
m_AudioSource.volume = volume;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Toggles the shuffle mode.
|
||||
/// </summary>
|
||||
/// <param name="toggle">The new toggle state.</param>
|
||||
void ToggleShuffle(bool toggle)
|
||||
{
|
||||
m_Shuffle = toggle;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Toggles the play/pause state of the audio source.
|
||||
/// </summary>
|
||||
/// <param name="toggle">The new toggle state.</param>
|
||||
void TogglePlay(bool toggle)
|
||||
{
|
||||
if (IsServer)
|
||||
{
|
||||
m_IsPlayingNetworked.Value = toggle;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Picks a new song based on the shuffle mode and direction.
|
||||
/// </summary>
|
||||
/// <param name="dir">The direction to pick the new song (-1 for previous, 1 for next).</param>
|
||||
void PickNewSong(int dir = 1)
|
||||
{
|
||||
if (!IsServer)
|
||||
return;
|
||||
|
||||
if (m_Shuffle)
|
||||
{
|
||||
PickRandomSong();
|
||||
}
|
||||
else
|
||||
{
|
||||
int nextSongId = Utils.RealMod(m_Dropdown.value + dir, m_MusicClips.Length);
|
||||
PickSong(nextSongId);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Picks a random song to play.
|
||||
/// </summary>
|
||||
void PickRandomSong()
|
||||
{
|
||||
int tries = 10;
|
||||
int randomSongId = m_CurrentSongIdNetworked.Value;
|
||||
while (tries > 0)
|
||||
{
|
||||
tries--;
|
||||
randomSongId = Random.Range(0, m_MusicClips.Length);
|
||||
|
||||
if (randomSongId != m_CurrentSongIdNetworked.Value)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
PickSong(randomSongId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Picks a song to play based on the selected dropdown option.
|
||||
/// </summary>
|
||||
/// <param name="songId">The ID of the song to play.</param>
|
||||
void PickSong(int songId)
|
||||
{
|
||||
m_Dropdown.value = songId;
|
||||
SetClipTime(0.0f);
|
||||
if (IsServer)
|
||||
{
|
||||
m_CurrentSongIdNetworked.Value = songId;
|
||||
m_IsPlayingNetworked.Value = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plays a song based on the given song ID.
|
||||
/// </summary>
|
||||
/// <param name="oldSongId">The ID of the previous song.</param>
|
||||
/// <param name="songId">The ID of the new song.</param>
|
||||
void CurrentSongUpdated(int oldSongId, int songId)
|
||||
{
|
||||
if (songId >= 0 && songId < m_MusicClips.Length)
|
||||
{
|
||||
SetSong(songId);
|
||||
|
||||
if (m_IsPlayingNetworked.Value)
|
||||
{
|
||||
SetClipTime(0.0f);
|
||||
m_AudioSource.Play();
|
||||
m_PlayPauseImage.sprite = m_PauseSprite;
|
||||
|
||||
m_PlayPauseToggle.SetIsOnWithoutNotify(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SetSong(int songId)
|
||||
{
|
||||
m_AudioSource.clip = m_MusicClips[songId];
|
||||
currentSongId = songId;
|
||||
UpdateSongTitleText();
|
||||
}
|
||||
|
||||
void OnIsPlayingChanged(bool oldValue, bool newValue)
|
||||
{
|
||||
if (newValue)
|
||||
{
|
||||
m_AudioSource.Play();
|
||||
m_PlayPauseImage.sprite = m_PauseSprite;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_AudioSource.Pause();
|
||||
m_PlayPauseImage.sprite = m_PlaySprite;
|
||||
}
|
||||
|
||||
m_PlayPauseToggle.SetIsOnWithoutNotify(newValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the clip time of the audio source based on the timeline slider value.
|
||||
/// </summary>
|
||||
/// <param name="value">The new value of the timeline slider.</param>
|
||||
void SetClipTime(float value)
|
||||
{
|
||||
if (!enabled)
|
||||
return;
|
||||
|
||||
m_AudioSource.time = Mathf.Clamp(value, .01f, .99f) * m_AudioSource.clip.length;
|
||||
}
|
||||
|
||||
[Rpc(SendTo.Server)]
|
||||
void GetCurrentSongPercFromServerRpc(RpcParams rpcParams = default)
|
||||
{
|
||||
SendCurrentSongPercToClientRpc(m_AudioSource.time / m_AudioSource.clip.length, RpcTarget.Single(rpcParams.Receive.SenderClientId, RpcTargetUse.Temp));
|
||||
}
|
||||
|
||||
[Rpc(SendTo.SpecifiedInParams)]
|
||||
void SendCurrentSongPercToClientRpc(float perc, RpcParams rpcParams = default)
|
||||
{
|
||||
m_AudioSource.time = perc * m_AudioSource.clip.length;
|
||||
m_TimelineSlider.SetValueWithoutNotify(perc / m_AudioSource.clip.length);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the song title text.
|
||||
/// </summary>
|
||||
void UpdateSongTitleText()
|
||||
{
|
||||
string songName = m_MusicClips[currentSongId == -1 ? 0 : currentSongId].name;
|
||||
string[] songNameSplit = m_MusicClips[currentSongId == -1 ? 0 : currentSongId].name.Split('-');
|
||||
if (songNameSplit.Length > 1)
|
||||
{
|
||||
songName = $"<b>{songNameSplit[0]}</b>\n{songNameSplit[1]}";
|
||||
}
|
||||
|
||||
foreach (TMP_Text text in m_CurrentSongText)
|
||||
{
|
||||
text.text = songName;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 39d12819270ed1848bd16ba90bc2b2ad
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6472f94daff69584c81dc332057da384
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,130 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
namespace XRMultiplayer
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a projectile in the game.
|
||||
/// </summary>
|
||||
public class Projectile : MonoBehaviour
|
||||
{
|
||||
/// <summary>
|
||||
/// The trail renderer for the projectile.
|
||||
/// </summary>
|
||||
[SerializeField] protected TrailRenderer m_TrailRenderer;
|
||||
|
||||
[SerializeField] protected float m_Lifetime = 10.0f;
|
||||
|
||||
/// <summary>
|
||||
/// The previous position of the projectile.
|
||||
/// </summary>
|
||||
Vector3 m_PrevPos = Vector3.zero;
|
||||
|
||||
/// <summary>
|
||||
/// The raycast hit for the projectile.
|
||||
/// </summary>
|
||||
RaycastHit m_Hit;
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether the projectile has hit a target.
|
||||
/// </summary>
|
||||
bool m_HasHitTarget = false;
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether the projectile belongs to the local player.
|
||||
/// </summary>
|
||||
bool m_LocalPlayerProjectile;
|
||||
|
||||
Action<Projectile> m_OnReturnToPool;
|
||||
|
||||
Rigidbody m_Rigidybody;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Sets up the projectile with the specified parameters.
|
||||
/// </summary>
|
||||
/// <param name="localPlayer">Indicates whether the projectile belongs to the local player.</param>
|
||||
/// <param name="playerColor">The color of the player.</param>
|
||||
public void Setup(bool localPlayer, Color playerColor, Action<Projectile> returnToPoolAction = null)
|
||||
{
|
||||
if (m_Rigidybody == null)
|
||||
{
|
||||
TryGetComponent(out m_Rigidybody);
|
||||
}
|
||||
|
||||
m_LocalPlayerProjectile = localPlayer;
|
||||
m_TrailRenderer.startColor = playerColor;
|
||||
m_TrailRenderer.endColor = playerColor;
|
||||
m_TrailRenderer.Clear();
|
||||
m_PrevPos = transform.position;
|
||||
if (returnToPoolAction != null)
|
||||
{
|
||||
m_OnReturnToPool = returnToPoolAction;
|
||||
StartCoroutine(ResetProjectileAfterTime());
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerator ResetProjectileAfterTime()
|
||||
{
|
||||
yield return new WaitForSeconds(m_Lifetime);
|
||||
ResetProjectile();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
private void FixedUpdate()
|
||||
{
|
||||
if (!m_LocalPlayerProjectile || m_HasHitTarget) return;
|
||||
if (Physics.Linecast(m_PrevPos, transform.position, out m_Hit))
|
||||
{
|
||||
if (m_Hit.transform.CompareTag("Target"))
|
||||
{
|
||||
HitTarget(m_Hit.transform.GetComponentInParent<Target>());
|
||||
}
|
||||
|
||||
CheckForInteractableHit(m_Hit.transform);
|
||||
}
|
||||
|
||||
m_PrevPos = transform.position;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
void OnTriggerEnter(Collider other)
|
||||
{
|
||||
if (other.CompareTag("Target"))
|
||||
{
|
||||
HitTarget(other.GetComponentInParent<Target>());
|
||||
}
|
||||
}
|
||||
|
||||
void OnCollisionEnter(Collision collision)
|
||||
{
|
||||
if (!m_LocalPlayerProjectile) return;
|
||||
CheckForInteractableHit(collision.transform);
|
||||
}
|
||||
|
||||
void CheckForInteractableHit(Transform t)
|
||||
{
|
||||
NetworkPhysicsInteractable networkPhysicsInteractable = t.GetComponentInParent<NetworkPhysicsInteractable>();
|
||||
if (networkPhysicsInteractable != null)
|
||||
{
|
||||
networkPhysicsInteractable.RequestOwnership();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the projectile hits a target.
|
||||
/// </summary>
|
||||
/// <param name="target">The target that was hit.</param>
|
||||
protected virtual void HitTarget(Target target)
|
||||
{
|
||||
target.TargetHitLocal();
|
||||
m_HasHitTarget = true;
|
||||
}
|
||||
|
||||
public void ResetProjectile()
|
||||
{
|
||||
StopAllCoroutines();
|
||||
m_OnReturnToPool?.Invoke(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d181f0b4266f1f94cb661cf45595c347
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,109 @@
|
||||
using System.Collections;
|
||||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
|
||||
namespace XRMultiplayer
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a target object in the target practice gameplay.
|
||||
/// </summary>
|
||||
public class Target : MonoBehaviour
|
||||
{
|
||||
/// <summary>
|
||||
/// The target value.
|
||||
/// </summary>
|
||||
public int targetValue = 1;
|
||||
|
||||
/// <summary>
|
||||
/// The animator for the target.
|
||||
/// </summary>
|
||||
[SerializeField] Animator m_Animator;
|
||||
|
||||
/// <summary>
|
||||
/// The collider for the target.
|
||||
/// </summary>
|
||||
[SerializeField] Collider m_TriggerCollider;
|
||||
|
||||
/// <summary>
|
||||
/// The particle system for the target.
|
||||
/// </summary>
|
||||
[SerializeField] ParticleSystem m_Particles;
|
||||
|
||||
/// <summary>
|
||||
/// The target manager for the target.
|
||||
/// </summary>
|
||||
TargetManager m_TargetManager;
|
||||
|
||||
/// <inheritdoc/>
|
||||
void Start()
|
||||
{
|
||||
m_TargetManager = GetComponentInParent<TargetManager>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enables the target object.
|
||||
/// </summary>
|
||||
public void EnableTarget()
|
||||
{
|
||||
m_Animator.SetTrigger("Activate");
|
||||
m_TriggerCollider.enabled = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the local target hit event.
|
||||
/// </summary>
|
||||
public void TargetHitLocal()
|
||||
{
|
||||
m_TargetManager.HitTargetServerRpc(NetworkManager.Singleton.LocalClientId, XRINetworkGameManager.LocalPlayerColor.Value);
|
||||
PlayHitEffects(XRINetworkGameManager.LocalPlayerColor.Value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the network target hit event.
|
||||
/// </summary>
|
||||
/// <param name="clientId">The ID of the client that hit the target.</param>
|
||||
/// <param name="playerColor">The color of the player who hit the target.</param>
|
||||
public void TargetHitNetwork(ulong clientId, Color playerColor)
|
||||
{
|
||||
if (NetworkManager.Singleton.LocalClientId != clientId)
|
||||
{
|
||||
PlayHitEffects(playerColor);
|
||||
}
|
||||
|
||||
if (m_TargetManager.IsServer)
|
||||
{
|
||||
StartCoroutine(TargetHitSequence());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plays the hit effects for the target.
|
||||
/// </summary>
|
||||
/// <param name="playerColor"></param>
|
||||
void PlayHitEffects(Color playerColor)
|
||||
{
|
||||
m_Animator.SetTrigger("Hit");
|
||||
m_TriggerCollider.enabled = false;
|
||||
var main = m_Particles.main;
|
||||
main.startColor = playerColor;
|
||||
m_Particles.Play();
|
||||
StartCoroutine(HideAfterHit());
|
||||
}
|
||||
|
||||
IEnumerator HideAfterHit()
|
||||
{
|
||||
yield return new WaitForSeconds(3.0f);
|
||||
gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the target hit sequence.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
IEnumerator TargetHitSequence()
|
||||
{
|
||||
yield return new WaitForSeconds(2.25f);
|
||||
m_TargetManager.ServerIncreaseDifficulty();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a272dbc7287bfa24bab3cc2e39c3a962
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,137 @@
|
||||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
|
||||
namespace XRMultiplayer
|
||||
{
|
||||
/// <summary>
|
||||
/// Manages the targets in the target practice game.
|
||||
/// </summary>
|
||||
public class TargetManager : NetworkBehaviour
|
||||
{
|
||||
/// <summary>
|
||||
/// The targets in the game.
|
||||
/// </summary>
|
||||
public Target[] targets;
|
||||
|
||||
/// <summary>
|
||||
/// The difficulty level of the game.
|
||||
/// </summary>
|
||||
protected NetworkVariable<int> m_DifficultyLevel = new NetworkVariable<int>(-1, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server);
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether the targets are activated.
|
||||
/// </summary>
|
||||
protected bool m_activated = false;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual void Start()
|
||||
{
|
||||
// Subscribe to difficulty level change events
|
||||
m_DifficultyLevel.OnValueChanged += OnDifficultyChanged;
|
||||
|
||||
// Deactivate all targets
|
||||
for (int i = 0; i < targets.Length; i++)
|
||||
{
|
||||
targets[i].gameObject.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void OnNetworkSpawn()
|
||||
{
|
||||
base.OnNetworkSpawn();
|
||||
|
||||
// If the difficulty level is already set, manually trigger the difficulty change event
|
||||
if (m_DifficultyLevel.Value != -1)
|
||||
{
|
||||
OnDifficultyChanged(-1, m_DifficultyLevel.Value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Activates the targets.
|
||||
/// </summary>
|
||||
public void ActivateTargets()
|
||||
{
|
||||
m_activated = true;
|
||||
|
||||
// Set the difficulty level to 0 on the server
|
||||
if (IsServer)
|
||||
{
|
||||
m_DifficultyLevel.Value = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deactivates the targets.
|
||||
/// </summary>
|
||||
public void DeactivateTargets()
|
||||
{
|
||||
m_activated = false;
|
||||
|
||||
// Set the difficulty level to -1 on the server
|
||||
if (IsServer)
|
||||
{
|
||||
m_DifficultyLevel.Value = -1;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the difficulty level changes.
|
||||
/// </summary>
|
||||
/// <param name="old">The old difficulty level.</param>
|
||||
/// <param name="current">The current difficulty level.</param>
|
||||
void OnDifficultyChanged(int old, int current)
|
||||
{
|
||||
// Activate the target corresponding to the current difficulty level and enable it
|
||||
for (int i = 0; i < targets.Length; i++)
|
||||
{
|
||||
targets[i].gameObject.SetActive(current == i);
|
||||
if (current == i)
|
||||
{
|
||||
targets[i].EnableTarget();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the difficulty level on the server.
|
||||
/// </summary>
|
||||
/// <param name="newDifficulty">The new difficulty level.</param>
|
||||
public void ServerSetDifficulty(int newDifficulty)
|
||||
{
|
||||
m_DifficultyLevel.Value = newDifficulty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Increases the difficulty level on the server.
|
||||
/// </summary>
|
||||
public void ServerIncreaseDifficulty()
|
||||
{
|
||||
m_DifficultyLevel.Value = (m_DifficultyLevel.Value + 1) % targets.Length;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Server RPC method called when a target is hit.
|
||||
/// </summary>
|
||||
/// <param name="clientId">The client ID of the player who hit the target.</param>
|
||||
/// <param name="playerColor">The color of the player who hit the target.</param>
|
||||
[ServerRpc(RequireOwnership = false)]
|
||||
public void HitTargetServerRpc(ulong clientId, Color playerColor)
|
||||
{
|
||||
HitTargetClientRpc(clientId, playerColor);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Client RPC method called when a target is hit.
|
||||
/// </summary>
|
||||
/// <param name="clientId">The client ID of the player who hit the target.</param>
|
||||
/// <param name="playerColor">The color of the player who hit the target.</param>
|
||||
[ClientRpc]
|
||||
public void HitTargetClientRpc(ulong clientId, Color playerColor)
|
||||
{
|
||||
// Call the TargetHitNetwork method on the target corresponding to the current difficulty level
|
||||
targets[Mathf.Clamp(m_DifficultyLevel.Value, 0, targets.Length - 1)].TargetHitNetwork(clientId, playerColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d68a6a3d50c927749b49fe7bb42b655a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c692a426548c4f34293f0dcd409b09b4
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,109 @@
|
||||
using Unity.Mathematics;
|
||||
using UnityEngine;
|
||||
using UnityEngine.XR.Interaction.Toolkit.Utilities.Tweenables.Primitives;
|
||||
|
||||
namespace XRMultiplayer
|
||||
{
|
||||
/// <summary>
|
||||
/// Helper script used to control the Teleport Anchor visuals animations.
|
||||
/// </summary>
|
||||
public class AnchorVisuals : MonoBehaviour
|
||||
{
|
||||
[SerializeField, Tooltip("The animation for the vertical glow element on the platform.")]
|
||||
Animation m_FadeAnimation;
|
||||
|
||||
[SerializeField, Tooltip("The arrow transform, at the center of the platform.")]
|
||||
Transform m_Arrow;
|
||||
|
||||
[SerializeField, Tooltip("Height of the arrow transform when teleport ray hovers the teleport pad.")]
|
||||
float m_TargetArrowHeight = 1.0f;
|
||||
|
||||
[SerializeField, Tooltip("Animation duration of the arrow transform to and from the target arrow height.")]
|
||||
float m_ArrowAnimationDuration = 0.2f;
|
||||
|
||||
[SerializeField, Tooltip("Animation curve of hte arrow transform to and from the target arrow height.")]
|
||||
AnimationCurve m_AnimationCurve;
|
||||
|
||||
Coroutine m_ArrowCoroutine;
|
||||
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
Vector3TweenableVariable m_ArrowHeight;
|
||||
|
||||
Vector3 m_InitialArrowScale;
|
||||
|
||||
void Start()
|
||||
{
|
||||
if (m_FadeAnimation != null)
|
||||
{
|
||||
var fadeAnim = m_FadeAnimation;
|
||||
var clipName = m_FadeAnimation.clip.name;
|
||||
fadeAnim[clipName].normalizedTime = 1f;
|
||||
}
|
||||
|
||||
m_ArrowHeight = new Vector3TweenableVariable
|
||||
{
|
||||
animationCurve = m_AnimationCurve
|
||||
};
|
||||
m_InitialArrowScale = m_Arrow.localScale;
|
||||
}
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
|
||||
void Update()
|
||||
{
|
||||
m_Arrow.localPosition = m_ArrowHeight.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs animations when teleport interactor enters the teleport anchor selection.
|
||||
/// </summary>
|
||||
public void OnAnchorEnter()
|
||||
{
|
||||
m_Arrow.localScale = m_InitialArrowScale;
|
||||
|
||||
if (m_FadeAnimation != null)
|
||||
{
|
||||
var fadeAnim = m_FadeAnimation;
|
||||
var clipName = m_FadeAnimation.clip.name;
|
||||
fadeAnim[clipName].normalizedTime = 0f;
|
||||
fadeAnim[clipName].speed = 1f;
|
||||
fadeAnim.Play();
|
||||
}
|
||||
|
||||
if (m_ArrowCoroutine != null)
|
||||
StopCoroutine(m_ArrowCoroutine);
|
||||
|
||||
var arrowPosition = m_Arrow.localPosition;
|
||||
m_ArrowCoroutine = StartCoroutine(m_ArrowHeight.PlaySequence(arrowPosition, new float3(arrowPosition.x, m_TargetArrowHeight, arrowPosition.z), m_ArrowAnimationDuration));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs animations when teleport interactor exits the teleport anchor selection.
|
||||
/// </summary>
|
||||
public void OnAnchorExit()
|
||||
{
|
||||
if (m_FadeAnimation != null)
|
||||
{
|
||||
// Set time to 1, at the end of the animation, play at 1.5x speed
|
||||
var fadeAnim = m_FadeAnimation;
|
||||
var clipName = m_FadeAnimation.clip.name;
|
||||
fadeAnim[clipName].normalizedTime = 1f;
|
||||
fadeAnim[clipName].speed = -1.5f;
|
||||
fadeAnim.Play();
|
||||
}
|
||||
|
||||
if (m_ArrowCoroutine != null)
|
||||
StopCoroutine(m_ArrowCoroutine);
|
||||
|
||||
var arrowPosition = m_Arrow.localPosition;
|
||||
m_ArrowCoroutine = StartCoroutine(m_ArrowHeight.PlaySequence(arrowPosition, new float3(arrowPosition.x, 0, arrowPosition.z), m_ArrowAnimationDuration));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hides the arrow visual when teleporting
|
||||
/// </summary>
|
||||
public void HideArrowOnTeleport()
|
||||
{
|
||||
m_Arrow.localScale = Vector3.zero;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 31b457110c38f45909b40fd3abb1af16
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,36 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace XRMultiplayer
|
||||
{
|
||||
public class Billboard : MonoBehaviour
|
||||
{
|
||||
[SerializeField] bool m_WorldUp;
|
||||
[SerializeField] bool m_FlipForward;
|
||||
|
||||
protected Camera m_Camera;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
m_Camera = Camera.main;
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
Quaternion lookRot = Quaternion.LookRotation(m_Camera.transform.position - transform.position);
|
||||
|
||||
if (m_WorldUp)
|
||||
{
|
||||
Vector3 offset = lookRot.eulerAngles;
|
||||
offset.x = 0;
|
||||
offset.z = 0;
|
||||
|
||||
if (m_FlipForward)
|
||||
offset.y += 180;
|
||||
|
||||
lookRot = Quaternion.Euler(offset);
|
||||
}
|
||||
|
||||
transform.rotation = lookRot;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8f4f9a0c05e79af428c8c2ac20edde54
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,91 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace Unity.VRTemplate
|
||||
{
|
||||
/// <summary>
|
||||
/// Controls the visual states of a boolean toggle switch UI
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(Toggle))]
|
||||
public class BooleanToggleVisualsController : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler
|
||||
{
|
||||
const float k_TargetPositionX = 17f;
|
||||
|
||||
#pragma warning disable 649
|
||||
[SerializeField, Tooltip("The boolean toggle knob.")]
|
||||
RectTransform m_Knob;
|
||||
|
||||
[SerializeField, Tooltip("How much to translate the button imagery on the z on hover.")]
|
||||
float m_ZTranslation = 5f;
|
||||
#pragma warning restore 649
|
||||
|
||||
Toggle m_Toggle;
|
||||
float m_InitialBackground;
|
||||
Coroutine m_ColorFade;
|
||||
Coroutine m_LocalMove;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
m_Toggle = gameObject.GetComponent<Toggle>();
|
||||
|
||||
//Add listener for when the state of the Toggle changes, to take action
|
||||
m_Toggle.onValueChanged.AddListener(ToggleValueChanged);
|
||||
|
||||
if (m_Knob != null)
|
||||
{
|
||||
m_InitialBackground = m_Knob.localPosition.z;
|
||||
}
|
||||
}
|
||||
|
||||
void OnEnable()
|
||||
{
|
||||
ToggleValueChanged(m_Toggle.isOn);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
void IPointerEnterHandler.OnPointerEnter(PointerEventData eventData)
|
||||
{
|
||||
PerformEntranceActions();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
void IPointerExitHandler.OnPointerExit(PointerEventData eventData)
|
||||
{
|
||||
PerformExitActions();
|
||||
}
|
||||
|
||||
void ToggleValueChanged(bool value)
|
||||
{
|
||||
if (value)
|
||||
{
|
||||
m_Knob.localPosition = new Vector3(k_TargetPositionX, m_Knob.localPosition.y, m_Knob.localPosition.z);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_Knob.localPosition = new Vector3(-k_TargetPositionX, m_Knob.localPosition.y, m_Knob.localPosition.z);
|
||||
}
|
||||
}
|
||||
|
||||
void PerformEntranceActions()
|
||||
{
|
||||
if (m_Knob != null)
|
||||
{
|
||||
var backgroundLocalPosition = m_Knob.localPosition;
|
||||
backgroundLocalPosition.z = m_InitialBackground - m_ZTranslation;
|
||||
m_Knob.localPosition = backgroundLocalPosition;
|
||||
}
|
||||
}
|
||||
|
||||
void PerformExitActions()
|
||||
{
|
||||
if (m_Knob != null)
|
||||
{
|
||||
var backgroundLocalPosition = m_Knob.localPosition;
|
||||
backgroundLocalPosition.z = m_InitialBackground;
|
||||
m_Knob.localPosition = backgroundLocalPosition;
|
||||
m_Knob.localScale = Vector3.one;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f390e213230ce1d42a51aed871ab74ce
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,84 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.XR.Interaction.Toolkit.Locomotion.Teleportation;
|
||||
|
||||
namespace XRMultiplayer
|
||||
{
|
||||
public class CharacterResetter : MonoBehaviour
|
||||
{
|
||||
[SerializeField] Vector2 m_MinMaxHeight = new Vector2(-2.5f, 25.0f);
|
||||
[SerializeField] float m_ResetDistance = 75.0f;
|
||||
[SerializeField] Vector3 offlinePosition = new Vector3(0, .5f, -12.0f);
|
||||
[SerializeField] Vector3 onlinePosition = new Vector3(0, .15f, 0);
|
||||
TeleportationProvider m_TeleportationProvider;
|
||||
Vector3 m_ResetPosition;
|
||||
private void Start()
|
||||
{
|
||||
XRINetworkGameManager.Connected.Subscribe(UpdateResetPosition);
|
||||
m_TeleportationProvider = GetComponentInChildren<TeleportationProvider>();
|
||||
|
||||
m_ResetPosition = offlinePosition;
|
||||
ResetPlayer();
|
||||
}
|
||||
|
||||
void UpdateResetPosition(bool connected)
|
||||
{
|
||||
if (connected)
|
||||
{
|
||||
m_ResetPosition = onlinePosition;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_ResetPosition = offlinePosition;
|
||||
ResetPlayer();
|
||||
}
|
||||
}
|
||||
|
||||
// Update is called once per frame
|
||||
void Update()
|
||||
{
|
||||
if (transform.position.y < m_MinMaxHeight.x)
|
||||
{
|
||||
ResetPlayer();
|
||||
}
|
||||
else if (transform.position.y > m_MinMaxHeight.y)
|
||||
{
|
||||
ResetPlayer();
|
||||
}
|
||||
if (Mathf.Abs(transform.position.x) > m_ResetDistance || Mathf.Abs(transform.position.z) > m_ResetDistance)
|
||||
{
|
||||
ResetPlayer();
|
||||
}
|
||||
}
|
||||
|
||||
public void ResetPlayer()
|
||||
{
|
||||
ResetPlayer(m_ResetPosition);
|
||||
}
|
||||
|
||||
void ResetPlayer(Vector3 destination)
|
||||
{
|
||||
TeleportRequest teleportRequest = new()
|
||||
{
|
||||
destinationPosition = destination,
|
||||
destinationRotation = Quaternion.identity
|
||||
};
|
||||
|
||||
if (!m_TeleportationProvider.QueueTeleportRequest(teleportRequest))
|
||||
{
|
||||
Utils.LogWarning("Failed to queue teleport request");
|
||||
}
|
||||
}
|
||||
|
||||
[ContextMenu("Set Player To Online Position")]
|
||||
void SetPlayerToOnlinePosition()
|
||||
{
|
||||
ResetPlayer(onlinePosition);
|
||||
}
|
||||
|
||||
[ContextMenu("Set Player To Offline Position")]
|
||||
void SetPlayerToOfflinePosition()
|
||||
{
|
||||
ResetPlayer(offlinePosition);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c954b6e8024c0b248a278eab8c6828ee
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cd5b2479bf1334144a93423ea317ae20
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,202 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Unity.VRTemplate
|
||||
{
|
||||
/// <summary>
|
||||
/// Draws a bezier curve from a starting point transform to an end point transform
|
||||
/// </summary>
|
||||
public class BezierCurve : MonoBehaviour
|
||||
{
|
||||
/// <summary>
|
||||
/// If the view scale changes more than this amount, then the line width will be updated causing the line to be rebuilt.
|
||||
/// </summary>
|
||||
const float k_ViewerScaleChangeThreshold = 0.1f;
|
||||
|
||||
/// <summary>
|
||||
/// The time within the frame that the curve will be updated.
|
||||
/// </summary>
|
||||
/// <seealso cref="UnityEngine.XR.Interaction.Toolkit.XRBaseController.UpdateType"/>
|
||||
public enum UpdateType
|
||||
{
|
||||
/// <summary>
|
||||
/// Sample at both update and directly before rendering. For smooth tracking,
|
||||
/// we recommend using this value as it will provide the lowest input latency for the device.
|
||||
/// </summary>
|
||||
UpdateAndBeforeRender,
|
||||
|
||||
/// <summary>
|
||||
/// Only sample input during the update phase of the frame.
|
||||
/// </summary>
|
||||
Update,
|
||||
|
||||
/// <summary>
|
||||
/// Only sample input directly before rendering.
|
||||
/// </summary>
|
||||
BeforeRender,
|
||||
}
|
||||
|
||||
#pragma warning disable 649
|
||||
[SerializeField, Tooltip("The time within the frame that the curve will be updated. If this Bezier Curve is attached to a transform that is updating before render, then enabling updates in Before Render will keep the line connected without delay.")]
|
||||
UpdateType m_UpdateTrackingType = UpdateType.Update;
|
||||
|
||||
[SerializeField, Tooltip("The transform that determines the position, handle rotation, and handle scale of the start point of the bezier curve.")]
|
||||
Transform m_StartPoint;
|
||||
|
||||
[SerializeField, Tooltip("The transform that determines the position, handle rotation, and handle scale of the end point of the bezier curve.")]
|
||||
Transform m_EndPoint;
|
||||
|
||||
[SerializeField, Tooltip("Controls the scale factor of the curve's start bezier handle.")]
|
||||
float m_CurveFactorStart = 1.0f;
|
||||
|
||||
[SerializeField, Tooltip("Controls the scale factor of the curve's end bezier handle.")]
|
||||
float m_CurveFactorEnd = 1.0f;
|
||||
|
||||
[SerializeField, Tooltip("Controls the number of segments used to draw the curve.")]
|
||||
int m_SegmentCount = 50;
|
||||
|
||||
[SerializeField, Tooltip("When enabled, the line color gradient will be animated so that an opaque part travels along the line.")]
|
||||
bool m_Animate;
|
||||
|
||||
[SerializeField, Tooltip("If animated, this controls the speed that the animation of the line.")]
|
||||
float m_AnimSpeed = 0.25f;
|
||||
|
||||
[SerializeField, Tooltip("If animated, this color will be the main opaque color of the gradient")]
|
||||
Color m_GradientKeyColor = new Color(0.1254902f, 0.5882353f, 0.9529412f);
|
||||
|
||||
[SerializeField, Tooltip("The line renderer that will draw the curve. If not set it will find a line renderer on this GameObject.")]
|
||||
LineRenderer m_LineRenderer;
|
||||
#pragma warning restore 649
|
||||
|
||||
Vector3[] m_ControlPoints = new Vector3[4];
|
||||
float m_Time;
|
||||
float m_LineWidth;
|
||||
float m_LastViewerScale;
|
||||
|
||||
Vector3 m_LastStartPosition;
|
||||
Vector3 m_LastEndPosition;
|
||||
//IProvidesViewerScale IFunctionalitySubscriber<IProvidesViewerScale>.provider { get; set; }
|
||||
|
||||
void Awake()
|
||||
{
|
||||
if (m_LineRenderer == null)
|
||||
m_LineRenderer = GetComponent<LineRenderer>();
|
||||
|
||||
m_LineWidth = m_LineRenderer.startWidth;
|
||||
}
|
||||
|
||||
void OnEnable()
|
||||
{
|
||||
DrawCurve();
|
||||
Application.onBeforeRender += OnBeforeRender;
|
||||
}
|
||||
|
||||
void OnDisable()
|
||||
{
|
||||
Application.onBeforeRender -= OnBeforeRender;
|
||||
|
||||
}
|
||||
|
||||
void OnBeforeRender()
|
||||
{
|
||||
if (m_UpdateTrackingType == UpdateType.BeforeRender || m_UpdateTrackingType == UpdateType.UpdateAndBeforeRender)
|
||||
DrawCurve();
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (m_UpdateTrackingType == UpdateType.Update || m_UpdateTrackingType == UpdateType.UpdateAndBeforeRender)
|
||||
DrawCurve();
|
||||
|
||||
if (m_Animate)
|
||||
{
|
||||
AnimateCurve();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the line points to draw the bezier curve.
|
||||
/// </summary>
|
||||
[ContextMenu("Draw")]
|
||||
public void DrawCurve()
|
||||
{
|
||||
var startPointPosition = m_StartPoint.position;
|
||||
var endPointPosition = m_EndPoint.position;
|
||||
|
||||
if (startPointPosition == m_LastStartPosition &&
|
||||
endPointPosition == m_LastEndPosition)
|
||||
return; // Return early if the start and end have not changed to avoid recalculating the curve
|
||||
|
||||
var dist = Vector3.Distance(startPointPosition, endPointPosition);
|
||||
|
||||
m_ControlPoints[0] = startPointPosition;
|
||||
m_ControlPoints[1] = startPointPosition + (m_StartPoint.right * (dist * m_CurveFactorStart));
|
||||
m_ControlPoints[2] = endPointPosition - (m_EndPoint.right * (dist * m_CurveFactorEnd));
|
||||
m_ControlPoints[3] = endPointPosition;
|
||||
|
||||
int segmentCount;
|
||||
const float smallestCurveLength = 0.0125f;
|
||||
if (Vector3.Distance(startPointPosition, endPointPosition) < (smallestCurveLength * m_LastViewerScale))
|
||||
{
|
||||
segmentCount = 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
segmentCount = m_SegmentCount;
|
||||
}
|
||||
|
||||
m_LineRenderer.positionCount = segmentCount + 1;
|
||||
m_LineRenderer.SetPosition(0, m_ControlPoints[0]);
|
||||
for (var i = 1; i <= segmentCount; i++)
|
||||
{
|
||||
var t = i / (float)segmentCount;
|
||||
var pixel = CalculateCubicBezierPoint(t, m_ControlPoints[0], m_ControlPoints[1], m_ControlPoints[2], m_ControlPoints[3]);
|
||||
m_LineRenderer.SetPosition(i, pixel);
|
||||
}
|
||||
|
||||
m_LastStartPosition = startPointPosition;
|
||||
m_LastEndPosition = endPointPosition;
|
||||
}
|
||||
|
||||
static Vector3 CalculateCubicBezierPoint(float t, Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3)
|
||||
{
|
||||
var u = 1 - t;
|
||||
var tt = t * t;
|
||||
var uu = u * u;
|
||||
var uuu = uu * u;
|
||||
var ttt = tt * t;
|
||||
|
||||
var p = uuu * p0;
|
||||
p += 3 * uu * t * p1;
|
||||
p += 3 * u * tt * p2;
|
||||
p += ttt * p3;
|
||||
|
||||
return p;
|
||||
}
|
||||
|
||||
void AnimateCurve()
|
||||
{
|
||||
var newGrad = new Gradient();
|
||||
|
||||
var colorKeys = new GradientColorKey[1];
|
||||
var alphaKeys = new GradientAlphaKey[2];
|
||||
|
||||
var colorKey = new GradientColorKey(m_GradientKeyColor, 0f);
|
||||
colorKeys[0] = colorKey;
|
||||
|
||||
var alphaKeyStart = new GradientAlphaKey(.25f, m_Time);
|
||||
var alphaKeyEnd = new GradientAlphaKey(1f, 1f);
|
||||
alphaKeys[0] = alphaKeyStart;
|
||||
alphaKeys[1] = alphaKeyEnd;
|
||||
|
||||
newGrad.SetKeys(colorKeys, alphaKeys);
|
||||
newGrad.mode = GradientMode.Blend;
|
||||
|
||||
m_LineRenderer.colorGradient = newGrad;
|
||||
m_Time += (Time.unscaledDeltaTime * m_AnimSpeed);
|
||||
|
||||
if (m_Time >= 1f)
|
||||
m_Time = 0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 72fb4b8d89bc26347a49177acaa93913
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,129 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Unity.VRTemplate
|
||||
{
|
||||
/// <summary>
|
||||
/// Callout used to display information like world and controller tooltips.
|
||||
/// </summary>
|
||||
public class Callout : MonoBehaviour
|
||||
{
|
||||
[SerializeField, Tooltip("Whether Gaze Callout is used.")]
|
||||
bool m_UseGazeCallout = true;
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("The tooltip Transform associated with this Callout.")]
|
||||
Transform m_LazyTooltip;
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("The line curve GameObject associated with this Callout.")]
|
||||
GameObject m_Curve;
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("The required time to dwell on this callout before the tooltip and curve are enabled.")]
|
||||
float m_DwellTime = 1f;
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("Whether the associated tooltip will be unparented on Start.")]
|
||||
bool m_Unparent = true;
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("Whether the associated tooltip and curve will be disabled on Start.")]
|
||||
bool m_TurnOffAtStart = true;
|
||||
|
||||
bool m_Gazing = false;
|
||||
|
||||
Coroutine m_StartCo;
|
||||
Coroutine m_EndCo;
|
||||
|
||||
void Start()
|
||||
{
|
||||
if (!m_UseGazeCallout)
|
||||
{
|
||||
DisableCallout();
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_Unparent)
|
||||
{
|
||||
if (m_LazyTooltip != null)
|
||||
m_LazyTooltip.SetParent(null);
|
||||
}
|
||||
|
||||
if (m_TurnOffAtStart)
|
||||
{
|
||||
if (m_LazyTooltip != null)
|
||||
m_LazyTooltip.gameObject.SetActive(false);
|
||||
if (m_Curve != null)
|
||||
m_Curve.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
public void GazeHoverStart()
|
||||
{
|
||||
if (!m_UseGazeCallout)
|
||||
{
|
||||
DisableCallout();
|
||||
return;
|
||||
}
|
||||
|
||||
m_Gazing = true;
|
||||
if (m_StartCo != null)
|
||||
StopCoroutine(m_StartCo);
|
||||
if (m_EndCo != null)
|
||||
StopCoroutine(m_EndCo);
|
||||
m_StartCo = StartCoroutine(StartDelay());
|
||||
}
|
||||
|
||||
public void GazeHoverEnd()
|
||||
{
|
||||
if (!m_UseGazeCallout)
|
||||
{
|
||||
DisableCallout();
|
||||
return;
|
||||
}
|
||||
|
||||
m_Gazing = false;
|
||||
m_EndCo = StartCoroutine(EndDelay());
|
||||
}
|
||||
|
||||
IEnumerator StartDelay()
|
||||
{
|
||||
yield return new WaitForSeconds(m_DwellTime);
|
||||
if (m_Gazing)
|
||||
TurnOnStuff();
|
||||
}
|
||||
|
||||
IEnumerator EndDelay()
|
||||
{
|
||||
if (!m_Gazing)
|
||||
TurnOffStuff();
|
||||
yield return null;
|
||||
}
|
||||
|
||||
void TurnOnStuff()
|
||||
{
|
||||
if (m_LazyTooltip != null)
|
||||
m_LazyTooltip.gameObject.SetActive(true);
|
||||
if (m_Curve != null)
|
||||
m_Curve.SetActive(true);
|
||||
}
|
||||
|
||||
void TurnOffStuff()
|
||||
{
|
||||
if (m_LazyTooltip != null)
|
||||
m_LazyTooltip.gameObject.SetActive(false);
|
||||
if (m_Curve != null)
|
||||
m_Curve.SetActive(false);
|
||||
}
|
||||
|
||||
void DisableCallout()
|
||||
{
|
||||
if (m_StartCo != null)
|
||||
StopCoroutine(m_StartCo);
|
||||
if (m_EndCo != null)
|
||||
StopCoroutine(m_EndCo);
|
||||
TurnOffStuff();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 16809ed3baa3d2341b75ec4c0aa874d0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,74 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Unity.VRTemplate
|
||||
{
|
||||
/// <summary>
|
||||
/// Makes this object face a target smoothly and along specific axes
|
||||
/// </summary>
|
||||
public class TurnToFace : MonoBehaviour
|
||||
{
|
||||
#pragma warning disable 649
|
||||
public Transform faceTarget
|
||||
{
|
||||
get => m_FaceTarget;
|
||||
set => m_FaceTarget = value;
|
||||
}
|
||||
[SerializeField]
|
||||
[Tooltip("Target to face towards. If not set, this will default to the main camera")]
|
||||
Transform m_FaceTarget;
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("Speed to turn")]
|
||||
float m_TurnToFaceSpeed = 5f;
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("Local rotation offset")]
|
||||
Vector3 m_RotationOffset = Vector3.zero;
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("If enabled, ignore the x axis when rotating")]
|
||||
bool m_IgnoreX;
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("If enabled, ignore the y axis when rotating")]
|
||||
bool m_IgnoreY;
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("If enabled, ignore the z axis when rotating")]
|
||||
bool m_IgnoreZ;
|
||||
#pragma warning restore 649
|
||||
|
||||
void Awake()
|
||||
{
|
||||
// Default to main camera
|
||||
if (m_FaceTarget == null)
|
||||
if (Camera.main != null)
|
||||
m_FaceTarget = Camera.main.transform;
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (m_FaceTarget != null)
|
||||
{
|
||||
var facePosition = m_FaceTarget.position;
|
||||
var forward = facePosition - transform.position;
|
||||
var targetRotation = forward.sqrMagnitude > float.Epsilon ? Quaternion.LookRotation(forward, Vector3.up) : Quaternion.identity;
|
||||
targetRotation *= Quaternion.Euler(m_RotationOffset);
|
||||
if (m_IgnoreX || m_IgnoreY || m_IgnoreZ)
|
||||
{
|
||||
var targetEuler = targetRotation.eulerAngles;
|
||||
var currentEuler = transform.rotation.eulerAngles;
|
||||
targetRotation = Quaternion.Euler
|
||||
(
|
||||
m_IgnoreX ? currentEuler.x : targetEuler.x,
|
||||
m_IgnoreY ? currentEuler.y : targetEuler.y,
|
||||
m_IgnoreZ ? currentEuler.z : targetEuler.z
|
||||
);
|
||||
}
|
||||
|
||||
var ease = 1f - Mathf.Exp(-m_TurnToFaceSpeed * Time.unscaledDeltaTime);
|
||||
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, ease);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 35c58493ec2a8cb43ba320ce1af1adc6
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,30 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
|
||||
namespace XRMultiplayer
|
||||
{
|
||||
public class DelayedUnityEvent : MonoBehaviour
|
||||
{
|
||||
[SerializeField] float m_TimeToEnable = 4.0f;
|
||||
[SerializeField] UnityEvent m_UnityEvent;
|
||||
Coroutine m_EnablingRoutine;
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (m_EnablingRoutine != null) StopCoroutine(m_EnablingRoutine);
|
||||
m_EnablingRoutine = StartCoroutine(EnableAfterTime());
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (m_EnablingRoutine != null) StopCoroutine(m_EnablingRoutine);
|
||||
}
|
||||
|
||||
IEnumerator EnableAfterTime()
|
||||
{
|
||||
yield return new WaitForSeconds(m_TimeToEnable);
|
||||
m_UnityEvent.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d283a9893352d6e49b6b6db49148e069
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,17 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace XRMultiplayer
|
||||
{
|
||||
public class GameObjectToggle : MonoBehaviour
|
||||
{
|
||||
[SerializeField] GameObject[] objectsToToggle;
|
||||
|
||||
public void ToggleObjects()
|
||||
{
|
||||
foreach (var obj in objectsToToggle)
|
||||
{
|
||||
obj.SetActive(!obj.activeSelf);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7a9a7fb087d961449a55b0c20d051ffb
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 59b8da3d55e3cb74c91ad743d542a9a2
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace UnityEngine.XR.Content.Interaction
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface to implement for objects that hold a set of <c>Key</c>s
|
||||
/// </summary>
|
||||
public interface IKeychain
|
||||
{
|
||||
/// <summary>
|
||||
/// This callback is used to check if this keychain has a specific <c>Key</c>
|
||||
/// <see cref="Lock"/>
|
||||
/// </summary>
|
||||
/// <param name="key">the key to be checked</param>
|
||||
/// <returns>True if this keychain has the supplied key; false otherwise</returns>
|
||||
bool Contains(Key key);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b3273498567e6fc4b944c2985269269d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace UnityEngine.XR.Content.Interaction
|
||||
{
|
||||
/// <summary>
|
||||
/// An asset that represents a key. Used to check if an object can perform some action
|
||||
/// (<see cref="XRLockSocketInteractor"/> and <see cref="Keychain"/>)
|
||||
/// </summary>
|
||||
[CreateAssetMenuAttribute(menuName = "XR/Key Lock System/Key")]
|
||||
public class Key : ScriptableObject
|
||||
{ }
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4e629f4cfca91134e86ae027aaa5d4eb
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,73 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace UnityEngine.XR.Content.Interaction
|
||||
{
|
||||
/// <summary>
|
||||
/// A generic Keychain component that holds the <see cref="Key"/>s to open a <see cref="Lock"/>.
|
||||
/// Attach a Keychain component to an Interactable and assign to it the same Keys of an <see cref="XRLockSocketInteractor"/>
|
||||
/// or an <see cref="XRLockGridSocketInteractor"/> to open (or interact with) them.
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
public class Keychain : MonoBehaviour, IKeychain
|
||||
{
|
||||
[SerializeField]
|
||||
[Tooltip("The keys on this keychain" +
|
||||
"Create new keys by selecting \"Assets/Create/XR/Key Lock System/Key\"")]
|
||||
List<Key> m_Keys;
|
||||
|
||||
HashSet<int> m_KeysHashSet = new HashSet<int>();
|
||||
|
||||
void Awake()
|
||||
{
|
||||
RepopulateHashSet();
|
||||
}
|
||||
|
||||
void OnValidate()
|
||||
{
|
||||
// A key was added through the inspector while the game was running?
|
||||
if (Application.isPlaying && m_Keys.Count != m_KeysHashSet.Count)
|
||||
RepopulateHashSet();
|
||||
}
|
||||
|
||||
void RepopulateHashSet()
|
||||
{
|
||||
m_KeysHashSet.Clear();
|
||||
foreach (var key in m_Keys)
|
||||
{
|
||||
if (key != null)
|
||||
m_KeysHashSet.Add(key.GetInstanceID());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the supplied key to this keychain
|
||||
/// </summary>
|
||||
/// <param name="key">The key to be added to the keychain</param>
|
||||
public void AddKey(Key key)
|
||||
{
|
||||
if (key == null || Contains(key))
|
||||
return;
|
||||
|
||||
m_Keys.Add(key);
|
||||
m_KeysHashSet.Add(key.GetInstanceID());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the supplied key from this keychain
|
||||
/// </summary>
|
||||
/// <param name="key">The key to be removed from the keychain</param>
|
||||
public void RemoveKey(Key key)
|
||||
{
|
||||
m_Keys.Remove(key);
|
||||
|
||||
if (key != null)
|
||||
m_KeysHashSet.Remove(key.GetInstanceID());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Contains(Key key)
|
||||
{
|
||||
return key != null && m_KeysHashSet.Contains(key.GetInstanceID());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 505599121cd7c2d4d87a596056b0142b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,48 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace UnityEngine.XR.Content.Interaction
|
||||
{
|
||||
/// <summary>
|
||||
/// Use this object as a generic way to validate if an object can perform some action.
|
||||
/// The check is done in the <see cref="CanUnlock"/> method.
|
||||
/// This class is used in combination with a <see cref="Keychain"/> component.
|
||||
/// </summary>
|
||||
/// <seealso cref="XRLockSocketInteractor"/>
|
||||
/// <seealso cref="XRLockGridSocketInteractor"/>
|
||||
[Serializable]
|
||||
public class Lock
|
||||
{
|
||||
[SerializeField]
|
||||
[Tooltip("The required keys to unlock this lock" +
|
||||
"Create new keys by selecting \"Assets/Create/XR/Key Lock System/Key\"")]
|
||||
List<Key> m_RequiredKeys;
|
||||
|
||||
/// <summary>
|
||||
/// Returns the required keys to unlock this lock.
|
||||
/// </summary>
|
||||
public List<Key> requiredKeys => m_RequiredKeys;
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the supplied keychain has all the required keys to open this lock.
|
||||
/// </summary>
|
||||
/// <param name="keychain">The keychain to be checked.</param>
|
||||
/// <returns>True if the supplied keychain has all the required keys; false otherwise.</returns>
|
||||
public bool CanUnlock(IKeychain keychain)
|
||||
{
|
||||
if (keychain == null)
|
||||
return m_RequiredKeys.Count == 0;
|
||||
|
||||
foreach (var requiredKey in m_RequiredKeys)
|
||||
{
|
||||
if (requiredKey == null)
|
||||
continue;
|
||||
|
||||
if (!keychain.Contains(requiredKey))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1d479cceb8cf26842888dcaf56f46717
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,150 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Unity.VRTemplate
|
||||
{
|
||||
/// <summary>
|
||||
/// Makes the object this is attached to follow a target with a slight delay
|
||||
/// </summary>
|
||||
public class LazyFollow : MonoBehaviour
|
||||
{
|
||||
#pragma warning disable 649
|
||||
[SerializeField]
|
||||
[Tooltip("The object being followed.")]
|
||||
Transform m_Target;
|
||||
#pragma warning restore 649
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("Whether to always follow or only when in-view.")]
|
||||
bool m_FOV = false;
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("Whether rotation is locked to the z-axis for can move in any direction.")]
|
||||
bool m_ZRot = true;
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("Adjusts the follow point from the target by this amount.")]
|
||||
Vector3 m_TargetOffset = Vector3.forward;
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("Snap to target position when this component is enabled.")]
|
||||
bool m_SnapOnEnable = true;
|
||||
|
||||
public bool followActive = true;
|
||||
|
||||
Vector3 m_TargetLastPos;
|
||||
Camera m_Camera;
|
||||
public float smoothTime = 0.3F;
|
||||
private Vector3 velocity = Vector3.zero;
|
||||
bool m_InFOV = false;
|
||||
|
||||
Vector3 targetPosition => m_Target.position + m_Target.TransformVector(m_TargetOffset);
|
||||
Quaternion targetRotation
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!m_ZRot)
|
||||
{
|
||||
var eulerAngles = m_Target.eulerAngles;
|
||||
eulerAngles = new Vector3(eulerAngles.x, eulerAngles.y, 0f);
|
||||
return Quaternion.Euler(eulerAngles);
|
||||
}
|
||||
|
||||
return m_Target.rotation;
|
||||
}
|
||||
}
|
||||
|
||||
void Awake()
|
||||
{
|
||||
if (m_Camera == null)
|
||||
m_Camera = Camera.main;
|
||||
|
||||
// Default to main camera
|
||||
if (m_Target == null)
|
||||
if (m_Camera != null)
|
||||
m_Target = m_Camera.transform;
|
||||
}
|
||||
|
||||
void Start()
|
||||
{
|
||||
var targetPos = targetPosition;
|
||||
m_TargetLastPos = targetPos;
|
||||
}
|
||||
|
||||
void OnEnable()
|
||||
{
|
||||
if (m_SnapOnEnable)
|
||||
{
|
||||
transform.position = targetPosition;
|
||||
velocity = Vector3.zero;
|
||||
}
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (m_FOV)
|
||||
{
|
||||
Vector3 screenPoint = m_Camera.WorldToViewportPoint(this.gameObject.transform.position);
|
||||
var inFov = screenPoint.z > 0f && screenPoint.x > 0f && screenPoint.x < 1f && screenPoint.y > 0f && screenPoint.y < 1f;
|
||||
if (inFov)
|
||||
return;
|
||||
}
|
||||
|
||||
var targetPos = targetPosition;
|
||||
if (m_TargetLastPos == targetPos)
|
||||
return;
|
||||
|
||||
if (followActive)
|
||||
{
|
||||
transform.position = Vector3.SmoothDamp(transform.position, targetPos, ref velocity, smoothTime);
|
||||
m_TargetLastPos = targetPos;
|
||||
}
|
||||
}
|
||||
|
||||
public void Summon()
|
||||
{
|
||||
m_InFOV = false;
|
||||
if (!followActive)
|
||||
StartCoroutine(OneTimeSummonPosition());
|
||||
}
|
||||
|
||||
IEnumerator OneTimeSummonFOV()
|
||||
{
|
||||
while (!m_InFOV)
|
||||
{
|
||||
Vector3 screenPoint = m_Camera.WorldToViewportPoint(this.gameObject.transform.position);
|
||||
var inFov = screenPoint.z > 0f && screenPoint.x > 0.3f && screenPoint.x < 0.7f && screenPoint.y > 0.3f && screenPoint.y < 0.7f;
|
||||
if (inFov)
|
||||
{
|
||||
m_InFOV = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_InFOV = false;
|
||||
|
||||
var targetPos = targetPosition;
|
||||
if (m_TargetLastPos != targetPos)
|
||||
{
|
||||
transform.position = Vector3.SmoothDamp(transform.position, targetPos, ref velocity, smoothTime);
|
||||
m_TargetLastPos = targetPos;
|
||||
}
|
||||
}
|
||||
yield return null;
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerator OneTimeSummonPosition()
|
||||
{
|
||||
while (Vector3.Distance(transform.position, targetPosition) > 0.1f)
|
||||
{
|
||||
var targetPos = targetPosition;
|
||||
if (m_TargetLastPos != targetPos)
|
||||
{
|
||||
transform.position = Vector3.SmoothDamp(transform.position, targetPos, ref velocity, smoothTime);
|
||||
m_TargetLastPos = targetPos;
|
||||
}
|
||||
yield return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 63be463a5616ad444a25ac2d0faf7074
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,78 @@
|
||||
using UnityEngine;
|
||||
using System.Linq;
|
||||
|
||||
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor;
|
||||
#endif
|
||||
|
||||
namespace XRMultiplayer
|
||||
{
|
||||
/// <summary>
|
||||
/// Simple Utility class that toggles on Shadow Casting for static renderers before a bake
|
||||
/// and toggles off Shadow Casting upon bake completion.
|
||||
/// </summary>
|
||||
public class LightBakeUtility : MonoBehaviour
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
[SerializeField, Tooltip("Renderers assigned here will enable shadows before light baking and disable shadows upon light bake completion.")]
|
||||
Renderer[] m_StaticRenderers;
|
||||
|
||||
[SerializeField, Tooltip("Renderers assigned here will not have their shadow settings changed by this tool during the light baking process.")]
|
||||
Renderer[] m_Filters;
|
||||
|
||||
[SerializeField, Tooltip("Transforms assigned here will gather all children Renderers and will enable and disable shadow during the light baking process.")]
|
||||
Transform[] m_RendererParents;
|
||||
|
||||
[SerializeField] bool m_Log = false;
|
||||
|
||||
void OnValidate()
|
||||
{
|
||||
Log("Unsubsrcibing to Light Bake Events");
|
||||
Lightmapping.bakeStarted -= BakeLight;
|
||||
Lightmapping.bakeCompleted -= OnBakeCompleted;
|
||||
|
||||
Log("Subscribing to Light Bake Events");
|
||||
Lightmapping.bakeStarted += BakeLight;
|
||||
Lightmapping.bakeCompleted += OnBakeCompleted;
|
||||
}
|
||||
|
||||
void BakeLight()
|
||||
{
|
||||
Log("Starting Light Bake");
|
||||
ToggleShadowCasting(true);
|
||||
}
|
||||
|
||||
private void OnBakeCompleted()
|
||||
{
|
||||
Log("Light Bake Completed");
|
||||
ToggleShadowCasting(false);
|
||||
}
|
||||
|
||||
void ToggleShadowCasting(bool toggle)
|
||||
{
|
||||
foreach (var renderer in m_StaticRenderers)
|
||||
{
|
||||
if(renderer == null || m_Filters.Contains(renderer)){ continue; }
|
||||
|
||||
renderer.shadowCastingMode = toggle ? UnityEngine.Rendering.ShadowCastingMode.On : UnityEngine.Rendering.ShadowCastingMode.Off;
|
||||
}
|
||||
|
||||
foreach(Transform t in m_RendererParents)
|
||||
{
|
||||
foreach (var renderer in t.GetComponentsInChildren<Renderer>())
|
||||
{
|
||||
if(renderer == null || m_Filters.Contains(renderer)){ continue; }
|
||||
renderer.shadowCastingMode = toggle ? UnityEngine.Rendering.ShadowCastingMode.On : UnityEngine.Rendering.ShadowCastingMode.Off;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Log(string message)
|
||||
{
|
||||
if(m_Log)
|
||||
Utils.Log(message);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: aff3e70e7b23cdc478231849b7482699
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,40 @@
|
||||
using System.Collections.Generic;
|
||||
using Unity.Netcode.Components;
|
||||
using XRMultiplayer;
|
||||
|
||||
namespace UnityEngine.XR.Content.Interaction
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides the ability to reset objects
|
||||
/// </summary>
|
||||
public class ObjectReset : MonoBehaviour
|
||||
{
|
||||
[SerializeField] Transform m_ResetTransform;
|
||||
List<NetworkPhysicsInteractable> m_Interactables = new List<NetworkPhysicsInteractable>();
|
||||
|
||||
void OnTriggerEnter(Collider collider)
|
||||
{
|
||||
NetworkPhysicsInteractable networkBaseInteractable = collider.GetComponentInParent<NetworkPhysicsInteractable>();
|
||||
if (networkBaseInteractable != null && !networkBaseInteractable.isInteracting & !m_Interactables.Contains(networkBaseInteractable) && networkBaseInteractable.IsOwner)
|
||||
{
|
||||
m_Interactables.Add(networkBaseInteractable);
|
||||
ResetTransform(networkBaseInteractable);
|
||||
}
|
||||
}
|
||||
|
||||
void ResetTransform(NetworkPhysicsInteractable networkBaseInteractable)
|
||||
{
|
||||
Transform currentTransform = networkBaseInteractable.transform;
|
||||
networkBaseInteractable.GetComponent<NetworkTransform>().Teleport(m_ResetTransform.position, m_ResetTransform.rotation, networkBaseInteractable.transform.localScale);
|
||||
|
||||
var rigidBody = currentTransform.GetComponentInChildren<Rigidbody>();
|
||||
if (rigidBody != null)
|
||||
{
|
||||
networkBaseInteractable.ResetObjectPhysics();
|
||||
}
|
||||
|
||||
if (m_Interactables.Contains(networkBaseInteractable))
|
||||
m_Interactables.Remove(networkBaseInteractable);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 78ec3e941589f6747b883dfa38151663
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,107 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.Pool;
|
||||
|
||||
namespace XRMultiplayer
|
||||
{
|
||||
public class Pooler : MonoBehaviour
|
||||
{
|
||||
/// <summary>
|
||||
/// The Prefab to spawn and use for pooling.
|
||||
/// </summary>
|
||||
[SerializeField, Tooltip("The Prefab to spawn and use for pooling")]
|
||||
GameObject m_SpawnPrefab;
|
||||
|
||||
/// <summary>
|
||||
/// Collection checks are performed when an instance is returned back to the pool.
|
||||
/// An exception will be thrown if the instance is already in the pool.
|
||||
/// Collection checks are only performed in the Editor.
|
||||
/// </summary>
|
||||
[SerializeField, Tooltip("An exception will be thrown if the instance is already in the pool")]
|
||||
bool m_UseCollectionChecks = true;
|
||||
|
||||
/// <summary>
|
||||
/// The default capacity the pool will be created with.
|
||||
/// </summary>
|
||||
[SerializeField, Tooltip("he default capacity the pool will be created with")]
|
||||
int m_DefaultCapacity = 30;
|
||||
|
||||
/// <summary>
|
||||
/// The maximum size of the pool.
|
||||
/// When the pool reaches the max size then any further instances returned to the pool will be ignored and can be garbage collected.
|
||||
/// This can be used to prevent the pool growing to a very large size
|
||||
/// </summary>
|
||||
[SerializeField, Tooltip("The maximum size of the pool")]
|
||||
int m_MaxCapacity = 1000;
|
||||
|
||||
/// <summary>
|
||||
/// If true, the spawned object will be parented under the transform of the Pooler.
|
||||
/// </summary>
|
||||
[SerializeField, Tooltip("Spawned objects will be parented under this Transform")]
|
||||
bool m_ParentUnderTransform = false;
|
||||
|
||||
IObjectPool<GameObject> m_Pool;
|
||||
|
||||
protected virtual void Start()
|
||||
{
|
||||
InitializePool();
|
||||
}
|
||||
|
||||
protected void InitializePool()
|
||||
{
|
||||
m_Pool = new ObjectPool<GameObject>(CreateNewObject, OnTakeFromPool, OnReturnToPool,
|
||||
OnDestroyPoolObject, m_UseCollectionChecks, m_DefaultCapacity, m_MaxCapacity);
|
||||
}
|
||||
|
||||
protected GameObject CreateNewObject()
|
||||
{
|
||||
GameObject spawnedObject = Instantiate(m_SpawnPrefab);
|
||||
if (m_ParentUnderTransform)
|
||||
spawnedObject.transform.SetParent(transform);
|
||||
|
||||
return spawnedObject;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when an instance is taken from the pool.
|
||||
/// </summary>
|
||||
protected void OnTakeFromPool(GameObject go)
|
||||
{
|
||||
go.SetActive(true);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Called when returning an instance to the pool.
|
||||
/// </summary>
|
||||
protected void OnReturnToPool(GameObject go)
|
||||
{
|
||||
go.SetActive(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when returning an instance to a pool that is full, or when called <see cref="ObjectPool.Dispose"/>, or <see cref="ObjectPool.Clear"/>
|
||||
/// </summary>
|
||||
/// <param name="go"></param>
|
||||
protected void OnDestroyPoolObject(GameObject go)
|
||||
{
|
||||
Destroy(go);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get an instance from the pool. If the pool is empty then a new instance will be created.
|
||||
/// </summary>
|
||||
public GameObject GetItem()
|
||||
{
|
||||
return m_Pool.Get();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the instance back to the pool. Returning an instance to a pool that is full will cause the instance to be destroyed.
|
||||
/// </summary>
|
||||
/// <param name="item"></param>
|
||||
public void ReturnItem(GameObject item)
|
||||
{
|
||||
m_Pool.Release(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dce48ae454aeed348839c2c3f91ab34f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,4 @@
|
||||
namespace XRMultiplayer
|
||||
{
|
||||
public class PoolerProjectiles : Pooler { }
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 51e84aab007306d48bdf6774e8046b82
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,27 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace XRMultiplayer
|
||||
{
|
||||
[ExecuteInEditMode]
|
||||
public class PositionalClampY : MonoBehaviour
|
||||
{
|
||||
[SerializeField] Vector2 m_minMaxHeight;
|
||||
|
||||
private void Update()
|
||||
{
|
||||
ClampBounds();
|
||||
}
|
||||
|
||||
void ClampBounds()
|
||||
{
|
||||
if (transform.position.y < m_minMaxHeight.x)
|
||||
{
|
||||
transform.position = new Vector3(transform.position.x, m_minMaxHeight.x, transform.position.z);
|
||||
}
|
||||
else if (transform.position.y > m_minMaxHeight.y)
|
||||
{
|
||||
transform.position = new Vector3(transform.position.x, m_minMaxHeight.y, transform.position.z);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5d92ef1488c75c64da4a33d0379b73a4
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,20 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.Video;
|
||||
|
||||
/// <summary>
|
||||
/// This script Toggles on / off the video player after each loop to fix a bug where the video player freezes after time.
|
||||
/// </summary>
|
||||
public class ResetVideoOnLoop : MonoBehaviour
|
||||
{
|
||||
[SerializeField] VideoPlayer m_VideoPlayer;
|
||||
// Start is called before the first frame update
|
||||
void Start() => m_VideoPlayer.loopPointReached += OnLoopPointReached;
|
||||
void OnDestroy() => m_VideoPlayer.loopPointReached -= OnLoopPointReached;
|
||||
|
||||
|
||||
private void OnLoopPointReached(VideoPlayer source)
|
||||
{
|
||||
source.enabled = false;
|
||||
source.enabled = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c41195ed3f3c0c042afc4aadf698fe04
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,32 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace XRMultiplayer
|
||||
{
|
||||
[ExecuteInEditMode]
|
||||
public class SnapToPlayerHeight : MonoBehaviour
|
||||
{
|
||||
[SerializeField] float m_heightOffset = -.25f;
|
||||
[SerializeField] float m_ZOffset;
|
||||
[SerializeField] Transform m_CameraTransform;
|
||||
|
||||
|
||||
void Start() => SetupReferences();
|
||||
void OnValidate() => SetupReferences();
|
||||
|
||||
void SetupReferences()
|
||||
{
|
||||
if (m_CameraTransform == null && Camera.main != null)
|
||||
m_CameraTransform = Camera.main.transform;
|
||||
}
|
||||
|
||||
// Update is called once per frame
|
||||
void Update()
|
||||
{
|
||||
if (m_CameraTransform != null)
|
||||
{
|
||||
transform.position = new Vector3(transform.position.x, m_CameraTransform.position.y + m_heightOffset, transform.position.z);
|
||||
transform.localPosition = new Vector3(transform.localPosition.x, transform.localPosition.y, m_ZOffset);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 28f3812bf17744341bcc863f74d26d65
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,31 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace XRMultiplayer
|
||||
{
|
||||
/// <summary>
|
||||
/// A simple class used for callbacks when OnTriggerEnter or OnTriggerExit is called.
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(Collider))]
|
||||
public class SubTrigger : MonoBehaviour
|
||||
{
|
||||
public Action<Collider, bool> OnTriggerAction;
|
||||
public Collider subTriggerCollider;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (subTriggerCollider == null)
|
||||
TryGetComponent(out subTriggerCollider);
|
||||
}
|
||||
|
||||
private void OnTriggerEnter(Collider other)
|
||||
{
|
||||
OnTriggerAction?.Invoke(other, true);
|
||||
}
|
||||
|
||||
private void OnTriggerExit(Collider other)
|
||||
{
|
||||
OnTriggerAction?.Invoke(other, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7a05b87c216ca9e45a3773cd55311622
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,194 @@
|
||||
using System.Collections.Generic;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityEngine.XR.Interaction.Toolkit.Utilities.Tweenables.Primitives;
|
||||
|
||||
namespace XRMultiplayer
|
||||
{
|
||||
public class UIComponentToggler : CalloutGazeController
|
||||
{
|
||||
[Header("Component Toggling")]
|
||||
[SerializeField] CanvasGroup m_CanvasGroup;
|
||||
[SerializeField] TooltipUI m_TooltipUI;
|
||||
[SerializeField] float m_FadeDuration = .25f;
|
||||
[SerializeField] Vector2 m_MinMaxThresholdDistance = new Vector2(2.0f, 5.0f);
|
||||
[SerializeField] Vector2 m_MinMaxFacingThreshold = new Vector2(.8f, .995f);
|
||||
[SerializeField] float m_MaxRenderingDistance = 15.0f;
|
||||
[SerializeField] List<MonoBehaviour> m_ComponentsToToggle;
|
||||
[SerializeField] GameObject[] m_ObjectsToToggle;
|
||||
[SerializeField] bool m_StartHidden = true;
|
||||
[SerializeField] bool m_DisableCanvasGroupObject = false;
|
||||
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
FloatTweenableVariable m_FloatFadeTweenableVariable = new FloatTweenableVariable();
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
bool m_Hidden = false;
|
||||
bool m_InRange = false;
|
||||
|
||||
Coroutine m_FadeRoutine;
|
||||
// Start is called before the first frame update
|
||||
void Start()
|
||||
{
|
||||
if (m_GazeTransform == null)
|
||||
{
|
||||
m_GazeTransform = Camera.main.transform;
|
||||
}
|
||||
|
||||
if (m_CanvasGroup == null)
|
||||
{
|
||||
m_CanvasGroup = GetComponentInChildren<CanvasGroup>();
|
||||
}
|
||||
|
||||
if (m_TooltipUI == null)
|
||||
{
|
||||
m_TooltipUI = GetComponentInChildren<TooltipUI>();
|
||||
}
|
||||
|
||||
m_FacingThreshold = .98f;
|
||||
m_FacingEntered.AddListener(delegate { ToggleFade(false); });
|
||||
m_FacingExited.AddListener(delegate { ToggleFade(true); });
|
||||
|
||||
m_FloatFadeTweenableVariable.Subscribe(UpdateFade);
|
||||
|
||||
if (m_StartHidden)
|
||||
{
|
||||
ToggleFade(true);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
float currentDistance = Vector3.Distance(transform.position, m_GazeTransform.position);
|
||||
|
||||
if (m_InRange)
|
||||
{
|
||||
float perc = (Mathf.Clamp(currentDistance, m_MinMaxThresholdDistance.x, m_MinMaxThresholdDistance.y) - m_MinMaxThresholdDistance.x) / (m_MinMaxThresholdDistance.y - m_MinMaxThresholdDistance.x);
|
||||
m_FacingThreshold = Mathf.Lerp(m_MinMaxFacingThreshold.x, m_MinMaxFacingThreshold.y, perc);
|
||||
|
||||
if (currentDistance > m_MaxRenderingDistance)
|
||||
{
|
||||
m_InRange = false;
|
||||
ToggleFade(true);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (currentDistance <= m_MaxRenderingDistance)
|
||||
{
|
||||
m_InRange = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
m_FacingEntered.RemoveListener(delegate { ToggleFade(false); });
|
||||
m_FacingExited.RemoveListener(delegate { ToggleFade(true); });
|
||||
}
|
||||
|
||||
[ContextMenu("Get References")]
|
||||
void FindRendererReferences()
|
||||
{
|
||||
m_ComponentsToToggle = new List<MonoBehaviour>();
|
||||
List<Image> images = new List<Image>(GetComponentsInChildren<Image>());
|
||||
List<TMP_Text> texts = new List<TMP_Text>(GetComponentsInChildren<TMP_Text>());
|
||||
|
||||
foreach (Image image in images)
|
||||
{
|
||||
m_ComponentsToToggle.Add(image);
|
||||
}
|
||||
|
||||
foreach (TMP_Text text in texts)
|
||||
{
|
||||
m_ComponentsToToggle.Add(text);
|
||||
}
|
||||
}
|
||||
|
||||
[ContextMenu("Toggle Components")]
|
||||
void ToggleFade()
|
||||
{
|
||||
ToggleFade(!m_Hidden);
|
||||
}
|
||||
|
||||
void ToggleFade(bool toggle)
|
||||
{
|
||||
m_Hidden = toggle;
|
||||
if (!m_Hidden)
|
||||
{
|
||||
ToggleComponents(true);
|
||||
}
|
||||
|
||||
if (m_FadeRoutine != null) StopCoroutine(m_FadeRoutine);
|
||||
m_FadeRoutine = StartCoroutine(m_FloatFadeTweenableVariable.PlaySequence(m_FloatFadeTweenableVariable.Value, m_Hidden ? 0.0f : 1.0f, m_FadeDuration, CompleteFade));
|
||||
}
|
||||
|
||||
void ToggleComponents(bool show)
|
||||
{
|
||||
foreach (var c in m_ComponentsToToggle)
|
||||
{
|
||||
if (c != null)
|
||||
c.enabled = show;
|
||||
else
|
||||
Utils.Log("Component Toggler is missing references", 1);
|
||||
}
|
||||
|
||||
foreach (GameObject go in m_ObjectsToToggle)
|
||||
{
|
||||
if (go != null)
|
||||
go.SetActive(show);
|
||||
else
|
||||
Utils.Log("Component Toggler is missing references", 1);
|
||||
}
|
||||
|
||||
if (m_DisableCanvasGroupObject)
|
||||
{
|
||||
if (m_CanvasGroup != null)
|
||||
m_CanvasGroup.gameObject.SetActive(show);
|
||||
else
|
||||
Utils.Log("Component Toggler is missing references", 1);
|
||||
}
|
||||
|
||||
if (m_TooltipUI != null)
|
||||
{
|
||||
if (!show)
|
||||
{
|
||||
if (m_TooltipUI != null)
|
||||
m_TooltipUI.ResetTooltip();
|
||||
else
|
||||
Utils.Log("Component Toggler is missing references", 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateFade(float fadeAmount)
|
||||
{
|
||||
if (m_CanvasGroup != null)
|
||||
{
|
||||
m_CanvasGroup.alpha = fadeAmount;
|
||||
}
|
||||
}
|
||||
|
||||
void CompleteFade()
|
||||
{
|
||||
if (m_FloatFadeTweenableVariable.Value <= 0.0f)
|
||||
{
|
||||
ToggleComponents(false);
|
||||
}
|
||||
}
|
||||
|
||||
public void ToggleShow(bool show)
|
||||
{
|
||||
if (show)
|
||||
{
|
||||
m_FacingEntered.Invoke();
|
||||
}
|
||||
else
|
||||
{
|
||||
CheckPointerExit();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ba379b52851771f438e4580c5da84a31
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,93 @@
|
||||
using System.Text;
|
||||
using TMPro;
|
||||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace XRMultiplayer
|
||||
{
|
||||
public class Utils : MonoBehaviour
|
||||
{
|
||||
public const string k_LogPrefix = "<color=#33FF64>[XRMultiplayer]</color> ";
|
||||
public static LogLevel s_LogLevel = LogLevel.Developer;
|
||||
|
||||
public static void LogError(string message) => Log(message, 2);
|
||||
public static void LogWarning(string message) => Log(message, 1);
|
||||
public static void Log(string message, int logLevel = 0)
|
||||
{
|
||||
if (s_LogLevel == LogLevel.Nothing) return;
|
||||
StringBuilder sb = new(k_LogPrefix);
|
||||
sb.Append(message);
|
||||
|
||||
switch (logLevel)
|
||||
{
|
||||
case 0:
|
||||
if (s_LogLevel == 0)
|
||||
Debug.Log(sb);
|
||||
break;
|
||||
case 1:
|
||||
if ((int)s_LogLevel < 2)
|
||||
Debug.LogWarning(sb);
|
||||
break;
|
||||
case 2:
|
||||
Debug.LogError(sb);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public static string GetOrdinal(int num)
|
||||
{
|
||||
if (num <= 0) return num.ToString();
|
||||
|
||||
switch (num % 100)
|
||||
{
|
||||
case 11:
|
||||
case 12:
|
||||
case 13:
|
||||
return "th";
|
||||
}
|
||||
|
||||
switch (num % 10)
|
||||
{
|
||||
case 1:
|
||||
return "st";
|
||||
case 2:
|
||||
return "nd";
|
||||
case 3:
|
||||
return "rd";
|
||||
default:
|
||||
return "th";
|
||||
}
|
||||
}
|
||||
|
||||
public static int RealMod(int a, int b)
|
||||
{
|
||||
return (a % b + b) % b;
|
||||
}
|
||||
|
||||
public static float GetPercentOfValueBetweenTwoValues(float min, float max, float input)
|
||||
{
|
||||
input = Mathf.Clamp(input, min, max);
|
||||
|
||||
return (input - min) / (max - min);
|
||||
}
|
||||
}
|
||||
|
||||
[System.Serializable]
|
||||
public class TextButton
|
||||
{
|
||||
public Button button;
|
||||
public TMP_Text buttonText;
|
||||
|
||||
public void UpdateButton(UnityAction clickFunction, string newText, bool removeAllListeners = true, bool isInteractable = true)
|
||||
{
|
||||
if (removeAllListeners)
|
||||
button.onClick.RemoveAllListeners();
|
||||
|
||||
button.interactable = isInteractable;
|
||||
button.onClick.AddListener(clickFunction);
|
||||
buttonText.text = newText;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 50f56249bf6fe724c90534cda8b5d920
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,38 @@
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
|
||||
namespace XRMultiplayer
|
||||
{
|
||||
public class VersionText : MonoBehaviour
|
||||
{
|
||||
[SerializeField] TMP_Text[] m_VersionTextComponents;
|
||||
[SerializeField] string m_Prefix = "v";
|
||||
[SerializeField] string m_Suffix = "";
|
||||
|
||||
// Start is called before the first frame update
|
||||
void Start()
|
||||
{
|
||||
SetText();
|
||||
}
|
||||
|
||||
private void OnValidate()
|
||||
{
|
||||
SetText();
|
||||
}
|
||||
|
||||
void SetText()
|
||||
{
|
||||
if (m_VersionTextComponents != null)
|
||||
{
|
||||
foreach (TMP_Text t in m_VersionTextComponents)
|
||||
{
|
||||
t.text = $"{m_Prefix}{Application.version}{m_Suffix}";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Utils.Log("Missing Text component on VersionText script", 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4f5a2bea27a2d584dbd76071985c5c5e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,435 @@
|
||||
using System;
|
||||
using UnityEngine.Events;
|
||||
using UnityEngine.XR.Interaction.Toolkit;
|
||||
using UnityEngine.XR.Interaction.Toolkit.Interactors;
|
||||
|
||||
namespace UnityEngine.XR.Content.Interaction
|
||||
{
|
||||
/// <summary>
|
||||
/// An interactable knob that follows the rotation of the interactor
|
||||
/// </summary>
|
||||
public class XRKnob : UnityEngine.XR.Interaction.Toolkit.Interactables.XRBaseInteractable
|
||||
{
|
||||
const float k_ModeSwitchDeadZone = 0.1f; // Prevents rapid switching between the different rotation tracking modes
|
||||
|
||||
/// <summary>
|
||||
/// Helper class used to track rotations that can go beyond 180 degrees while minimizing accumulation error
|
||||
/// </summary>
|
||||
struct TrackedRotation
|
||||
{
|
||||
/// <summary>
|
||||
/// The anchor rotation we calculate an offset from
|
||||
/// </summary>
|
||||
float m_BaseAngle;
|
||||
|
||||
/// <summary>
|
||||
/// The target rotate we calculate the offset to
|
||||
/// </summary>
|
||||
float m_CurrentOffset;
|
||||
|
||||
/// <summary>
|
||||
/// Any previous offsets we've added in
|
||||
/// </summary>
|
||||
float m_AccumulatedAngle;
|
||||
|
||||
/// <summary>
|
||||
/// The total rotation that occurred from when this rotation started being tracked
|
||||
/// </summary>
|
||||
public float totalOffset => m_AccumulatedAngle + m_CurrentOffset;
|
||||
|
||||
/// <summary>
|
||||
/// Resets the tracked rotation so that total offset returns 0
|
||||
/// </summary>
|
||||
public void Reset()
|
||||
{
|
||||
m_BaseAngle = 0.0f;
|
||||
m_CurrentOffset = 0.0f;
|
||||
m_AccumulatedAngle = 0.0f;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets a new anchor rotation while maintaining any previously accumulated offset
|
||||
/// </summary>
|
||||
/// <param name="direction">The XZ vector used to calculate a rotation angle</param>
|
||||
public void SetBaseFromVector(Vector3 direction)
|
||||
{
|
||||
// Update any accumulated angle
|
||||
m_AccumulatedAngle += m_CurrentOffset;
|
||||
|
||||
// Now set a new base angle
|
||||
m_BaseAngle = Mathf.Atan2(direction.z, direction.x) * Mathf.Rad2Deg;
|
||||
m_CurrentOffset = 0.0f;
|
||||
}
|
||||
|
||||
public void SetTargetFromVector(Vector3 direction)
|
||||
{
|
||||
// Set the target angle
|
||||
var targetAngle = Mathf.Atan2(direction.z, direction.x) * Mathf.Rad2Deg;
|
||||
|
||||
// Return the offset
|
||||
m_CurrentOffset = ShortestAngleDistance(m_BaseAngle, targetAngle, 360.0f);
|
||||
|
||||
// If the offset is greater than 90 degrees, we update the base so we can rotate beyond 180 degrees
|
||||
if (Mathf.Abs(m_CurrentOffset) > 90.0f)
|
||||
{
|
||||
m_BaseAngle = targetAngle;
|
||||
m_AccumulatedAngle += m_CurrentOffset;
|
||||
m_CurrentOffset = 0.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class ValueChangeEvent : UnityEvent<float> { }
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("The object that is visually grabbed and manipulated")]
|
||||
Transform m_Handle = null;
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("The transform to snap the interactor to when holding the lever")]
|
||||
Transform m_InteractorSnapTransform = null;
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("The value of the knob")]
|
||||
[Range(0.0f, 1.0f)]
|
||||
float m_Value = 0.5f;
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("Whether this knob's rotation should be clamped by the angle limits")]
|
||||
bool m_ClampedMotion = true;
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("Rotation of the knob at value '1'")]
|
||||
float m_MaxAngle = 90.0f;
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("Rotation of the knob at value '0'")]
|
||||
float m_MinAngle = -90.0f;
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("Angle increments to support, if greater than '0'")]
|
||||
float m_AngleIncrement = 0.0f;
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("The position of the interactor controls rotation when outside this radius")]
|
||||
float m_PositionTrackedRadius = 0.1f;
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("How much controller rotation ")]
|
||||
float m_TwistSensitivity = 1.5f;
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("Events to trigger when the knob is rotated")]
|
||||
ValueChangeEvent m_OnValueChange = new ValueChangeEvent();
|
||||
|
||||
UnityEngine.XR.Interaction.Toolkit.Interactors.IXRSelectInteractor m_Interactor;
|
||||
|
||||
bool m_PositionDriven = false;
|
||||
bool m_UpVectorDriven = false;
|
||||
|
||||
TrackedRotation m_PositionAngles = new TrackedRotation();
|
||||
TrackedRotation m_UpVectorAngles = new TrackedRotation();
|
||||
TrackedRotation m_ForwardVectorAngles = new TrackedRotation();
|
||||
|
||||
float m_BaseKnobRotation = 0.0f;
|
||||
|
||||
/// <summary>
|
||||
/// The object that is visually grabbed and manipulated
|
||||
/// </summary>
|
||||
public Transform handle
|
||||
{
|
||||
get => m_Handle;
|
||||
set => m_Handle = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The value of the knob
|
||||
/// </summary>
|
||||
public float value
|
||||
{
|
||||
get => m_Value;
|
||||
set
|
||||
{
|
||||
SetValue(value);
|
||||
SetKnobRotation(ValueToRotation());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether this knob's rotation should be clamped by the angle limits
|
||||
/// </summary>
|
||||
public bool clampedMotion
|
||||
{
|
||||
get => m_ClampedMotion;
|
||||
set => m_ClampedMotion = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rotation of the knob at value '1'
|
||||
/// </summary>
|
||||
public float maxAngle
|
||||
{
|
||||
get => m_MaxAngle;
|
||||
set => m_MaxAngle = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rotation of the knob at value '0'
|
||||
/// </summary>
|
||||
public float minAngle
|
||||
{
|
||||
get => m_MinAngle;
|
||||
set => m_MinAngle = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The position of the interactor controls rotation when outside this radius
|
||||
/// </summary>
|
||||
public float positionTrackedRadius
|
||||
{
|
||||
get => m_PositionTrackedRadius;
|
||||
set => m_PositionTrackedRadius = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Events to trigger when the knob is rotated
|
||||
/// </summary>
|
||||
public ValueChangeEvent onValueChange => m_OnValueChange;
|
||||
|
||||
void Start()
|
||||
{
|
||||
SetValue(m_Value);
|
||||
SetKnobRotation(ValueToRotation());
|
||||
}
|
||||
|
||||
protected override void OnEnable()
|
||||
{
|
||||
base.OnEnable();
|
||||
selectEntered.AddListener(StartGrab);
|
||||
selectExited.AddListener(EndGrab);
|
||||
}
|
||||
|
||||
protected override void OnDisable()
|
||||
{
|
||||
selectEntered.RemoveListener(StartGrab);
|
||||
selectExited.RemoveListener(EndGrab);
|
||||
base.OnDisable();
|
||||
}
|
||||
|
||||
void StartGrab(SelectEnterEventArgs args)
|
||||
{
|
||||
m_Interactor = args.interactorObject;
|
||||
|
||||
m_PositionAngles.Reset();
|
||||
m_UpVectorAngles.Reset();
|
||||
m_ForwardVectorAngles.Reset();
|
||||
|
||||
UpdateBaseKnobRotation();
|
||||
UpdateRotation(true);
|
||||
}
|
||||
|
||||
void EndGrab(SelectExitEventArgs args)
|
||||
{
|
||||
m_Interactor = null;
|
||||
}
|
||||
|
||||
public override Transform GetAttachTransform(IXRInteractor interactor)
|
||||
{
|
||||
return m_InteractorSnapTransform;
|
||||
}
|
||||
|
||||
public override void ProcessInteractable(XRInteractionUpdateOrder.UpdatePhase updatePhase)
|
||||
{
|
||||
base.ProcessInteractable(updatePhase);
|
||||
|
||||
if (updatePhase == XRInteractionUpdateOrder.UpdatePhase.Dynamic)
|
||||
{
|
||||
if (isSelected)
|
||||
{
|
||||
UpdateRotation();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateRotation(bool freshCheck = false)
|
||||
{
|
||||
// Are we in position offset or direction rotation mode?
|
||||
var interactorTransform = m_Interactor.GetAttachTransform(this);
|
||||
|
||||
// We cache the three potential sources of rotation - the position offset, the forward vector of the controller, and up vector of the controller
|
||||
// We store any data used for determining which rotation to use, then flatten the vectors to the local xz plane
|
||||
var localOffset = transform.InverseTransformVector(interactorTransform.position - m_Handle.position);
|
||||
localOffset.y = 0.0f;
|
||||
var radiusOffset = transform.TransformVector(localOffset).magnitude;
|
||||
localOffset.Normalize();
|
||||
|
||||
var localForward = transform.InverseTransformDirection(interactorTransform.forward);
|
||||
var localY = Math.Abs(localForward.y);
|
||||
localForward.y = 0.0f;
|
||||
localForward.Normalize();
|
||||
|
||||
var localUp = transform.InverseTransformDirection(interactorTransform.up);
|
||||
localUp.y = 0.0f;
|
||||
localUp.Normalize();
|
||||
|
||||
|
||||
if (m_PositionDriven && !freshCheck)
|
||||
radiusOffset *= (1.0f + k_ModeSwitchDeadZone);
|
||||
|
||||
// Determine when a certain source of rotation won't contribute - in that case we bake in the offset it has applied
|
||||
// and set a new anchor when they can contribute again
|
||||
if (radiusOffset >= m_PositionTrackedRadius)
|
||||
{
|
||||
if (!m_PositionDriven || freshCheck)
|
||||
{
|
||||
m_PositionAngles.SetBaseFromVector(localOffset);
|
||||
m_PositionDriven = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
m_PositionDriven = false;
|
||||
|
||||
// If it's not a fresh check, then we weight the local Y up or down to keep it from flickering back and forth at boundaries
|
||||
if (!freshCheck)
|
||||
{
|
||||
if (!m_UpVectorDriven)
|
||||
localY *= (1.0f - (k_ModeSwitchDeadZone * 0.5f));
|
||||
else
|
||||
localY *= (1.0f + (k_ModeSwitchDeadZone * 0.5f));
|
||||
}
|
||||
|
||||
if (localY > 0.707f)
|
||||
{
|
||||
if (!m_UpVectorDriven || freshCheck)
|
||||
{
|
||||
m_UpVectorAngles.SetBaseFromVector(localUp);
|
||||
m_UpVectorDriven = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (m_UpVectorDriven || freshCheck)
|
||||
{
|
||||
m_ForwardVectorAngles.SetBaseFromVector(localForward);
|
||||
m_UpVectorDriven = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Get angle from position
|
||||
if (m_PositionDriven)
|
||||
m_PositionAngles.SetTargetFromVector(localOffset);
|
||||
|
||||
if (m_UpVectorDriven)
|
||||
m_UpVectorAngles.SetTargetFromVector(localUp);
|
||||
else
|
||||
m_ForwardVectorAngles.SetTargetFromVector(localForward);
|
||||
|
||||
// Apply offset to base knob rotation to get new knob rotation
|
||||
var knobRotation = m_BaseKnobRotation - ((m_UpVectorAngles.totalOffset + m_ForwardVectorAngles.totalOffset) * m_TwistSensitivity) - m_PositionAngles.totalOffset;
|
||||
|
||||
// Clamp to range
|
||||
if (m_ClampedMotion)
|
||||
knobRotation = Mathf.Clamp(knobRotation, m_MinAngle, m_MaxAngle);
|
||||
|
||||
SetKnobRotation(knobRotation);
|
||||
|
||||
// Reverse to get value
|
||||
var knobValue = (knobRotation - m_MinAngle) / (m_MaxAngle - m_MinAngle);
|
||||
SetValue(knobValue);
|
||||
}
|
||||
|
||||
void SetKnobRotation(float angle)
|
||||
{
|
||||
if (m_AngleIncrement > 0)
|
||||
{
|
||||
var normalizeAngle = angle - m_MinAngle;
|
||||
angle = (Mathf.Round(normalizeAngle / m_AngleIncrement) * m_AngleIncrement) + m_MinAngle;
|
||||
}
|
||||
|
||||
if (m_Handle != null)
|
||||
m_Handle.localEulerAngles = new Vector3(0.0f, angle, 0.0f);
|
||||
}
|
||||
|
||||
void SetValue(float value)
|
||||
{
|
||||
if (m_ClampedMotion)
|
||||
value = Mathf.Clamp01(value);
|
||||
|
||||
if (m_AngleIncrement > 0)
|
||||
{
|
||||
var angleRange = m_MaxAngle - m_MinAngle;
|
||||
var angle = Mathf.Lerp(0.0f, angleRange, value);
|
||||
angle = Mathf.Round(angle / m_AngleIncrement) * m_AngleIncrement;
|
||||
value = Mathf.InverseLerp(0.0f, angleRange, angle);
|
||||
}
|
||||
|
||||
m_Value = value;
|
||||
m_OnValueChange.Invoke(m_Value);
|
||||
}
|
||||
|
||||
float ValueToRotation()
|
||||
{
|
||||
return m_ClampedMotion ? Mathf.Lerp(m_MinAngle, m_MaxAngle, m_Value) : Mathf.LerpUnclamped(m_MinAngle, m_MaxAngle, m_Value);
|
||||
}
|
||||
|
||||
void UpdateBaseKnobRotation()
|
||||
{
|
||||
m_BaseKnobRotation = Mathf.LerpUnclamped(m_MinAngle, m_MaxAngle, m_Value);
|
||||
}
|
||||
|
||||
static float ShortestAngleDistance(float start, float end, float max)
|
||||
{
|
||||
var angleDelta = end - start;
|
||||
var angleSign = Mathf.Sign(angleDelta);
|
||||
|
||||
angleDelta = Math.Abs(angleDelta) % max;
|
||||
if (angleDelta > (max * 0.5f))
|
||||
angleDelta = -(max - angleDelta);
|
||||
|
||||
return angleDelta * angleSign;
|
||||
}
|
||||
|
||||
void OnDrawGizmosSelected()
|
||||
{
|
||||
const int k_CircleSegments = 16;
|
||||
const float k_SegmentRatio = 1.0f / k_CircleSegments;
|
||||
|
||||
// Nothing to do if position radius is too small
|
||||
if (m_PositionTrackedRadius <= Mathf.Epsilon)
|
||||
return;
|
||||
|
||||
// Draw a circle from the handle point at size of position tracked radius
|
||||
var circleCenter = transform.position;
|
||||
|
||||
if (m_Handle != null)
|
||||
circleCenter = m_Handle.position;
|
||||
|
||||
var circleX = transform.right;
|
||||
var circleY = transform.forward;
|
||||
|
||||
Gizmos.color = Color.green;
|
||||
var segmentCounter = 0;
|
||||
while (segmentCounter < k_CircleSegments)
|
||||
{
|
||||
var startAngle = (float)segmentCounter * k_SegmentRatio * 2.0f * Mathf.PI;
|
||||
segmentCounter++;
|
||||
var endAngle = (float)segmentCounter * k_SegmentRatio * 2.0f * Mathf.PI;
|
||||
|
||||
Gizmos.DrawLine(circleCenter + (Mathf.Cos(startAngle) * circleX + Mathf.Sin(startAngle) * circleY) * m_PositionTrackedRadius,
|
||||
circleCenter + (Mathf.Cos(endAngle) * circleX + Mathf.Sin(endAngle) * circleY) * m_PositionTrackedRadius);
|
||||
}
|
||||
}
|
||||
|
||||
void OnValidate()
|
||||
{
|
||||
if (m_ClampedMotion)
|
||||
m_Value = Mathf.Clamp01(m_Value);
|
||||
|
||||
if (m_MinAngle > m_MaxAngle)
|
||||
m_MinAngle = m_MaxAngle;
|
||||
|
||||
SetKnobRotation(ValueToRotation());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 65a8500fa86faa04596fb5f9d40efeae
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,242 @@
|
||||
using UnityEngine.Events;
|
||||
using UnityEngine.XR.Interaction.Toolkit;
|
||||
using UnityEngine.XR.Interaction.Toolkit.Interactors;
|
||||
|
||||
namespace UnityEngine.XR.Content.Interaction
|
||||
{
|
||||
/// <summary>
|
||||
/// An interactable lever that snaps into an on or off position by a direct interactor
|
||||
/// </summary>
|
||||
public class XRLever : XR.Interaction.Toolkit.Interactables.XRBaseInteractable
|
||||
{
|
||||
const float k_LeverDeadZone = 0.1f; // Prevents rapid switching between on and off states when right in the middle
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("The object that is visually grabbed and manipulated")]
|
||||
Transform m_Handle = null;
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("The transform to snap the interactor to when holding the lever")]
|
||||
Transform m_InteractorSnapTransform = null;
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("The value of the lever")]
|
||||
bool m_Value = false;
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("If enabled, the lever will snap to the value position when released")]
|
||||
bool m_LockToValue;
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("Angle of the lever in the 'on' position")]
|
||||
[Range(-90.0f, 90.0f)]
|
||||
float m_MaxAngle = 90.0f;
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("Angle of the lever in the 'off' position")]
|
||||
[Range(-90.0f, 90.0f)]
|
||||
float m_MinAngle = -90.0f;
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("Events to trigger when the lever activates")]
|
||||
UnityEvent m_OnLeverActivate = new UnityEvent();
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("Events to trigger when the lever deactivates")]
|
||||
UnityEvent m_OnLeverDeactivate = new UnityEvent();
|
||||
|
||||
UnityEngine.XR.Interaction.Toolkit.Interactors.IXRSelectInteractor m_Interactor;
|
||||
|
||||
/// <summary>
|
||||
/// The object that is visually grabbed and manipulated
|
||||
/// </summary>
|
||||
public Transform handle
|
||||
{
|
||||
get => m_Handle;
|
||||
set => m_Handle = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The value of the lever
|
||||
/// </summary>
|
||||
public bool value
|
||||
{
|
||||
get => m_Value;
|
||||
set => SetValue(value, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If enabled, the lever will snap to the value position when released
|
||||
/// </summary>
|
||||
public bool lockToValue { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Angle of the lever in the 'on' position
|
||||
/// </summary>
|
||||
public float maxAngle
|
||||
{
|
||||
get => m_MaxAngle;
|
||||
set => m_MaxAngle = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Angle of the lever in the 'off' position
|
||||
/// </summary>
|
||||
public float minAngle
|
||||
{
|
||||
get => m_MinAngle;
|
||||
set => m_MinAngle = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Events to trigger when the lever activates
|
||||
/// </summary>
|
||||
public UnityEvent onLeverActivate => m_OnLeverActivate;
|
||||
|
||||
/// <summary>
|
||||
/// Events to trigger when the lever deactivates
|
||||
/// </summary>
|
||||
public UnityEvent onLeverDeactivate => m_OnLeverDeactivate;
|
||||
|
||||
void Start()
|
||||
{
|
||||
SetValue(m_Value, true);
|
||||
}
|
||||
|
||||
protected override void OnEnable()
|
||||
{
|
||||
base.OnEnable();
|
||||
selectEntered.AddListener(StartGrab);
|
||||
selectExited.AddListener(EndGrab);
|
||||
}
|
||||
|
||||
protected override void OnDisable()
|
||||
{
|
||||
selectEntered.RemoveListener(StartGrab);
|
||||
selectExited.RemoveListener(EndGrab);
|
||||
base.OnDisable();
|
||||
}
|
||||
|
||||
void StartGrab(SelectEnterEventArgs args)
|
||||
{
|
||||
m_Interactor = args.interactorObject;
|
||||
}
|
||||
|
||||
void EndGrab(SelectExitEventArgs args)
|
||||
{
|
||||
SetValue(m_Value, true);
|
||||
m_Interactor = null;
|
||||
}
|
||||
|
||||
public override Transform GetAttachTransform(IXRInteractor interactor)
|
||||
{
|
||||
return m_InteractorSnapTransform;
|
||||
}
|
||||
|
||||
// public override Transform GetAttachTransform(IXRInteractor interactor)
|
||||
// {
|
||||
// return base.GetAttachTransform(interactor);
|
||||
// }
|
||||
public override void ProcessInteractable(XRInteractionUpdateOrder.UpdatePhase updatePhase)
|
||||
{
|
||||
base.ProcessInteractable(updatePhase);
|
||||
|
||||
if (updatePhase == XRInteractionUpdateOrder.UpdatePhase.Dynamic)
|
||||
{
|
||||
if (isSelected)
|
||||
{
|
||||
UpdateValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Vector3 GetLookDirection()
|
||||
{
|
||||
Vector3 direction = m_Interactor.GetAttachTransform(this).position - m_Handle.position;
|
||||
direction = transform.InverseTransformDirection(direction);
|
||||
direction.x = 0;
|
||||
|
||||
return direction.normalized;
|
||||
}
|
||||
|
||||
void UpdateValue()
|
||||
{
|
||||
var lookDirection = GetLookDirection();
|
||||
var lookAngle = Mathf.Atan2(lookDirection.z, lookDirection.y) * Mathf.Rad2Deg;
|
||||
|
||||
if (m_MinAngle < m_MaxAngle)
|
||||
lookAngle = Mathf.Clamp(lookAngle, m_MinAngle, m_MaxAngle);
|
||||
else
|
||||
lookAngle = Mathf.Clamp(lookAngle, m_MaxAngle, m_MinAngle);
|
||||
|
||||
var maxAngleDistance = Mathf.Abs(m_MaxAngle - lookAngle);
|
||||
var minAngleDistance = Mathf.Abs(m_MinAngle - lookAngle);
|
||||
|
||||
if (m_Value)
|
||||
maxAngleDistance *= (1.0f - k_LeverDeadZone);
|
||||
else
|
||||
minAngleDistance *= (1.0f - k_LeverDeadZone);
|
||||
|
||||
var newValue = (maxAngleDistance < minAngleDistance);
|
||||
|
||||
SetHandleAngle(lookAngle);
|
||||
|
||||
SetValue(newValue);
|
||||
}
|
||||
|
||||
void SetValue(bool isOn, bool forceRotation = false)
|
||||
{
|
||||
if (m_Value == isOn)
|
||||
{
|
||||
if (forceRotation)
|
||||
SetHandleAngle(m_Value ? m_MaxAngle : m_MinAngle);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
m_Value = isOn;
|
||||
|
||||
if (m_Value)
|
||||
{
|
||||
m_OnLeverActivate.Invoke();
|
||||
}
|
||||
else
|
||||
{
|
||||
m_OnLeverDeactivate.Invoke();
|
||||
}
|
||||
|
||||
if (!isSelected && (m_LockToValue || forceRotation))
|
||||
SetHandleAngle(m_Value ? m_MaxAngle : m_MinAngle);
|
||||
}
|
||||
|
||||
void SetHandleAngle(float angle)
|
||||
{
|
||||
if (m_Handle != null)
|
||||
m_Handle.localRotation = Quaternion.Euler(angle, 0.0f, 0.0f);
|
||||
}
|
||||
|
||||
void OnDrawGizmosSelected()
|
||||
{
|
||||
var angleStartPoint = transform.position;
|
||||
|
||||
if (m_Handle != null)
|
||||
angleStartPoint = m_Handle.position;
|
||||
|
||||
const float k_AngleLength = 0.25f;
|
||||
|
||||
var angleMaxPoint = angleStartPoint + transform.TransformDirection(Quaternion.Euler(m_MaxAngle, 0.0f, 0.0f) * Vector3.up) * k_AngleLength;
|
||||
var angleMinPoint = angleStartPoint + transform.TransformDirection(Quaternion.Euler(m_MinAngle, 0.0f, 0.0f) * Vector3.up) * k_AngleLength;
|
||||
|
||||
Gizmos.color = Color.green;
|
||||
Gizmos.DrawLine(angleStartPoint, angleMaxPoint);
|
||||
|
||||
Gizmos.color = Color.red;
|
||||
Gizmos.DrawLine(angleStartPoint, angleMinPoint);
|
||||
}
|
||||
|
||||
void OnValidate()
|
||||
{
|
||||
SetHandleAngle(m_Value ? m_MaxAngle : m_MinAngle);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e601cf28e9702c945abe3b90e7f51974
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,44 @@
|
||||
|
||||
|
||||
namespace UnityEngine.XR.Content.Interaction
|
||||
{
|
||||
/// <summary>
|
||||
/// Socket interactor that only selects and hovers interactables with a keychain component containing specific keys.
|
||||
/// </summary>
|
||||
public class XRLockSocketInteractor : UnityEngine.XR.Interaction.Toolkit.Interactors.XRSocketInteractor
|
||||
{
|
||||
[Space]
|
||||
[SerializeField]
|
||||
[Tooltip("The required keys to interact with this socket.")]
|
||||
Lock m_Lock;
|
||||
|
||||
/// <summary>
|
||||
/// The required keys to interact with this socket.
|
||||
/// </summary>
|
||||
public Lock keychainLock
|
||||
{
|
||||
get => m_Lock;
|
||||
set => m_Lock = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanHover(UnityEngine.XR.Interaction.Toolkit.Interactables.IXRHoverInteractable interactable)
|
||||
{
|
||||
if (!base.CanHover(interactable))
|
||||
return false;
|
||||
|
||||
var keyChain = interactable.transform.GetComponent<IKeychain>();
|
||||
return m_Lock.CanUnlock(keyChain);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanSelect(UnityEngine.XR.Interaction.Toolkit.Interactables.IXRSelectInteractable interactable)
|
||||
{
|
||||
if (!base.CanSelect(interactable))
|
||||
return false;
|
||||
|
||||
var keyChain = interactable.transform.GetComponent<IKeychain>();
|
||||
return m_Lock.CanUnlock(keyChain);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: eecc085bf63270540b2d9a418fb5f149
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,246 @@
|
||||
using Unity.Mathematics;
|
||||
using Unity.XR.CoreUtils.Bindings;
|
||||
using UnityEngine;
|
||||
using UnityEngine.XR.Interaction.Toolkit.AffordanceSystem.State;
|
||||
using UnityEngine.XR.Interaction.Toolkit.Filtering;
|
||||
using UnityEngine.XR.Interaction.Toolkit.Utilities.Tweenables.Primitives;
|
||||
|
||||
namespace XRMultiplayer
|
||||
{
|
||||
/// <summary>
|
||||
/// Follow animation affordance for <see cref="IPokeStateDataProvider"/>, such as <see cref="XRPokeFilter"/>.
|
||||
/// Used to animate a pressed transform, such as a button to follow the poke position.
|
||||
/// </summary>
|
||||
[AddComponentMenu("XR/XR Poke Follow Affordance Fill", 22)]
|
||||
public class XRPokeFollowAffordanceFill : MonoBehaviour
|
||||
{
|
||||
[SerializeField]
|
||||
[Tooltip("Transform that will move in the poke direction when this or a parent GameObject is poked." +
|
||||
"\nNote: Should be a direct child GameObject.")]
|
||||
Transform m_PokeFollowTransform;
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("Transform that will scale the mask when this interactable is poked.")]
|
||||
RectTransform m_PokeFill;
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("The max width size for the poke fill image when pressed")]
|
||||
float m_PokeFillMaxSizeX;
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("The max height size for the poke fill image when pressed")]
|
||||
float m_PokeFillMaxSizeY;
|
||||
|
||||
/// <summary>
|
||||
/// Transform that will animate along the axis of interaction when this interactable is poked.
|
||||
/// Note: Must be a direct child GameObject as it moves in local space relative to the poke target's transform.
|
||||
/// </summary>
|
||||
public Transform pokeFollowTransform
|
||||
{
|
||||
get => m_PokeFollowTransform;
|
||||
set => m_PokeFollowTransform = value;
|
||||
}
|
||||
|
||||
[SerializeField]
|
||||
[Range(0f, 20f)]
|
||||
[Tooltip("Multiplies transform position interpolation as a factor of Time.deltaTime. If 0, no smoothing will be applied.")]
|
||||
float m_SmoothingSpeed = 8f;
|
||||
|
||||
/// <summary>
|
||||
/// Multiplies transform position interpolation as a factor of <see cref="Time.deltaTime"/>. If <c>0</c>, no smoothing will be applied.
|
||||
/// </summary>
|
||||
public float smoothingSpeed
|
||||
{
|
||||
get => m_SmoothingSpeed;
|
||||
set => m_SmoothingSpeed = value;
|
||||
}
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("When this component is no longer the target of the poke, the Poke Follow Transform returns to the original position.")]
|
||||
bool m_ReturnToInitialPosition = true;
|
||||
|
||||
/// <summary>
|
||||
/// When this component is no longer the target of the poke, the <see cref="pokeFollowTransform"/> returns to the original position.
|
||||
/// </summary>
|
||||
public bool returnToInitialPosition
|
||||
{
|
||||
get => m_ReturnToInitialPosition;
|
||||
set => m_ReturnToInitialPosition = value;
|
||||
}
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("Whether to apply the follow animation if the target of the poke is a child of this transform. " +
|
||||
"This is useful for UI objects that may have child graphics.")]
|
||||
bool m_ApplyIfChildIsTarget = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to apply the follow animation if the target of the poke is a child of this transform.
|
||||
/// This is useful for UI objects that may have child graphics.
|
||||
/// </summary>
|
||||
public bool applyIfChildIsTarget
|
||||
{
|
||||
get => m_ApplyIfChildIsTarget;
|
||||
set => m_ApplyIfChildIsTarget = value;
|
||||
}
|
||||
|
||||
[Header("Distance Clamping")]
|
||||
[SerializeField]
|
||||
[Tooltip("Whether to keep the Poke Follow Transform from moving past a minimum distance from the poke target.")]
|
||||
bool m_ClampToMinDistance;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to keep the <see cref="pokeFollowTransform"/> from moving past <see cref="minDistance"/> from the poke target.
|
||||
/// </summary>
|
||||
public bool clampToMinDistance
|
||||
{
|
||||
get => m_ClampToMinDistance;
|
||||
set => m_ClampToMinDistance = value;
|
||||
}
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("The minimum distance from this transform that the Poke Follow Transform can move.")]
|
||||
float m_MinDistance;
|
||||
|
||||
/// <summary>
|
||||
/// The minimum distance from this transform that the <see cref="pokeFollowTransform"/> can move when
|
||||
/// <see cref="clampToMinDistance"/> is <see langword="true"/>.
|
||||
/// </summary>
|
||||
public float minDistance
|
||||
{
|
||||
get => m_MinDistance;
|
||||
set => m_MinDistance = value;
|
||||
}
|
||||
[Space]
|
||||
[SerializeField]
|
||||
[Tooltip("Whether to keep the Poke Follow Transform from moving past a maximum distance from the poke target.")]
|
||||
bool m_ClampToMaxDistance;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to keep the <see cref="pokeFollowTransform"/> from moving past <see cref="maxDistance"/> from the poke target.
|
||||
/// </summary>
|
||||
public bool clampToMaxDistance
|
||||
{
|
||||
get => m_ClampToMaxDistance;
|
||||
set => m_ClampToMaxDistance = value;
|
||||
}
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("The maximum distance from this transform that the Poke Follow Transform can move. Will shrink to the distance of initial position if that is smaller, or if this is 0.")]
|
||||
float m_MaxDistance;
|
||||
|
||||
/// <summary>
|
||||
/// The maximum distance from this transform that the <see cref="pokeFollowTransform"/> can move when
|
||||
/// <see cref="clampToMaxDistance"/> is <see langword="true"/>.
|
||||
/// </summary>
|
||||
public float maxDistance
|
||||
{
|
||||
get => m_MaxDistance;
|
||||
set => m_MaxDistance = value;
|
||||
}
|
||||
|
||||
IPokeStateDataProvider m_PokeDataProvider;
|
||||
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
readonly Vector3TweenableVariable m_TransformTweenableVariable = new Vector3TweenableVariable();
|
||||
readonly FloatTweenableVariable m_PokeStrengthTweenableVariable = new FloatTweenableVariable();
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
readonly BindingsGroup m_BindingsGroup = new BindingsGroup();
|
||||
Vector3 m_InitialPosition;
|
||||
bool m_IsFirstFrame;
|
||||
|
||||
/// <summary>
|
||||
/// See <see cref="MonoBehaviour"/>.
|
||||
/// </summary>
|
||||
protected void Awake()
|
||||
{
|
||||
m_PokeDataProvider = GetComponentInParent<IPokeStateDataProvider>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// See <see cref="MonoBehaviour"/>.
|
||||
/// </summary>
|
||||
protected void Start()
|
||||
{
|
||||
if (m_PokeFollowTransform != null)
|
||||
{
|
||||
m_InitialPosition = m_PokeFollowTransform.localPosition;
|
||||
m_MaxDistance = m_MaxDistance > 0f ? Mathf.Min(m_InitialPosition.magnitude, m_MaxDistance) : m_InitialPosition.magnitude;
|
||||
m_BindingsGroup.AddBinding(m_TransformTweenableVariable.Subscribe(OnTransformTweenableVariableUpdated));
|
||||
m_BindingsGroup.AddBinding(m_PokeStrengthTweenableVariable.Subscribe(OnPokeStrengthChanged));
|
||||
m_BindingsGroup.AddBinding(m_PokeDataProvider.pokeStateData.SubscribeAndUpdate(OnPokeStateDataUpdated));
|
||||
}
|
||||
else
|
||||
{
|
||||
enabled = false;
|
||||
Debug.LogWarning($"Missing Poke Follow Transform assignment on {this}. Disabling component.", this);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// See <see cref="MonoBehaviour"/>.
|
||||
/// </summary>
|
||||
protected void OnDestroy()
|
||||
{
|
||||
m_BindingsGroup.Clear();
|
||||
m_TransformTweenableVariable?.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// See <see cref="MonoBehaviour"/>.
|
||||
/// </summary>
|
||||
protected void LateUpdate()
|
||||
{
|
||||
if (m_IsFirstFrame)
|
||||
{
|
||||
m_TransformTweenableVariable.HandleTween(1f);
|
||||
m_PokeStrengthTweenableVariable.target = 0f;
|
||||
m_PokeStrengthTweenableVariable.HandleTween(1f);
|
||||
m_IsFirstFrame = false;
|
||||
return;
|
||||
}
|
||||
|
||||
float tweenAmt = m_SmoothingSpeed > 0f ? Time.deltaTime * m_SmoothingSpeed : 1f;
|
||||
m_TransformTweenableVariable.HandleTween(tweenAmt);
|
||||
m_PokeStrengthTweenableVariable.HandleTween(tweenAmt);
|
||||
}
|
||||
|
||||
void OnTransformTweenableVariableUpdated(float3 position)
|
||||
{
|
||||
m_PokeFollowTransform.localPosition = position;
|
||||
}
|
||||
|
||||
void OnPokeStrengthChanged(float newStrength)
|
||||
{
|
||||
var newX = m_PokeFillMaxSizeX * newStrength;
|
||||
var newY = m_PokeFillMaxSizeY * newStrength;
|
||||
m_PokeFill.sizeDelta = new Vector2(newX, newY);
|
||||
}
|
||||
|
||||
void OnPokeStateDataUpdated(PokeStateData data)
|
||||
{
|
||||
var pokeTarget = data.target;
|
||||
var applyFollow = m_ApplyIfChildIsTarget
|
||||
? pokeTarget != null && pokeTarget.IsChildOf(transform)
|
||||
: pokeTarget == transform;
|
||||
|
||||
if (applyFollow)
|
||||
{
|
||||
var targetPosition = pokeTarget.InverseTransformPoint(data.axisAlignedPokeInteractionPoint);
|
||||
|
||||
if (m_ClampToMinDistance && targetPosition.sqrMagnitude < m_MinDistance * m_MinDistance)
|
||||
targetPosition = Vector3.ClampMagnitude(targetPosition, m_MinDistance);
|
||||
|
||||
if (m_ClampToMaxDistance && targetPosition.sqrMagnitude > m_MaxDistance * m_MaxDistance)
|
||||
targetPosition = Vector3.ClampMagnitude(targetPosition, m_MaxDistance);
|
||||
|
||||
m_TransformTweenableVariable.target = targetPosition;
|
||||
m_PokeStrengthTweenableVariable.target = Mathf.Clamp01(data.interactionStrength);
|
||||
}
|
||||
else if (m_ReturnToInitialPosition)
|
||||
{
|
||||
m_TransformTweenableVariable.target = m_InitialPosition;
|
||||
m_PokeStrengthTweenableVariable.target = 0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0908100b30fe0ab4191734ae3261431f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4207f4760e9c6d740b8356c66af83137
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user