LinkedObjectSystem.cs 11 KB

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