|
|
@@ -2,6 +2,7 @@ using UdonSharp;
|
|
|
using UnityEngine;
|
|
|
using VRC.SDKBase;
|
|
|
|
|
|
+[UdonBehaviourSyncMode(BehaviourSyncMode.Manual)]
|
|
|
[DisallowMultipleComponent]
|
|
|
[AddComponentMenu("Artyom Scripting System/Linked Object System")]
|
|
|
public class LinkedObjectSystem : UdonSharpBehaviour
|
|
|
@@ -9,197 +10,241 @@ public class LinkedObjectSystem : UdonSharpBehaviour
|
|
|
[Header("Script Settings\n")]
|
|
|
[Tooltip("Print all variables to console for debug purposes")]
|
|
|
public bool debugMode;
|
|
|
- [Tooltip("Delay for Temporary interact, ignore if not needed")]
|
|
|
+ [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("Trigger behavior type")]
|
|
|
-
|
|
|
|
|
|
+ [Header("Interactions Settings\n")]
|
|
|
+ [Tooltip("Who keeps the output on while inside this trigger")]
|
|
|
public TriggerType triggerType = TriggerType.None;
|
|
|
- [Tooltip("Interact via select type")]
|
|
|
+ [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;
|
|
|
+ public string[] outputParameterNames;
|
|
|
[Space]
|
|
|
public GameObject audioObjectTrue;
|
|
|
public GameObject audioObjectFalse;
|
|
|
+
|
|
|
// 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;
|
|
|
-
|
|
|
- [UdonSynced]
|
|
|
- private int isOccupied;
|
|
|
- private float timer;
|
|
|
+ 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()
|
|
|
{
|
|
|
- isOccupied = 0;
|
|
|
- if (triggerType == TriggerType.BothPlayerAndObject)
|
|
|
+ playerTriggerEnabled = triggerType == TriggerType.Player || triggerType == TriggerType.BothPlayerAndObject;
|
|
|
+ pickupTriggerEnabled = triggerType == TriggerType.Object || triggerType == TriggerType.BothPlayerAndObject;
|
|
|
+ if (interactionType == InteractionType.None)
|
|
|
{
|
|
|
- playerTriggerEnabled = true;
|
|
|
- pickupTriggerEnabled = true;
|
|
|
+ DisableInteractive = true;
|
|
|
}
|
|
|
- else if (triggerType == TriggerType.Player)
|
|
|
+ }
|
|
|
+
|
|
|
+ // ------------------------------------------------------------ interactions
|
|
|
+
|
|
|
+ public override void Interact()
|
|
|
+ {
|
|
|
+ if (interactionType == InteractionType.Permanent)
|
|
|
{
|
|
|
- playerTriggerEnabled = true;
|
|
|
- pickupTriggerEnabled = false;
|
|
|
+ if (!interactState) SetInteractState(true);
|
|
|
}
|
|
|
- else if (triggerType == TriggerType.Object)
|
|
|
+ else if (interactionType == InteractionType.Toggle)
|
|
|
{
|
|
|
- playerTriggerEnabled = false;
|
|
|
- pickupTriggerEnabled = true;
|
|
|
+ SetInteractState(!interactState);
|
|
|
}
|
|
|
- if (interactionType == InteractionType.None)
|
|
|
+ else if (interactionType == InteractionType.Temporary)
|
|
|
{
|
|
|
- DisableInteractive = true;
|
|
|
+ if (interactState) return;
|
|
|
+ SetInteractState(true);
|
|
|
+ SendCustomEventDelayedSeconds(nameof(DisableTemp), delayedInteractTime);
|
|
|
}
|
|
|
}
|
|
|
- private void Update()
|
|
|
+
|
|
|
+ /// <summary>Ends a Temporary interaction (run by whoever owns this object when the time is up).</summary>
|
|
|
+ public void DisableTemp()
|
|
|
{
|
|
|
- if (interactionType == InteractionType.Temporary && isOccupied == 1)
|
|
|
- {
|
|
|
- if (debugMode) Debug.Log("Linked Object System: Interaction Type Checked As Temprary");
|
|
|
- if (timer < delayedInteractTime)
|
|
|
- {
|
|
|
- isOccupied = 1;
|
|
|
- if (debugMode) Debug.LogWarning("LOS: Logged isOccupied as " + isOccupied.ToString());
|
|
|
- timer += Time.deltaTime;
|
|
|
- if (debugMode) Debug.Log("Linked Object System: DeltaTime Update, New Value -> " + timer.ToString());
|
|
|
- }
|
|
|
- else if (timer >= delayedInteractTime)
|
|
|
- {
|
|
|
- isOccupied = 0;
|
|
|
- if (debugMode) Debug.LogWarning("LOS: Logged isOccupied as " + isOccupied.ToString());
|
|
|
- UpdateAnims(false);
|
|
|
- timer = 0f;
|
|
|
- }
|
|
|
- }
|
|
|
+ if (!interactState || !Networking.IsOwner(gameObject)) return;
|
|
|
+ SetInteractState(false);
|
|
|
}
|
|
|
-
|
|
|
- private void UpdateAnims(bool state)
|
|
|
+
|
|
|
+ private void SetInteractState(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);
|
|
|
- }
|
|
|
+ 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();
|
|
|
+ }
|
|
|
|
|
|
- // Enable or disable GameObjects based on the state
|
|
|
- if (audioObjectFalse != null && audioObjectTrue != null)
|
|
|
- {
|
|
|
- if (!state)
|
|
|
- {
|
|
|
- if (debugMode) Debug.Log("Linked Object System: GameObject set audio to false");
|
|
|
- audioObjectFalse.SetActive(true);
|
|
|
- audioObjectTrue.SetActive(false);
|
|
|
- }
|
|
|
- else
|
|
|
- {
|
|
|
- if (debugMode) Debug.Log("Linked Object System: GameObject set audio to true");
|
|
|
- audioObjectTrue.SetActive(true);
|
|
|
- audioObjectFalse.SetActive(false);
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- private void OnTriggerEnter(Collider other)
|
|
|
+ public override void OnDeserialization()
|
|
|
{
|
|
|
- if (pickupTriggerEnabled && other.GetComponent<VRC_Pickup>())
|
|
|
- {
|
|
|
- isOccupied++;
|
|
|
- if (debugMode) Debug.Log("Linked Object System: Pickup set trigger to " + isOccupied.ToString());
|
|
|
- if (isOccupied > 0 && isOccupied <=2)
|
|
|
- {
|
|
|
- UpdateAnims(true);
|
|
|
- }
|
|
|
- }
|
|
|
+ if (debugMode) Debug.Log("Linked Object System: interaction received as " + interactState.ToString());
|
|
|
+ ApplyInteractState();
|
|
|
}
|
|
|
|
|
|
- public override void OnPlayerTriggerEnter(VRCPlayerApi player)
|
|
|
+ public override void OnOwnershipTransferred(VRCPlayerApi player)
|
|
|
{
|
|
|
- if (playerTriggerEnabled)
|
|
|
- {
|
|
|
- isOccupied++;
|
|
|
- if (debugMode) Debug.Log("Linked Object System: Player set trigger to " + isOccupied.ToString());
|
|
|
- if (isOccupied > 0 && isOccupied <=2)
|
|
|
- {
|
|
|
- UpdateAnims(true);
|
|
|
- }
|
|
|
- }
|
|
|
+ // 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 OnTriggerExit(Collider other)
|
|
|
+ private void ApplyInteractState()
|
|
|
{
|
|
|
- if (pickupTriggerEnabled)
|
|
|
- {
|
|
|
- VRC_Pickup pickup = other.GetComponent<VRC_Pickup>();
|
|
|
- if (pickup != null)
|
|
|
- {
|
|
|
- isOccupied--;
|
|
|
- if (debugMode) Debug.Log("Linked Object System: Pickup set trigger to " + isOccupied.ToString());
|
|
|
- if (isOccupied == 0)
|
|
|
- {
|
|
|
- UpdateAnims(false);
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
+ // 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();
|
|
|
+ }
|
|
|
+
|
|
|
public override void OnPlayerTriggerExit(VRCPlayerApi player)
|
|
|
{
|
|
|
- if (playerTriggerEnabled)
|
|
|
- {
|
|
|
- isOccupied--;
|
|
|
- if (debugMode) Debug.Log("Linked Object System: Player set trigger to " + isOccupied.ToString());
|
|
|
- if (isOccupied == 0)
|
|
|
- {
|
|
|
- UpdateAnims(false);
|
|
|
- }
|
|
|
- }
|
|
|
+ if (!playerTriggerEnabled || player == null) return;
|
|
|
+ RemovePlayer(player.playerId);
|
|
|
}
|
|
|
- public override void Interact()
|
|
|
+
|
|
|
+ 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 (interactionType.Equals(InteractionType.Permanent))
|
|
|
+ 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)
|
|
|
{
|
|
|
- isOccupied++;
|
|
|
- if (isOccupied == 1)
|
|
|
- {
|
|
|
- UpdateAnims(true);
|
|
|
- }
|
|
|
-
|
|
|
+ pickupCheckScheduled = true;
|
|
|
+ SendCustomEventDelayedSeconds(nameof(_CheckPickups), 1f);
|
|
|
}
|
|
|
- if (interactionType.Equals(InteractionType.Temporary))
|
|
|
+ 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--)
|
|
|
{
|
|
|
- if (isOccupied == 0)
|
|
|
- {
|
|
|
- isOccupied = 1;
|
|
|
- if (debugMode) Debug.LogWarning("LOS: Logged isOccupied as " + isOccupied.ToString());
|
|
|
- UpdateAnims(true);
|
|
|
- DisableInteractive = true;
|
|
|
- SendCustomEventDelayedSeconds(nameof(DisableTemp), delayedInteractTime+0.2f);
|
|
|
- }
|
|
|
+ Collider pickup = pickupsInside[i];
|
|
|
+ if (!Utilities.IsValid(pickup) || !pickup.enabled || !pickup.gameObject.activeInHierarchy) RemovePickupAt(i);
|
|
|
}
|
|
|
- if (interactionType.Equals(InteractionType.Toggle))
|
|
|
+ UpdateOutput();
|
|
|
+ if (pickupCount > 0)
|
|
|
{
|
|
|
- if (isOccupied == 1) {
|
|
|
- isOccupied = 0;
|
|
|
- UpdateAnims(false);
|
|
|
- }
|
|
|
- else {
|
|
|
- isOccupied = 1;
|
|
|
- UpdateAnims(true);
|
|
|
- }
|
|
|
+ pickupCheckScheduled = true;
|
|
|
+ SendCustomEventDelayedSeconds(nameof(_CheckPickups), 1f);
|
|
|
}
|
|
|
}
|
|
|
- public void DisableTemp()
|
|
|
+
|
|
|
+ private bool CountsAsPickup(Collider other)
|
|
|
{
|
|
|
- DisableInteractive = false;
|
|
|
- isOccupied = 0;
|
|
|
- if (debugMode) Debug.LogWarning("LOS: Logged isOccupied as " + isOccupied.ToString());
|
|
|
+ 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);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -216,4 +261,4 @@ public enum TriggerType
|
|
|
Player,
|
|
|
Object,
|
|
|
BothPlayerAndObject
|
|
|
-}
|
|
|
+}
|