|
|
@@ -0,0 +1,358 @@
|
|
|
+using UdonSharp;
|
|
|
+using UnityEngine;
|
|
|
+using VRC.SDKBase;
|
|
|
+using VRC.Udon;
|
|
|
+using VRC.Udon.Common.Interfaces;
|
|
|
+
|
|
|
+[UdonBehaviourSyncMode(BehaviourSyncMode.Manual)]
|
|
|
+[DisallowMultipleComponent]
|
|
|
+[AddComponentMenu("Artyom Scripting System/Linked Object System")]
|
|
|
+public class LinkedObjectSystem : UdonSharpBehaviour
|
|
|
+{
|
|
|
+ [Header("Script Settings\n")]
|
|
|
+ [Tooltip("Print all variables to console for debug purposes")]
|
|
|
+ public bool debugMode;
|
|
|
+ [Tooltip("How long a Temporary interaction stays on, in seconds. Ignore if not needed")]
|
|
|
+ public float delayedInteractTime = 0f;
|
|
|
+ [Tooltip("Objects on these layers don't count for the Object trigger")]
|
|
|
+ public LayerMask excludeLayers;
|
|
|
+
|
|
|
+ [Header("Interactions Settings\n")]
|
|
|
+ [Tooltip("Who keeps the output on while inside this trigger")]
|
|
|
+ public TriggerType triggerType = TriggerType.None;
|
|
|
+ [Tooltip("What interacting does. Synced: everyone sees it, including players who join later")]
|
|
|
+ public InteractionType interactionType = InteractionType.None;
|
|
|
+
|
|
|
+ [Header("Animator Links (bool only)\n")]
|
|
|
+ public Animator[] outputAnimators;
|
|
|
+ public string[] outputParameterNames;
|
|
|
+ [Space]
|
|
|
+ public GameObject audioObjectTrue;
|
|
|
+ public GameObject audioObjectFalse;
|
|
|
+
|
|
|
+ [Header("Script Links\n")]
|
|
|
+ [Tooltip("Udon scripts told when the output turns on or off")]
|
|
|
+ public UdonBehaviour[] outputBehaviours;
|
|
|
+ [Tooltip("For each script, the event called when the output turns on. Leave an entry empty for none")]
|
|
|
+ public string[] onEventNames;
|
|
|
+ [Tooltip("For each script, the event called when the output turns off. Leave an entry empty for none")]
|
|
|
+ public string[] offEventNames;
|
|
|
+ [Tooltip("Off: every player's client calls the events itself. On: only this object's owner calls them, as network " +
|
|
|
+ "events to everyone (players who join later don't get them, and event names can't start with _)")]
|
|
|
+ public bool networkEvents;
|
|
|
+
|
|
|
+ [Header("Station\n")]
|
|
|
+ [Tooltip("The player who turns this on by interacting, or who walks into the trigger, is seated here. They leave it themselves")]
|
|
|
+ public VRC.SDK3.Components.VRCStation station; // Named in full: VRC.SDKBase has a VRCStation too
|
|
|
+
|
|
|
+ // PRIVATE VARS
|
|
|
+
|
|
|
+ // Interactions only happen on the interacting player's client, so their result is synced.
|
|
|
+ // Triggers fire on every client for every player and synced pickup, so those are tracked locally.
|
|
|
+ [UdonSynced]
|
|
|
+ private bool interactState;
|
|
|
+
|
|
|
+ private bool playerTriggerEnabled = false;
|
|
|
+ private bool pickupTriggerEnabled = false;
|
|
|
+ private int[] playersInside = new int[100]; // Player IDs: a player can't be counted twice, and leaving the instance removes them
|
|
|
+ private int playerCount;
|
|
|
+ private bool playerCheckScheduled;
|
|
|
+ private Collider triggerCollider;
|
|
|
+ private Collider[] pickupsInside = new Collider[32];
|
|
|
+ private int pickupCount;
|
|
|
+ private bool pickupCheckScheduled;
|
|
|
+ private bool output; // What the animators and audio objects show now
|
|
|
+
|
|
|
+ void Start()
|
|
|
+ {
|
|
|
+ playerTriggerEnabled = triggerType == TriggerType.Player || triggerType == TriggerType.BothPlayerAndObject;
|
|
|
+ pickupTriggerEnabled = triggerType == TriggerType.Object || triggerType == TriggerType.BothPlayerAndObject;
|
|
|
+ triggerCollider = GetComponent<Collider>();
|
|
|
+ if (interactionType == InteractionType.None)
|
|
|
+ {
|
|
|
+ DisableInteractive = true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // ------------------------------------------------------------ interactions
|
|
|
+
|
|
|
+ public override void Interact()
|
|
|
+ {
|
|
|
+ if (interactionType == InteractionType.Permanent)
|
|
|
+ {
|
|
|
+ if (interactState) return;
|
|
|
+ SetInteractState(true);
|
|
|
+ SeatLocalPlayer();
|
|
|
+ }
|
|
|
+ else if (interactionType == InteractionType.Toggle)
|
|
|
+ {
|
|
|
+ SetInteractState(!interactState);
|
|
|
+ if (interactState) SeatLocalPlayer();
|
|
|
+ }
|
|
|
+ else if (interactionType == InteractionType.Temporary)
|
|
|
+ {
|
|
|
+ if (interactState) return;
|
|
|
+ SetInteractState(true);
|
|
|
+ SeatLocalPlayer();
|
|
|
+ SendCustomEventDelayedSeconds(nameof(DisableTemp), delayedInteractTime);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>Ends a Temporary interaction (run by whoever owns this object when the time is up).</summary>
|
|
|
+ public void DisableTemp()
|
|
|
+ {
|
|
|
+ if (!interactState || !Networking.IsOwner(gameObject)) return;
|
|
|
+ SetInteractState(false);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void SetInteractState(bool state)
|
|
|
+ {
|
|
|
+ if (!Networking.IsOwner(gameObject)) Networking.SetOwner(Networking.LocalPlayer, gameObject);
|
|
|
+ interactState = state;
|
|
|
+ RequestSerialization();
|
|
|
+ if (debugMode) Debug.Log("Linked Object System: interaction set to " + state.ToString());
|
|
|
+ ApplyInteractState();
|
|
|
+ }
|
|
|
+
|
|
|
+ public override void OnDeserialization()
|
|
|
+ {
|
|
|
+ if (debugMode) Debug.Log("Linked Object System: interaction received as " + interactState.ToString());
|
|
|
+ ApplyInteractState();
|
|
|
+ }
|
|
|
+
|
|
|
+ public override void OnOwnershipTransferred(VRCPlayerApi player)
|
|
|
+ {
|
|
|
+ // The owner left mid-way through a Temporary interaction: the new owner ends it
|
|
|
+ if (player.isLocal && interactionType == InteractionType.Temporary && interactState)
|
|
|
+ SendCustomEventDelayedSeconds(nameof(DisableTemp), delayedInteractTime);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void ApplyInteractState()
|
|
|
+ {
|
|
|
+ // A Temporary interaction can't be used again until it's over
|
|
|
+ if (interactionType == InteractionType.Temporary) DisableInteractive = interactState;
|
|
|
+ UpdateOutput();
|
|
|
+ }
|
|
|
+
|
|
|
+ // ------------------------------------------------------------ player trigger
|
|
|
+
|
|
|
+ public override void OnPlayerTriggerEnter(VRCPlayerApi player)
|
|
|
+ {
|
|
|
+ if (!playerTriggerEnabled || !Utilities.IsValid(player)) return;
|
|
|
+ int id = player.playerId;
|
|
|
+ if (IndexOfPlayer(id) >= 0 || playerCount == playersInside.Length) return;
|
|
|
+ playersInside[playerCount] = id;
|
|
|
+ playerCount++;
|
|
|
+ if (debugMode) Debug.Log("Linked Object System: players inside: " + playerCount.ToString());
|
|
|
+ if (!playerCheckScheduled)
|
|
|
+ {
|
|
|
+ playerCheckScheduled = true;
|
|
|
+ SendCustomEventDelayedSeconds(nameof(_CheckPlayers), 1f);
|
|
|
+ }
|
|
|
+ UpdateOutput();
|
|
|
+ if (player.isLocal) SeatLocalPlayer();
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Drops players who left without an exit: seated in a station elsewhere, or gone from the instance. Runs every
|
|
|
+ /// second while any player is inside.
|
|
|
+ /// </summary>
|
|
|
+ public void _CheckPlayers()
|
|
|
+ {
|
|
|
+ playerCheckScheduled = false;
|
|
|
+ for (int i = playerCount - 1; i >= 0; i--)
|
|
|
+ {
|
|
|
+ VRCPlayerApi player = VRCPlayerApi.GetPlayerById(playersInside[i]);
|
|
|
+ if (!Utilities.IsValid(player) || !NearTrigger(player)) RemovePlayer(playersInside[i]);
|
|
|
+ }
|
|
|
+ if (playerCount > 0)
|
|
|
+ {
|
|
|
+ playerCheckScheduled = true;
|
|
|
+ SendCustomEventDelayedSeconds(nameof(_CheckPlayers), 1f);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Whether the player's feet or head are within a metre of the trigger's bounds. Generous on purpose: a player
|
|
|
+ /// dropped here gets no new enter until they walk out and back in.
|
|
|
+ /// </summary>
|
|
|
+ private bool NearTrigger(VRCPlayerApi player)
|
|
|
+ {
|
|
|
+ if (triggerCollider == null) return true;
|
|
|
+ Bounds bounds = triggerCollider.bounds;
|
|
|
+ Vector3 center = bounds.center;
|
|
|
+ Vector3 reach = bounds.extents + Vector3.one;
|
|
|
+ return Within(player.GetPosition(), center, reach) ||
|
|
|
+ Within(player.GetTrackingData(VRCPlayerApi.TrackingDataType.Head).position, center, reach);
|
|
|
+ }
|
|
|
+
|
|
|
+ private bool Within(Vector3 point, Vector3 center, Vector3 reach)
|
|
|
+ {
|
|
|
+ Vector3 d = point - center;
|
|
|
+ return Mathf.Abs(d.x) <= reach.x && Mathf.Abs(d.y) <= reach.y && Mathf.Abs(d.z) <= reach.z;
|
|
|
+ }
|
|
|
+
|
|
|
+ public override void OnPlayerTriggerExit(VRCPlayerApi player)
|
|
|
+ {
|
|
|
+ if (!playerTriggerEnabled || player == null) return;
|
|
|
+ RemovePlayer(player.playerId);
|
|
|
+ }
|
|
|
+
|
|
|
+ public override void OnPlayerLeft(VRCPlayerApi player)
|
|
|
+ {
|
|
|
+ // Leaving the instance doesn't always count as leaving the trigger
|
|
|
+ if (!playerTriggerEnabled || player == null) return;
|
|
|
+ RemovePlayer(player.playerId);
|
|
|
+ }
|
|
|
+
|
|
|
+ private int IndexOfPlayer(int id)
|
|
|
+ {
|
|
|
+ for (int i = 0; i < playerCount; i++)
|
|
|
+ if (playersInside[i] == id) return i;
|
|
|
+ return -1;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void RemovePlayer(int id)
|
|
|
+ {
|
|
|
+ int index = IndexOfPlayer(id);
|
|
|
+ if (index < 0) return;
|
|
|
+ playerCount--;
|
|
|
+ playersInside[index] = playersInside[playerCount];
|
|
|
+ if (debugMode) Debug.Log("Linked Object System: players inside: " + playerCount.ToString());
|
|
|
+ UpdateOutput();
|
|
|
+ }
|
|
|
+
|
|
|
+ // ------------------------------------------------------------ object trigger
|
|
|
+
|
|
|
+ private void OnTriggerEnter(Collider other)
|
|
|
+ {
|
|
|
+ if (!pickupTriggerEnabled || !CountsAsPickup(other)) return;
|
|
|
+ if (IndexOfPickup(other) >= 0 || pickupCount == pickupsInside.Length) return;
|
|
|
+ pickupsInside[pickupCount] = other;
|
|
|
+ pickupCount++;
|
|
|
+ if (debugMode) Debug.Log("Linked Object System: pickups inside: " + pickupCount.ToString());
|
|
|
+ if (!pickupCheckScheduled)
|
|
|
+ {
|
|
|
+ pickupCheckScheduled = true;
|
|
|
+ SendCustomEventDelayedSeconds(nameof(_CheckPickups), 1f);
|
|
|
+ }
|
|
|
+ UpdateOutput();
|
|
|
+ }
|
|
|
+
|
|
|
+ private void OnTriggerExit(Collider other)
|
|
|
+ {
|
|
|
+ if (!pickupTriggerEnabled || other == null) return;
|
|
|
+ int index = IndexOfPickup(other);
|
|
|
+ if (index < 0) return;
|
|
|
+ RemovePickupAt(index);
|
|
|
+ if (debugMode) Debug.Log("Linked Object System: pickups inside: " + pickupCount.ToString());
|
|
|
+ UpdateOutput();
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Drops pickups that were disabled or destroyed while inside: those never get an exit. Runs every second
|
|
|
+ /// while any pickup is inside.
|
|
|
+ /// </summary>
|
|
|
+ public void _CheckPickups()
|
|
|
+ {
|
|
|
+ pickupCheckScheduled = false;
|
|
|
+ for (int i = pickupCount - 1; i >= 0; i--)
|
|
|
+ {
|
|
|
+ Collider pickup = pickupsInside[i];
|
|
|
+ if (!Utilities.IsValid(pickup) || !pickup.enabled || !pickup.gameObject.activeInHierarchy) RemovePickupAt(i);
|
|
|
+ }
|
|
|
+ UpdateOutput();
|
|
|
+ if (pickupCount > 0)
|
|
|
+ {
|
|
|
+ pickupCheckScheduled = true;
|
|
|
+ SendCustomEventDelayedSeconds(nameof(_CheckPickups), 1f);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private bool CountsAsPickup(Collider other)
|
|
|
+ {
|
|
|
+ if (other == null) return false;
|
|
|
+ if ((excludeLayers.value & (1 << other.gameObject.layer)) != 0) return false;
|
|
|
+ return other.GetComponent<VRC_Pickup>() != null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private int IndexOfPickup(Collider other)
|
|
|
+ {
|
|
|
+ for (int i = 0; i < pickupCount; i++)
|
|
|
+ if (pickupsInside[i] == other) return i;
|
|
|
+ return -1;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void RemovePickupAt(int index)
|
|
|
+ {
|
|
|
+ pickupCount--;
|
|
|
+ pickupsInside[index] = pickupsInside[pickupCount];
|
|
|
+ pickupsInside[pickupCount] = null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // ------------------------------------------------------------ output
|
|
|
+
|
|
|
+ /// <summary>On while the interaction is on, or anyone or anything counted is inside the trigger.</summary>
|
|
|
+ private void UpdateOutput()
|
|
|
+ {
|
|
|
+ bool on = interactState || playerCount > 0 || pickupCount > 0;
|
|
|
+ if (on == output) return;
|
|
|
+ output = on;
|
|
|
+ UpdateAnims(on);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void UpdateAnims(bool state)
|
|
|
+ {
|
|
|
+ int count = Mathf.Min(outputAnimators.Length, outputParameterNames.Length);
|
|
|
+ for (int i = 0; i < count; i++)
|
|
|
+ {
|
|
|
+ if (outputAnimators[i] != null && !string.IsNullOrEmpty(outputParameterNames[i]))
|
|
|
+ outputAnimators[i].SetBool(outputParameterNames[i], state);
|
|
|
+ }
|
|
|
+
|
|
|
+ // Enable or disable GameObjects based on the state
|
|
|
+ if (debugMode) Debug.Log("Linked Object System: GameObject set audio to " + state.ToString());
|
|
|
+ if (audioObjectTrue != null) audioObjectTrue.SetActive(state);
|
|
|
+ if (audioObjectFalse != null) audioObjectFalse.SetActive(!state);
|
|
|
+
|
|
|
+ SendEvents(state);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void SendEvents(bool state)
|
|
|
+ {
|
|
|
+ // Networked: the owner tells everyone, so the others mustn't send it again
|
|
|
+ if (networkEvents && !Networking.IsOwner(gameObject)) return;
|
|
|
+ string[] names = state ? onEventNames : offEventNames;
|
|
|
+ int count = Mathf.Min(outputBehaviours.Length, names.Length);
|
|
|
+ for (int i = 0; i < count; i++)
|
|
|
+ {
|
|
|
+ if (outputBehaviours[i] == null || string.IsNullOrEmpty(names[i])) continue;
|
|
|
+ if (debugMode) Debug.Log("Linked Object System: sending " + names[i] + (networkEvents ? " to everyone" : ""));
|
|
|
+ if (networkEvents) outputBehaviours[i].SendCustomNetworkEvent(NetworkEventTarget.All, names[i]);
|
|
|
+ else outputBehaviours[i].SendCustomEvent(names[i]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>Seats the local player in the station, if there is one: they turned this on, or walked into the trigger.</summary>
|
|
|
+ private void SeatLocalPlayer()
|
|
|
+ {
|
|
|
+ if (station == null) return;
|
|
|
+ if (debugMode) Debug.Log("Linked Object System: seating the local player");
|
|
|
+ station.UseStation(Networking.LocalPlayer);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+public enum InteractionType
|
|
|
+{
|
|
|
+ None,
|
|
|
+ Permanent,
|
|
|
+ Temporary,
|
|
|
+ Toggle
|
|
|
+}
|
|
|
+public enum TriggerType
|
|
|
+{
|
|
|
+ None,
|
|
|
+ Player,
|
|
|
+ Object,
|
|
|
+ BothPlayerAndObject
|
|
|
+}
|