// 04.10.2021 11:00 using System; using System.Collections.Generic; using System.Threading.Tasks; using UnityEngine; public enum PivotAxis { Free = 10, X = 20, Y = 30, Z = 40, XY = 50, XZ = 60, YZ = 70, } public enum EEnablingBehaviour { Transition = 0, Hold = 10, Jump = 20, } public class ThresholdedBillboard : MonoBehaviour { #region Inspector Properties [Header("Config Values")] [Tooltip("Specifies the axis about which the object will rotate.")] [SerializeField] private PivotAxis pivotAxis = PivotAxis.XY; [Tooltip("Specifies the desired OnEnable behaviour.")] [SerializeField] private EEnablingBehaviour enablingBehaviour; [Tooltip("Specifies the delay in seconds before initializing.")] [SerializeField] private float initDelay = 0.03f; [Tooltip("Specifies how fast the object will rotate.")] [SerializeField] private float rotationSpeed = 2f; [Tooltip("Specifies the deviation at which the object will start rotating.")] [SerializeField] private float startBillboardingAngle = 20f; [Tooltip("Specifies the deviation at which the object will stop rotating.")] [SerializeField] private float stopBillboardingAngle = 2f; [Tooltip("Specifies if the rotation should happen after a delay.")] [SerializeField] private bool isDelayedModeActive = false; [Tooltip("Only for delayed Mode: Specifies the delay before the rotation should happen.")] [SerializeField] private float delayedModeDelay = 0.3f; [Header("Scene Objects")] [Tooltip("Specifies the target we will orient to. If no target is specified, the main camera will be used.")] [SerializeField] private Transform targetTransform; [Tooltip("Specifies transforms which will be set to the same rotation.")] [SerializeField] private List linkedTransforms; #endregion #region Public Properties public PivotAxis PivotAxis { get { return pivotAxis; } set { pivotAxis = value; } } /// /// The target we will orient to. If no target is specified, the main camera will be used. /// public Transform TargetTransform => targetTransform; #endregion #region Private Properties private bool isInitialized = false; private Camera cam; private Quaternion rotationFrom; private Quaternion rotationTo; private bool delayedRotationIsPending = false; private float lastRotationRequiredTime = 0; #endregion #region Framework Functions void Awake() { this.cam = Camera.main; } void OnEnable() { this.initBillboardWithDelay(); } void Update() { if (this.isInitialized) { this.rotateToTarget(false, false); } } void OnDisable() { this.isInitialized = false; } #endregion #region Events #endregion #region Public Functions public void OverrideEnablingBehaviour(EEnablingBehaviour newBehaviour) { this.enablingBehaviour = newBehaviour; } public void OverrideRotationSpeed(float newRotationSpeed) { this.rotationSpeed = newRotationSpeed; } public void OverrideStartBillboardingAngle(float newStartBillboardingAngle) { this.startBillboardingAngle = newStartBillboardingAngle; } public void OverrideStopBillboardingAngle(float newStopBillboardingAngle) { this.stopBillboardingAngle = newStopBillboardingAngle; } public void JumpToDesiredRotation() { this.rotateToTarget(false, true); } #endregion #region Private Functions private async void initBillboardWithDelay() { await Task.Delay(TimeSpan.FromSeconds(this.initDelay)); if (this == null) { // Has been destroyed -> cancel return; } this.rotationFrom = Quaternion.identity; this.rotationTo = Quaternion.identity; if (this.enablingBehaviour == EEnablingBehaviour.Transition) { this.rotateToTarget(true, false); } if (this.enablingBehaviour == EEnablingBehaviour.Jump) { this.rotateToTarget(false, true); } this.isInitialized = true; } private void rotateToTarget(bool forceRotation, bool jumpToDesiredRotation) { if (this.targetTransform == null) { this.targetTransform = this.cam.transform; } // Get a Vector that points from the target to the main camera. Vector3 directionToTarget = targetTransform.position - transform.position; bool useCameraAsUpVector = true; // Adjust for the pivot axis. switch (pivotAxis) { case PivotAxis.X: directionToTarget.x = 0.0f; useCameraAsUpVector = false; break; case PivotAxis.Y: directionToTarget.y = 0.0f; useCameraAsUpVector = false; break; case PivotAxis.Z: directionToTarget.x = 0.0f; directionToTarget.y = 0.0f; break; case PivotAxis.XY: useCameraAsUpVector = false; break; case PivotAxis.XZ: directionToTarget.x = 0.0f; break; case PivotAxis.YZ: directionToTarget.y = 0.0f; break; case PivotAxis.Free: default: // No changes needed. break; } // If we are right next to the camera the rotation is undefined. if (directionToTarget.sqrMagnitude < 0.001f) { return; } Quaternion rotationCurrent = Quaternion.identity; // Calculate and apply the rotation required to reorient the object if (useCameraAsUpVector) { rotationCurrent = Quaternion.LookRotation(-directionToTarget, this.cam.transform.up); } else { rotationCurrent = Quaternion.LookRotation(-directionToTarget); } if (jumpToDesiredRotation) { this.transform.rotation = rotationCurrent; return; } float deviationToDesiredRotation = Mathf.Abs(Quaternion.Angle(rotationCurrent, transform.rotation)); // Check if rotation target needs to be set if (deviationToDesiredRotation > this.startBillboardingAngle || forceRotation) { if (this.isDelayedModeActive) { // Delayed mode if (!this.delayedRotationIsPending) { // Start "timer" this.delayedRotationIsPending = true; this.lastRotationRequiredTime = Time.time; } else { // "Timer" running if (Time.time > this.lastRotationRequiredTime + this.delayedModeDelay) { // Delaytime has passed -> start rotating this.delayedRotationIsPending = false; this.rotationFrom = transform.rotation; this.rotationTo = rotationCurrent; } } } else { // Non delayed mode -> start rotating this.rotationFrom = transform.rotation; this.rotationTo = rotationCurrent; } } // If delayed mode -> check if delayedRotationIsPending needs to be reset if (deviationToDesiredRotation < this.startBillboardingAngle && this.isDelayedModeActive) { this.delayedRotationIsPending = false; } if (this.rotationTo != Quaternion.identity) { // Currently rotating transform.rotation = Quaternion.Lerp(this.transform.rotation, this.rotationTo, Time.deltaTime * rotationSpeed); for (int i = 0; i < this.linkedTransforms.Count; i++) { this.linkedTransforms[i].rotation = this.transform.rotation; } // Check if should stop float deviationToTargetRotation = Mathf.Abs(Quaternion.Angle(this.transform.rotation, this.rotationTo)); if (deviationToTargetRotation < this.stopBillboardingAngle) { this.rotationFrom = Quaternion.identity; this.rotationTo = Quaternion.identity; } } } #endregion }