namespace VRTK.Prefabs.Interactions.Controllables { using UnityEngine; using Malimbe.BehaviourStateRequirementMethod; using Zinnia.Extension; using Zinnia.Data.Type; /// /// The basis to drive a control in a rotational angle. /// public abstract class RotationalDrive : Drive { /// /// The previous rotation angle of the control. /// protected float PreviousActualAngle => previousActualRotation[(int)Facade.DriveAxis]; /// /// The current rotation angle of the control. /// protected float CurrentActualAngle => currentActualRotation[(int)Facade.DriveAxis]; /// /// The target angle for the control to reach. /// protected float ActualTargetAngle => Mathf.Lerp(DriveLimits.minimum, DriveLimits.maximum, Facade.TargetValue); /// /// The total number of degrees in a circle. /// protected const float circleDegrees = 360f; /// /// The angle range that defines the upper right quadrant of a circle. /// protected static readonly FloatRange circleUpperRightQuadrant = new FloatRange(-1f, 90f); /// /// The angle range that defines the upper left quadrant of a circle. /// protected static readonly FloatRange circleUpperLeftQuadrant = new FloatRange(270f, 360f); /// /// The representation of the previous frame rotation in meaningful values and not limited to 0f to 360f. /// protected float previousPseudoRotation; /// /// The representation of the current frame rotation in meaningful values and not limited to 0f to 360f. /// protected float currentPseudoRotation; /// /// The representation of the rotational velocity. /// protected float pseudoAngularVelocity; /// /// The multiplier used to determine how many complete revolutions the drive has performed. /// protected float rotationMultiplier; /// /// The previous actual rotational value of the drive. /// protected Vector3 previousActualRotation; /// /// The current actual rotational value of the drive. /// protected Vector3 currentActualRotation; /// /// Calculates the location of the rotational hinge for the drive. /// /// The new local space for the hinge point. public abstract void CalculateHingeLocation(Vector3 newHingeLocation); /// /// Attempts to apply the existing angular velocity back on to the rotation of the drive. /// /// The amount to multiply the angular velocity to be applied by. public abstract void ApplyExistingAngularVelocity(float multiplier = 1f); /// [RequiresBehaviourState] public override void Process() { base.Process(); previousActualRotation = currentActualRotation; currentActualRotation = GetSimpleEulerAngles(); CalculateRotationMultiplier(); AttemptApplyLimits(); currentPseudoRotation = CurrentActualAngle + (circleDegrees * rotationMultiplier); pseudoAngularVelocity = !currentPseudoRotation.ApproxEquals(previousPseudoRotation) ? previousPseudoRotation - currentPseudoRotation : pseudoAngularVelocity; previousPseudoRotation = currentPseudoRotation; float autoDriveTargetVelocity = CalculateAutoDriveVelocity(); ProcessAutoDrive(autoDriveTargetVelocity); MatchActualTargetAngle(autoDriveTargetVelocity); } /// /// Automatically controls the drive to the target rotation at the given speed. /// /// The speed to automatically rotate the drive. protected abstract void ProcessAutoDrive(float driveSpeed); /// protected override void SetUpInternals() { ConfigureAutoDrive(Facade.MoveToTargetValue); CalculateHingeLocation(Facade.HingeLocation); } /// protected override FloatRange CalculateDriveLimits(RotationalDriveFacade facade) { return facade.DriveLimit; } /// [RequiresBehaviourState] protected override float CalculateValue(DriveAxis.Axis driveAxis, FloatRange limits) { return Mathf.Clamp(currentPseudoRotation, limits.minimum, limits.maximum); } /// /// Attempts to retrieve a simple x, y or z euler angle from the utilizing any other axis rotation. /// /// The actual axis angle from 0f to 360f. protected virtual Vector3 GetSimpleEulerAngles() { Vector3 currentEulerAngle = GetDriveTransform().localEulerAngles; if (Facade.DriveAxis == DriveAxis.Axis.XAxis && !currentEulerAngle.y.ApproxEquals(0f, 1f)) { currentEulerAngle.x = currentEulerAngle.y - (currentEulerAngle.x > (circleDegrees * 0.5f) ? currentEulerAngle.x - circleDegrees : currentEulerAngle.x); } return currentEulerAngle; } /// /// Calculates a multiplier based on the direction the rotation is traveling. /// /// The multiplier that represents the direction. protected virtual float CalculateDirectionMultiplier() { float actualAngle = ActualTargetAngle; if (actualAngle.ApproxEquals(currentPseudoRotation, TargetValueReachedThreshold)) { return 0f; } return actualAngle > currentPseudoRotation ? 1f : -1f; } /// /// Attempts to apply the limits on the drive. /// protected virtual void AttemptApplyLimits() { ApplyLimits(); } /// /// Applies the limits on the drive rotation. /// /// Whether the limits have been applied. protected virtual bool ApplyLimits() { if (currentPseudoRotation < DriveLimits.minimum - TargetValueReachedThreshold) { GetDriveTransform().localRotation = Quaternion.Euler(-AxisDirection * DriveLimits.minimum); return true; } else if (currentPseudoRotation > DriveLimits.maximum + TargetValueReachedThreshold) { GetDriveTransform().localRotation = Quaternion.Euler(-AxisDirection * DriveLimits.maximum); return true; } return false; } /// /// Calculates the velocity that the drive should automatically rotate the control with. /// /// The velocity to drive the control automatically with. protected virtual float CalculateAutoDriveVelocity() { return (Facade.MoveToTargetValue && !currentPseudoRotation.ApproxEquals(ActualTargetAngle, TargetValueReachedThreshold) ? Facade.DriveSpeed : 0f) * CalculateDirectionMultiplier(); } /// /// Calculates the current rotation the control is at. /// protected virtual void CalculateRotationMultiplier() { if (circleUpperLeftQuadrant.Contains(PreviousActualAngle) && circleUpperRightQuadrant.Contains(CurrentActualAngle)) { rotationMultiplier++; } else if (circleUpperRightQuadrant.Contains(PreviousActualAngle) && circleUpperLeftQuadrant.Contains(CurrentActualAngle)) { rotationMultiplier--; } } /// /// Attempts to match the target angle to set the control at the correct angle if the drive is no longer moving. /// /// The speed the drive is automatically rotating at. protected virtual void MatchActualTargetAngle(float driveSpeed) { if (Facade.MoveToTargetValue && driveSpeed.ApproxEquals(0f)) { GetDriveTransform().localRotation = Quaternion.Euler(-AxisDirection * ActualTargetAngle); } } } }