using System.Collections; using HurricaneVR.Framework.Core; using HurricaneVR.Framework.Core.ScriptableObjects; using HurricaneVR.Framework.Core.Utils; using HurricaneVR.Framework.Shared; using UnityEngine; namespace HurricaneVR.Framework.Components { /// /// This component helps setup the basic functionality of a hinged door. /// Includes locking and latching capability. Handle required rotation for unlatching. /// [HelpURL("https://cloudwalker2020.github.io/HurricaneVR-Docs/manual/components/door.html")] [RequireComponent(typeof(HVRRotationTracker))] [RequireComponent(typeof(Rigidbody))] public class HVRPhysicsDoor : MonoBehaviour { [Header("Settings")] [Tooltip("Local axis of rotation")] public HVRAxis Axis; [Tooltip("Door's rigidbody mass.")] public float Mass = 10f; public bool DisableGravity = true; [Tooltip("If true the door and it's handle will have their joint limit's locked on start.")] public bool StartLocked; [Tooltip("Rigidbody to connect the joint to")] public Rigidbody ConnectedBody; [Header("Door Closing Settings")] [Tooltip("Angle threshold to determine if the door is closed or not.")] public float CloseAngle = 5f; [Tooltip("The door will automatically shut over this amount of time once it's close enough to be closed.")] public float CloseOverTime = .25f; [Tooltip("How long the door angle must be below 'CloseAngle' to become closed.")] public float CloseDetectionTime = .5f; [Header("SFX")] [Tooltip("Angle threshold to play opening and closing sound effects.")] public float SFXThresholdAngle = 2.5f; public float SFXResetThreshold = 1f; public AudioClip SFXOpened; public AudioClip SFXClosed; [Tooltip("Delay before the open / close sfx can be played again")] public float SFXTimeout = 1f; [Tooltip("Optional transform to define the position of the open / close sound fx.")] public Transform SFXPosition; [Header("Handle")] [Tooltip("If true the handle must rotate beyond 'HandThreshold' amount of degrees before it will unlatch, if false the door will not latch automatically.")] public bool HandleRequiresRotation; [Tooltip("Required handle rotation to unlatch the door.")] public float HandleThreshold = 45f; [Tooltip("The rotation tracker that reports the amount of rotation of the handle.")] public HVRRotationTracker HandleRotationTracker; [Tooltip("If provided (and held) the door will not automatically shut when it is below 'CloseAngle' in degrees.")] public HVRGrabbable HandleGrabbable; [Tooltip("Rotational physics component that let's this door component lock the door handle's rotation when the door locks.")] public HVRPhysicsDial DoorKnob; [Tooltip("The rotation tracker that reports the amount of rotation of the handle.")] public HVRRotationTracker SecondHandleRotationTracker; [Tooltip("If provided (and held) the door will not automatically shut when it is below 'CloseAngle' in degrees.")] public HVRGrabbable SecondHandleGrabbable; [Tooltip("Rotational physics component that let's this door component lock the door handle's rotation when the door locks.")] public HVRPhysicsDial SecondDoorKnob; [Header("Joint Limits")] public bool LimitRotation = true; [Tooltip("Minimum Angle about the axis of rotation")] public float MinAngle; [Tooltip("Maximum rotation about the axis of rotation")] public float MaxAngle; [Header("Joint Settings")] [Tooltip("Angular Damper of the door hinge.")] public float Damper = 10; [Tooltip("Angular Spring that will return the door to it's starting rotation")] public float Spring; //[Header("Editor")] //[SerializeField] //protected Quaternion JointStartRotation; [Header("Debugging")] public float TargetAngularVelocity = 0f; public bool DoorLatched; public bool DoorClosed; public bool Opened; public bool Closed; public bool PreviousDoorLatched; public bool PreviousClosed; public bool VerboseLogging; public bool Locked; public Rigidbody Rigidbody { get; private set; } public HVRRotationTracker Tracker { get; private set; } protected ConfigurableJoint Joint { get; set; } private Quaternion _startRotation; private bool _doorClosing; private float _detectionTimer; private float _lastClosedSFXTime; private float _lastOpenedSFXTime; public virtual void Start() { Rigidbody = this.GetRigidbody(); Rigidbody.mass = Mass; Rigidbody.useGravity = !DisableGravity; Tracker = GetComponent(); if (MinAngle > 0) { MinAngle *= -1; } if (MaxAngle < 0) { MaxAngle *= -1; } MinAngle = Mathf.Clamp(MinAngle, MinAngle, 0); MaxAngle = Mathf.Clamp(MaxAngle, 0, MaxAngle); if (HandleRequiresRotation) { if (!HandleRotationTracker) { Debug.LogError("HandleRotationTracker not assigned."); } DoorLatched = true; } DoorClosed = true; PreviousDoorLatched = DoorLatched; PreviousClosed = DoorClosed; SetupJoint(); _startRotation = transform.localRotation; if (DoorLatched) { LockDoorJoint(); } if (StartLocked) { Lock(); } //set initial values to prevent sfx on start if (Tracker.UnsignedAngle < SFXThresholdAngle) { Closed = true; } else if (Tracker.UnsignedAngle > SFXThresholdAngle) { Opened = true; } } protected virtual void Update() { Joint.targetAngularVelocity = new Vector3(TargetAngularVelocity, 0f, 0f); if (_doorClosing) return; if (Tracker.UnsignedAngle < CloseAngle) { _detectionTimer += Time.deltaTime; } else if (Tracker.UnsignedAngle >= CloseAngle) { _detectionTimer = 0f; DoorClosed = false; } if (HandleGrabbable && HandleGrabbable.IsBeingHeld || SecondHandleGrabbable && SecondHandleGrabbable.IsBeingHeld) { _detectionTimer = 0f; } if (_detectionTimer > CloseDetectionTime) { DoorClosed = true; } if (!PreviousClosed && DoorClosed) { OnDoorClosed(); } else if (PreviousClosed && !DoorClosed) { OnDoorOpened(); } var reset = SFXResetThreshold; if (SFXResetThreshold > SFXThresholdAngle) { reset = SFXThresholdAngle * .5f; } if (!Opened && Tracker.UnsignedAngle > SFXThresholdAngle && Time.time - _lastOpenedSFXTime > SFXTimeout) { _lastOpenedSFXTime = Time.time; Opened = true; PlayOpenedSFX(); } else if (!Closed && Tracker.UnsignedAngle < SFXThresholdAngle && Time.time - _lastClosedSFXTime > SFXTimeout) { _lastClosedSFXTime = Time.time; Closed = true; PlayClosedSFX(); } else if (Opened && Tracker.UnsignedAngle < SFXThresholdAngle - reset) { Opened = false; } else if (Closed && Tracker.UnsignedAngle > SFXThresholdAngle + reset) { Closed = false; } if (HandleRequiresRotation) { if (HandleRotationTracker.UnsignedAngle >= HandleThreshold || (SecondHandleRotationTracker && SecondHandleRotationTracker.UnsignedAngle >= HandleThreshold)) { DoorLatched = false; } else if (HandleRotationTracker.UnsignedAngle < HandleThreshold && (!SecondHandleRotationTracker || SecondHandleRotationTracker.UnsignedAngle < HandleThreshold) && Tracker.UnsignedAngle < CloseAngle) { DoorLatched = true; } if (!Locked) { if (PreviousDoorLatched && !DoorLatched) { OnDoorUnLatched(); } else if (!PreviousDoorLatched && DoorLatched) { OnDoorLatched(); } } } PreviousDoorLatched = DoorLatched; PreviousClosed = DoorClosed; } protected virtual Vector3 GetSFXPosition() { var position = transform.position; if (SFXPosition) { position = SFXPosition.position; } return position; } protected virtual void PlayClosedSFX() { if (SFXPlayer.Instance) SFXPlayer.Instance.PlaySFX(SFXClosed, GetSFXPosition()); } protected virtual void PlayOpenedSFX() { if (SFXPlayer.Instance) SFXPlayer.Instance.PlaySFX(SFXOpened, GetSFXPosition()); } public virtual void OnDoorUnLatched() { if (VerboseLogging) Debug.Log($"OnDoorUnLatched"); UnlockDoorJoint(); } public virtual void OnDoorLatched() { if (VerboseLogging) Debug.Log($"OnDoorLatched"); LockDoorJoint(); } // ReSharper disable Unity.PerformanceAnalysis protected virtual void OnDoorClosed() { if (VerboseLogging) Debug.Log($"OnDoorClosed"); StartCoroutine(DoorCloseRoutine()); } // ReSharper disable Unity.PerformanceAnalysis protected virtual void OnDoorOpened() { if (VerboseLogging) Debug.Log($"OnDoorOpened"); } protected virtual void LockDoorJoint() { if (!LimitRotation) return; Joint.SetAngularXHighLimit(0); Joint.SetAngularXLowLimit(0); } protected virtual void UnlockDoorJoint() { Joint.SetAngularXHighLimit(-MinAngle); Joint.SetAngularXLowLimit(-MaxAngle); } /// /// Locks the door joint, and the door knob's joint. /// public virtual void Lock() { Locked = true; LockDoorJoint(); LockDoorKnob(); } /// /// Unlocks the door and allows the door handle to rotate. /// public virtual void Unlock() { Locked = false; if (!DoorLatched) { UnlockDoorJoint(); } UnlockDoorKnob(); } protected virtual void LockDoorKnob() { if (DoorKnob) DoorKnob.SetLimits(0, 0); if (SecondDoorKnob) SecondDoorKnob.SetLimits(0, 0); } protected virtual void UnlockDoorKnob() { if (DoorKnob) DoorKnob.ResetLimits(); if (SecondDoorKnob) SecondDoorKnob.ResetLimits(); } protected IEnumerator DoorCloseRoutine() { _doorClosing = true; var startRotation = transform.localRotation; var elapsed = 0f; while (elapsed < CloseOverTime) { transform.localRotation = Quaternion.Lerp(startRotation, _startRotation, elapsed / CloseOverTime); elapsed += Time.deltaTime; yield return null; } transform.localRotation = _startRotation; _doorClosing = false; } protected virtual void SetupJoint() { var currentRotation = transform.localRotation; //transform.localRotation = JointStartRotation; Joint = gameObject.AddComponent(); Joint.connectedBody = ConnectedBody; Joint.LockLinearMotion(); Joint.LockAngularYMotion(); Joint.LockAngularZMotion(); Joint.anchor = Vector3.zero; Joint.axis = Axis.GetVector(); if (LimitRotation) { Joint.LimitAngularXMotion(); Joint.SetAngularXHighLimit(-MinAngle); Joint.SetAngularXLowLimit(-MaxAngle); } else { Joint.angularXMotion = ConfigurableJointMotion.Free; } Joint.secondaryAxis = Joint.axis.OrthogonalVector(); Joint.SetAngularXDrive(Spring, Damper, 10000f); transform.localRotation = currentRotation; } } }