// 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
/************************************************************************************************************************/
}
}