LinkedObjectSystem.cs 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  1. using UdonSharp;
  2. using UnityEngine;
  3. using VRC.SDKBase;
  4. [UdonBehaviourSyncMode(BehaviourSyncMode.Manual)]
  5. [DisallowMultipleComponent]
  6. [AddComponentMenu("Artyom Scripting System/Linked Object System")]
  7. public class LinkedObjectSystem : UdonSharpBehaviour
  8. {
  9. [Header("Script Settings\n")]
  10. [Tooltip("Print all variables to console for debug purposes")]
  11. public bool debugMode;
  12. [Tooltip("How long a Temporary interaction stays on, in seconds. Ignore if not needed")]
  13. public float delayedInteractTime = 0f;
  14. [Tooltip("Objects on these layers don't count for the Object trigger")]
  15. public LayerMask excludeLayers;
  16. [Header("Interactions Settings\n")]
  17. [Tooltip("Who keeps the output on while inside this trigger")]
  18. public TriggerType triggerType = TriggerType.None;
  19. [Tooltip("What interacting does. Synced: everyone sees it, including players who join later")]
  20. public InteractionType interactionType = InteractionType.None;
  21. [Header("Animator Links (bool only)\n")]
  22. public Animator[] outputAnimators;
  23. public string[] outputParameterNames;
  24. [Space]
  25. public GameObject audioObjectTrue;
  26. public GameObject audioObjectFalse;
  27. // PRIVATE VARS
  28. // Interactions only happen on the interacting player's client, so their result is synced.
  29. // Triggers fire on every client for every player and synced pickup, so those are tracked locally.
  30. [UdonSynced]
  31. private bool interactState;
  32. private bool playerTriggerEnabled = false;
  33. private bool pickupTriggerEnabled = false;
  34. private int[] playersInside = new int[100]; // Player IDs: a player can't be counted twice, and leaving the instance removes them
  35. private int playerCount;
  36. private Collider[] pickupsInside = new Collider[32];
  37. private int pickupCount;
  38. private bool pickupCheckScheduled;
  39. private bool output; // What the animators and audio objects show now
  40. void Start()
  41. {
  42. playerTriggerEnabled = triggerType == TriggerType.Player || triggerType == TriggerType.BothPlayerAndObject;
  43. pickupTriggerEnabled = triggerType == TriggerType.Object || triggerType == TriggerType.BothPlayerAndObject;
  44. if (interactionType == InteractionType.None)
  45. {
  46. DisableInteractive = true;
  47. }
  48. }
  49. // ------------------------------------------------------------ interactions
  50. public override void Interact()
  51. {
  52. if (interactionType == InteractionType.Permanent)
  53. {
  54. if (!interactState) SetInteractState(true);
  55. }
  56. else if (interactionType == InteractionType.Toggle)
  57. {
  58. SetInteractState(!interactState);
  59. }
  60. else if (interactionType == InteractionType.Temporary)
  61. {
  62. if (interactState) return;
  63. SetInteractState(true);
  64. SendCustomEventDelayedSeconds(nameof(DisableTemp), delayedInteractTime);
  65. }
  66. }
  67. /// <summary>Ends a Temporary interaction (run by whoever owns this object when the time is up).</summary>
  68. public void DisableTemp()
  69. {
  70. if (!interactState || !Networking.IsOwner(gameObject)) return;
  71. SetInteractState(false);
  72. }
  73. private void SetInteractState(bool state)
  74. {
  75. if (!Networking.IsOwner(gameObject)) Networking.SetOwner(Networking.LocalPlayer, gameObject);
  76. interactState = state;
  77. RequestSerialization();
  78. if (debugMode) Debug.Log("Linked Object System: interaction set to " + state.ToString());
  79. ApplyInteractState();
  80. }
  81. public override void OnDeserialization()
  82. {
  83. if (debugMode) Debug.Log("Linked Object System: interaction received as " + interactState.ToString());
  84. ApplyInteractState();
  85. }
  86. public override void OnOwnershipTransferred(VRCPlayerApi player)
  87. {
  88. // The owner left mid-way through a Temporary interaction: the new owner ends it
  89. if (player.isLocal && interactionType == InteractionType.Temporary && interactState)
  90. SendCustomEventDelayedSeconds(nameof(DisableTemp), delayedInteractTime);
  91. }
  92. private void ApplyInteractState()
  93. {
  94. // A Temporary interaction can't be used again until it's over
  95. if (interactionType == InteractionType.Temporary) DisableInteractive = interactState;
  96. UpdateOutput();
  97. }
  98. // ------------------------------------------------------------ player trigger
  99. public override void OnPlayerTriggerEnter(VRCPlayerApi player)
  100. {
  101. if (!playerTriggerEnabled || !Utilities.IsValid(player)) return;
  102. int id = player.playerId;
  103. if (IndexOfPlayer(id) >= 0 || playerCount == playersInside.Length) return;
  104. playersInside[playerCount] = id;
  105. playerCount++;
  106. if (debugMode) Debug.Log("Linked Object System: players inside: " + playerCount.ToString());
  107. UpdateOutput();
  108. }
  109. public override void OnPlayerTriggerExit(VRCPlayerApi player)
  110. {
  111. if (!playerTriggerEnabled || player == null) return;
  112. RemovePlayer(player.playerId);
  113. }
  114. public override void OnPlayerLeft(VRCPlayerApi player)
  115. {
  116. // Leaving the instance doesn't always count as leaving the trigger
  117. if (!playerTriggerEnabled || player == null) return;
  118. RemovePlayer(player.playerId);
  119. }
  120. private int IndexOfPlayer(int id)
  121. {
  122. for (int i = 0; i < playerCount; i++)
  123. if (playersInside[i] == id) return i;
  124. return -1;
  125. }
  126. private void RemovePlayer(int id)
  127. {
  128. int index = IndexOfPlayer(id);
  129. if (index < 0) return;
  130. playerCount--;
  131. playersInside[index] = playersInside[playerCount];
  132. if (debugMode) Debug.Log("Linked Object System: players inside: " + playerCount.ToString());
  133. UpdateOutput();
  134. }
  135. // ------------------------------------------------------------ object trigger
  136. private void OnTriggerEnter(Collider other)
  137. {
  138. if (!pickupTriggerEnabled || !CountsAsPickup(other)) return;
  139. if (IndexOfPickup(other) >= 0 || pickupCount == pickupsInside.Length) return;
  140. pickupsInside[pickupCount] = other;
  141. pickupCount++;
  142. if (debugMode) Debug.Log("Linked Object System: pickups inside: " + pickupCount.ToString());
  143. if (!pickupCheckScheduled)
  144. {
  145. pickupCheckScheduled = true;
  146. SendCustomEventDelayedSeconds(nameof(_CheckPickups), 1f);
  147. }
  148. UpdateOutput();
  149. }
  150. private void OnTriggerExit(Collider other)
  151. {
  152. if (!pickupTriggerEnabled || other == null) return;
  153. int index = IndexOfPickup(other);
  154. if (index < 0) return;
  155. RemovePickupAt(index);
  156. if (debugMode) Debug.Log("Linked Object System: pickups inside: " + pickupCount.ToString());
  157. UpdateOutput();
  158. }
  159. /// <summary>
  160. /// Drops pickups that were disabled or destroyed while inside: those never get an exit. Runs every second
  161. /// while any pickup is inside.
  162. /// </summary>
  163. public void _CheckPickups()
  164. {
  165. pickupCheckScheduled = false;
  166. for (int i = pickupCount - 1; i >= 0; i--)
  167. {
  168. Collider pickup = pickupsInside[i];
  169. if (!Utilities.IsValid(pickup) || !pickup.enabled || !pickup.gameObject.activeInHierarchy) RemovePickupAt(i);
  170. }
  171. UpdateOutput();
  172. if (pickupCount > 0)
  173. {
  174. pickupCheckScheduled = true;
  175. SendCustomEventDelayedSeconds(nameof(_CheckPickups), 1f);
  176. }
  177. }
  178. private bool CountsAsPickup(Collider other)
  179. {
  180. if (other == null) return false;
  181. if ((excludeLayers.value & (1 << other.gameObject.layer)) != 0) return false;
  182. return other.GetComponent<VRC_Pickup>() != null;
  183. }
  184. private int IndexOfPickup(Collider other)
  185. {
  186. for (int i = 0; i < pickupCount; i++)
  187. if (pickupsInside[i] == other) return i;
  188. return -1;
  189. }
  190. private void RemovePickupAt(int index)
  191. {
  192. pickupCount--;
  193. pickupsInside[index] = pickupsInside[pickupCount];
  194. pickupsInside[pickupCount] = null;
  195. }
  196. // ------------------------------------------------------------ output
  197. /// <summary>On while the interaction is on, or anyone or anything counted is inside the trigger.</summary>
  198. private void UpdateOutput()
  199. {
  200. bool on = interactState || playerCount > 0 || pickupCount > 0;
  201. if (on == output) return;
  202. output = on;
  203. UpdateAnims(on);
  204. }
  205. private void UpdateAnims(bool state)
  206. {
  207. int count = Mathf.Min(outputAnimators.Length, outputParameterNames.Length);
  208. for (int i = 0; i < count; i++)
  209. {
  210. if (outputAnimators[i] != null && !string.IsNullOrEmpty(outputParameterNames[i]))
  211. outputAnimators[i].SetBool(outputParameterNames[i], state);
  212. }
  213. // Enable or disable GameObjects based on the state
  214. if (debugMode) Debug.Log("Linked Object System: GameObject set audio to " + state.ToString());
  215. if (audioObjectTrue != null) audioObjectTrue.SetActive(state);
  216. if (audioObjectFalse != null) audioObjectFalse.SetActive(!state);
  217. }
  218. }
  219. public enum InteractionType
  220. {
  221. None,
  222. Permanent,
  223. Temporary,
  224. Toggle
  225. }
  226. public enum TriggerType
  227. {
  228. None,
  229. Player,
  230. Object,
  231. BothPlayerAndObject
  232. }