// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik // using UnityEngine; namespace Animancer { /// A using as the group type. /// /// /// Sample: /// /// Directional Character 3D /// /// /// https://kybernetik.com.au/animancer/api/Animancer/DirectionalAnimations3D /// [AddComponentMenu(Strings.MenuPrefix + "Directional Animations 3D")] [AnimancerHelpUrl(typeof(DirectionalAnimations3D))] public class DirectionalAnimations3D : DirectionalAnimations3D { } /************************************************************************************************************************/ /// /// A component which manages a screen-facing billboard and plays animations from a /// to make it look like a /// based character is facing a particular direction in 3D space. /// /// /// /// Sample: /// /// Directional Character 3D /// /// /// https://kybernetik.com.au/animancer/api/Animancer/DirectionalAnimations3D_1 /// [AnimancerHelpUrl(typeof(DirectionalAnimations3D<>))] public class DirectionalAnimations3D : MonoBehaviour { /************************************************************************************************************************/ #region Fields and Properties /************************************************************************************************************************/ [SerializeField] [Tooltip("The object to rotate according to the " + nameof(Mode))] private Transform _Transform; /// [] /// The object to rotate according to the . /// /// Uses this by default. public ref Transform Transform => ref _Transform; /************************************************************************************************************************/ [SerializeField] [Tooltip("The " + nameof(UnityEngine.Camera) + " to make the " + nameof(Transform) + " face towards" + "\n\nLeave this null to automatically use the Main Camera")] private Transform _Camera; /// [] /// The to make the face towards. /// /// /// Leave this null to automatically use the . /// public Transform Camera { get { if (_Camera == null) { var camera = UnityEngine.Camera.main; if (camera != null) _Camera = camera.transform; } return _Camera; } set => _Camera = value; } /************************************************************************************************************************/ [SerializeField] [Tooltip("The " + nameof(AnimancerComponent) + " to play animations on")] private AnimancerComponent _Animancer; /// [] /// The to play animations on. /// public ref AnimancerComponent Animancer => ref _Animancer; /************************************************************************************************************************/ [SerializeField] [Tooltip("The " + nameof(DirectionalAnimationSet) + " to play animations from" + " (Forwards in 3D space corresponds to the Up animation)")] private DirectionalAnimationSet _Animations; /// [] /// The animations to choose between based on the direction. /// /// Forwards in 3D space corresponds to the Up animation. public ref DirectionalAnimationSet Animations => ref _Animations; /************************************************************************************************************************/ [SerializeField] [Tooltip("The World-Space direction this character is facing used to select which animation to play")] private Vector3 _Forward = Vector3.forward; /// [] /// The World-Space direction this character is facing used to select which animation to play. /// public Vector3 Forward { get => _Forward; set { _Forward = value; if (!enabled) PlayCurrentAnimation(TimeSynchronizer.CurrentGroup); } } /************************************************************************************************************************/ /// Functions used to face the towards the . public enum BillboardMode { /// Don't control the . None, /// Copy the 's rotation. MatchRotation, /// Face the 's position. FacePosition, /// As , but only rotate around the Y axis. UprightMatchRotation, /// As , but only rotate around the Y axis. UprightFacePosition, /// /// As , /// and also scale on the Y axis to maintain the same screen size /// regardless of the 's Euler X Angle. /// Only use this mode with an Orthographic Camera UprightMatchRotationStretched, /// /// As , /// and also scale on the Y axis to maintain the same screen size /// regardless of the 's Euler X Angle. /// Only use this mode with an Orthographic Camera UprightFacePositionStretched, } [SerializeField] [Tooltip("The function used to face the " + nameof(Transform) + " towards the " + nameof(Camera) + ":" + "\n• None - Don't control the " + nameof(Transform) + "\n• Match Rotation - Copy the " + nameof(Camera) + "'s rotation" + "\n• Face Position - Face the " + nameof(Camera) + "'s position" + "\n• Upright - As above, but only rotate around the Y axis" + "\n• Stretched - As above, and also scale on the Y axis to maintain the same screen size" + " regardless of the " + nameof(Camera) + "'s Euler X Angle (only use with an Orthographic Camera)")] private BillboardMode _Mode = BillboardMode.UprightMatchRotation; /// [] /// The function used to face the towards the . /// public BillboardMode Mode { get => _Mode; set { _Mode = value; ResetScaleIfNotStretched(); } } /************************************************************************************************************************/ /// /// Maintains the when swapping between animations. /// public readonly TimeSynchronizer TimeSynchronizer = new(default, true); /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Methods /************************************************************************************************************************/ /// /// Finds missing references, /// samples the current animation, /// and resets the scale to 1 if not using a stretched mode. /// protected virtual void OnValidate() { gameObject.GetComponentInParentOrChildren(ref _Transform); gameObject.GetComponentInParentOrChildren(ref _Animancer); if (TryGetCurrentAnimation(out var animation)) AnimancerUtilities.EditModeSampleAnimation(animation, _Animancer); ResetScaleIfNotStretched(); } /************************************************************************************************************************/ /// /// Finds missing references, /// samples the current animation, /// and resets the scale to 1 if not using a stretched mode. /// protected virtual void OnDrawGizmosSelected() { if (TryGetCurrentAnimation(out var animation)) AnimancerUtilities.EditModeSampleAnimation(animation, _Animancer); if (_Transform == null) return; var position = _Transform.position; var length = 1f; var renderer = GetComponentInChildren(); if (renderer != null) { var bounds = renderer.bounds; position.y += bounds.extents.y; length = bounds.extents.magnitude; } Gizmos.color = new(0.75f, 0.75f, 1, 1); Gizmos.DrawRay(position, Forward.normalized * length); } /************************************************************************************************************************/ /// /// Applies the then plays the appropriate animation /// based on the current rotation and direction. /// protected virtual void Update() { UpdateTransform(); PlayCurrentAnimation(TimeSynchronizer.CurrentGroup); } /************************************************************************************************************************/ /// Applies the . public void UpdateTransform() { switch (_Mode) { default: case BillboardMode.None: break; case BillboardMode.MatchRotation: _Transform.rotation = Camera.rotation; break; case BillboardMode.FacePosition: _Transform.rotation = Quaternion.LookRotation(_Transform.position - Camera.position); break; case BillboardMode.UprightMatchRotation: _Transform.eulerAngles = new(0, Camera.eulerAngles.y, 0); break; case BillboardMode.UprightFacePosition: var direction = _Transform.position - Camera.position; _Transform.eulerAngles = new( 0, Mathf.Atan2(direction.x, direction.z) * Mathf.Rad2Deg, 0); break; case BillboardMode.UprightMatchRotationStretched: var eulerAngles = Camera.eulerAngles; _Transform.eulerAngles = new(0, eulerAngles.y, 0); StretchHeight(eulerAngles.x); break; case BillboardMode.UprightFacePositionStretched: StretchHeight(Camera.eulerAngles.x); goto case BillboardMode.UprightFacePosition; } } /************************************************************************************************************************/ /// /// Scales the on the Y axis to maintain the same screen size /// regardless of the 's Euler X Angle. /// /// This calculation only makes sense with an orthographic camera. private void StretchHeight(float eulerX) { if (eulerX > 180) eulerX -= 360; else if (eulerX < -180) eulerX += 360; _Transform.localScale = new( 1, 1 / Mathf.Cos(eulerX * Mathf.Deg2Rad), 1); } /// /// Resets the to 1 if not using a stretched . /// private void ResetScaleIfNotStretched() { if (_Transform == null) return; switch (_Mode) { case BillboardMode.UprightMatchRotationStretched: case BillboardMode.UprightFacePositionStretched: break; default: _Transform.localScale = Vector3.one; break; } } /************************************************************************************************************************/ /// /// Sets the and plays the appropriate animation /// based on the current rotation and direction. /// public void SetAnimations(DirectionalAnimationSet animations, TGroup group = default) { _Animations = animations; PlayCurrentAnimation(group); } /************************************************************************************************************************/ /// /// Plays the appropriate animation based on the current rotation and direction. /// /// /// If the `group` is the same as the previous, the new animation will be given the same /// as the previous. /// public void PlayCurrentAnimation(TGroup group) { if (TryGetCurrentAnimation(out var animation)) { TimeSynchronizer.StoreTime(_Animancer); _Animancer.Play(animation); TimeSynchronizer.SyncTime(_Animancer, group); } } /************************************************************************************************************************/ /// /// Tries to get an appropriate animation based on the current rotation and direction. /// private bool TryGetCurrentAnimation(out AnimationClip animation) { if (_Animations == null || _Forward == default) { animation = null; return false; } var localForward = _Transform.InverseTransformDirection(_Forward); var horizontalForward = new Vector2(localForward.x, localForward.z); animation = _Animations.GetClip(horizontalForward); return true; } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ } }