LinkedObjectSystem.cs 13 KB

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