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