| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312 |
- using UdonSharp;
- using UnityEngine;
- using VRC.SDK3.Components;
- 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 VRCStation station;
- // 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 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;
- 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());
- UpdateOutput();
- if (player.isLocal) SeatLocalPlayer();
- }
- 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
- }
|