This commit is contained in:
2025-08-18 09:22:24 +08:00
commit cef5623ab0
1333 changed files with 305844 additions and 0 deletions
@@ -0,0 +1,358 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Animancer
{
/// <summary>A set of up/right/down/left animations.</summary>
/// <remarks>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/playing/directional-sets">
/// Directional Animation Sets</see>
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer/DirectionalAnimationSet
///
[CreateAssetMenu(
menuName = Strings.MenuPrefix + "Directional Animation Set/4 Directions",
order = Strings.AssetMenuOrder + 3)]
[AnimancerHelpUrl(typeof(DirectionalAnimationSet))]
public class DirectionalAnimationSet : ScriptableObject,
IAnimationClipSource
{
/************************************************************************************************************************/
[SerializeField]
private AnimationClip _Up;
/// <summary>[<see cref="SerializeField"/>] The animation facing up (0, 1).</summary>
/// <exception cref="ArgumentException"><see cref="AllowSetClips"/> was not called before setting this value.</exception>
public AnimationClip Up
{
get => _Up;
set
{
AssertCanSetClips();
_Up = value;
AnimancerUtilities.SetDirty(this);
}
}
/************************************************************************************************************************/
[SerializeField]
private AnimationClip _Right;
/// <summary>[<see cref="SerializeField"/>] The animation facing right (1, 0).</summary>
/// <exception cref="ArgumentException"><see cref="AllowSetClips"/> was not called before setting this value.</exception>
public AnimationClip Right
{
get => _Right;
set
{
AssertCanSetClips();
_Right = value;
AnimancerUtilities.SetDirty(this);
}
}
/************************************************************************************************************************/
[SerializeField]
private AnimationClip _Down;
/// <summary>[<see cref="SerializeField"/>] The animation facing down (0, -1).</summary>
/// <exception cref="ArgumentException"><see cref="AllowSetClips"/> was not called before setting this value.</exception>
public AnimationClip Down
{
get => _Down;
set
{
AssertCanSetClips();
_Down = value;
AnimancerUtilities.SetDirty(this);
}
}
/************************************************************************************************************************/
[SerializeField]
private AnimationClip _Left;
/// <summary>[<see cref="SerializeField"/>] The animation facing left (-1, 0).</summary>
/// <exception cref="ArgumentException"><see cref="AllowSetClips"/> was not called before setting this value.</exception>
public AnimationClip Left
{
get => _Left;
set
{
AssertCanSetClips();
_Left = value;
AnimancerUtilities.SetDirty(this);
}
}
/************************************************************************************************************************/
#if UNITY_ASSERTIONS
private bool _AllowSetClips;
#endif
/// <summary>[Assert-Only]
/// Determines whether the <see cref="AnimationClip"/> properties are allowed to be set.
/// </summary>
[System.Diagnostics.Conditional(Strings.Assertions)]
public void AllowSetClips(bool allow = true)
{
#if UNITY_ASSERTIONS
_AllowSetClips = allow;
#endif
}
/// <summary>[Assert-Only]
/// Throws an <see cref="ArgumentException"/> if <see cref="AllowSetClips"/> wasn't called.
/// </summary>
[System.Diagnostics.Conditional(Strings.Assertions)]
public void AssertCanSetClips()
{
#if UNITY_ASSERTIONS
AnimancerUtilities.Assert(_AllowSetClips,
$"{nameof(AllowSetClips)}() must be called before attempting to set any of" +
$" the animations in a {nameof(DirectionalAnimationSet)}" +
$" to ensure that they are not changed accidentally.");
#endif
}
/************************************************************************************************************************/
/// <summary>Returns the animation closest to the specified `direction`.</summary>
public virtual AnimationClip GetClip(Vector2 direction)
{
if (direction.x >= 0)
{
if (direction.y >= 0)
return direction.x > direction.y ? _Right : _Up;
else
return direction.x > -direction.y ? _Right : _Down;
}
else
{
if (direction.y >= 0)
return direction.x < -direction.y ? _Left : _Up;
else
return direction.x < direction.y ? _Left : _Down;
}
}
/************************************************************************************************************************/
#region Directions
/************************************************************************************************************************/
/// <summary>The number of animations in this set.</summary>
public virtual int ClipCount
=> 4;
/************************************************************************************************************************/
/// <summary>Up, Right, Down, or Left.</summary>
/// <remarks>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/playing/directional-sets">
/// Directional Animation Sets</see>
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer/Direction
///
public enum Direction
{
/// <summary><see cref="Vector2.up"/>.</summary>
Up,
/// <summary><see cref="Vector2.right"/>.</summary>
Right,
/// <summary><see cref="Vector2.down"/>.</summary>
Down,
/// <summary><see cref="Vector2.left"/>.</summary>
Left,
}
/************************************************************************************************************************/
/// <summary>Returns the name of the specified `direction`.</summary>
protected virtual string GetDirectionName(int direction)
=> ((Direction)direction).ToString();
/************************************************************************************************************************/
/// <summary>Returns the animation associated with the specified `direction`.</summary>
public AnimationClip GetClip(Direction direction)
=> direction switch
{
Direction.Up => _Up,
Direction.Right => _Right,
Direction.Down => _Down,
Direction.Left => _Left,
_ => throw AnimancerUtilities.CreateUnsupportedArgumentException(direction),
};
/// <summary>Returns the animation associated with the specified `direction`.</summary>
public virtual AnimationClip GetClip(int direction)
=> GetClip((Direction)direction);
/************************************************************************************************************************/
/// <summary>Sets the animation associated with the specified `direction`.</summary>
public void SetClip(Direction direction, AnimationClip clip)
{
switch (direction)
{
case Direction.Up: Up = clip; break;
case Direction.Right: Right = clip; break;
case Direction.Down: Down = clip; break;
case Direction.Left: Left = clip; break;
default: throw AnimancerUtilities.CreateUnsupportedArgumentException(direction);
}
}
/// <summary>Sets the animation associated with the specified `direction`.</summary>
public virtual void SetClip(int direction, AnimationClip clip) => SetClip((Direction)direction, clip);
/************************************************************************************************************************/
/// <summary>[Editor-Only]
/// Attempts to assign the `clip` to one of this set's fields based on its name and
/// returns the direction index of that field (or -1 if it was unable to determine the direction).
/// </summary>
public virtual int SetClipByName(AnimationClip clip)
{
var name = clip.name;
int bestDirection = -1;
int bestDirectionIndex = -1;
var directionCount = ClipCount;
for (int i = 0; i < directionCount; i++)
{
var index = name.LastIndexOf(GetDirectionName(i));
if (bestDirectionIndex < index)
{
bestDirectionIndex = index;
bestDirection = i;
}
}
if (bestDirection >= 0)
SetClip(bestDirection, clip);
return bestDirection;
}
/************************************************************************************************************************/
#region Conversion
/************************************************************************************************************************/
/// <summary>Returns a vector representing the specified `direction`.</summary>
public static Vector2 DirectionToVector(Direction direction)
=> direction switch
{
Direction.Up => Vector2.up,
Direction.Right => Vector2.right,
Direction.Down => Vector2.down,
Direction.Left => Vector2.left,
_ => throw AnimancerUtilities.CreateUnsupportedArgumentException(direction),
};
/// <summary>Returns a vector representing the specified `direction`.</summary>
public virtual Vector2 GetDirection(int direction)
=> DirectionToVector((Direction)direction);
/************************************************************************************************************************/
/// <summary>Returns the direction closest to the specified `vector`.</summary>
public static Direction VectorToDirection(Vector2 vector)
{
if (vector.x >= 0)
{
if (vector.y >= 0)
return vector.x > vector.y ? Direction.Right : Direction.Up;
else
return vector.x > -vector.y ? Direction.Right : Direction.Down;
}
else
{
if (vector.y >= 0)
return vector.x < -vector.y ? Direction.Left : Direction.Up;
else
return vector.x < vector.y ? Direction.Left : Direction.Down;
}
}
/************************************************************************************************************************/
/// <summary>Returns a copy of the `vector` pointing in the closest direction this set type has an animation for.</summary>
public static Vector2 SnapVectorToDirection(Vector2 vector)
{
var magnitude = vector.magnitude;
var direction = VectorToDirection(vector);
vector = DirectionToVector(direction) * magnitude;
return vector;
}
/// <summary>Returns a copy of the `vector` pointing in the closest direction this set has an animation for.</summary>
public virtual Vector2 Snap(Vector2 vector)
=> SnapVectorToDirection(vector);
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Collections
/************************************************************************************************************************/
/// <summary>Adds all animations from this set to the `clips`, starting from the specified `index`.</summary>
public void AddClips(AnimationClip[] clips, int index)
{
var count = ClipCount;
for (int i = 0; i < count; i++)
clips[index + i] = GetClip(i);
}
/// <summary>[<see cref="IAnimationClipSource"/>] Adds all animations from this set to the `clips`.</summary>
public void GetAnimationClips(List<AnimationClip> clips)
{
var count = ClipCount;
for (int i = 0; i < count; i++)
clips.Add(GetClip(i));
}
/************************************************************************************************************************/
/// <summary>
/// Adds unit vectors corresponding to each of the animations in this set to the `directions`, starting from
/// the specified `index`.
/// </summary>
public void AddDirections(Vector2[] directions, int index)
{
var count = ClipCount;
for (int i = 0; i < count; i++)
directions[index + i] = GetDirection(i);
}
/************************************************************************************************************************/
/// <summary>Calls <see cref="AddClips"/> and <see cref="AddDirections"/>.</summary>
public void AddClipsAndDirections(AnimationClip[] clips, Vector2[] directions, int index)
{
AddClips(clips, index);
AddDirections(directions, index);
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6eceeb59a892d074db28203df8e4cd3a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,326 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
using System;
using UnityEngine;
namespace Animancer
{
/// <summary>A set of up/right/down/left animations with diagonals as well.</summary>
/// <remarks>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/playing/directional-sets">
/// Directional Animation Sets</see>
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer/DirectionalAnimationSet8
///
[CreateAssetMenu(
menuName = Strings.MenuPrefix + "Directional Animation Set/8 Directions",
order = Strings.AssetMenuOrder + 4)]
[AnimancerHelpUrl(typeof(DirectionalAnimationSet8))]
public class DirectionalAnimationSet8 : DirectionalAnimationSet
{
/************************************************************************************************************************/
[SerializeField]
private AnimationClip _UpRight;
/// <summary>[<see cref="SerializeField"/>] The animation facing diagonally up-right ~(0.7, 0.7).</summary>
/// <exception cref="ArgumentException"><see cref="AllowSetClips"/> was not called before setting this value.</exception>
public AnimationClip UpRight
{
get => _UpRight;
set
{
AssertCanSetClips();
_UpRight = value;
AnimancerUtilities.SetDirty(this);
}
}
/************************************************************************************************************************/
[SerializeField]
private AnimationClip _DownRight;
/// <summary>[<see cref="SerializeField"/>] The animation facing diagonally down-right ~(0.7, -0.7).</summary>
/// <exception cref="ArgumentException"><see cref="AllowSetClips"/> was not called before setting this value.</exception>
public AnimationClip DownRight
{
get => _DownRight;
set
{
AssertCanSetClips();
_DownRight = value;
AnimancerUtilities.SetDirty(this);
}
}
/************************************************************************************************************************/
[SerializeField]
private AnimationClip _DownLeft;
/// <summary>[<see cref="SerializeField"/>] The animation facing diagonally down-left ~(-0.7, -0.7).</summary>
/// <exception cref="ArgumentException"><see cref="AllowSetClips"/> was not called before setting this value.</exception>
public AnimationClip DownLeft
{
get => _DownLeft;
set
{
AssertCanSetClips();
_DownLeft = value;
AnimancerUtilities.SetDirty(this);
}
}
/************************************************************************************************************************/
[SerializeField]
private AnimationClip _UpLeft;
/// <summary>[<see cref="SerializeField"/>] The animation facing diagonally up-left ~(-0.7, 0.7).</summary>
/// <exception cref="ArgumentException"><see cref="AllowSetClips"/> was not called before setting this value.</exception>
public AnimationClip UpLeft
{
get => _UpLeft;
set
{
AssertCanSetClips();
_UpLeft = value;
AnimancerUtilities.SetDirty(this);
}
}
/************************************************************************************************************************/
/// <summary>Returns the animation closest to the specified `direction`.</summary>
public override AnimationClip GetClip(Vector2 direction)
{
var angle = Mathf.Atan2(direction.y, direction.x);
var octant = Mathf.RoundToInt(8 * angle / (2 * Mathf.PI) + 8) % 8;
return octant switch
{
0 => Right,
1 => _UpRight,
2 => Up,
3 => _UpLeft,
4 => Left,
5 => _DownLeft,
6 => Down,
7 => _DownRight,
_ => throw new ArgumentOutOfRangeException("Invalid octant"),
};
}
/************************************************************************************************************************/
#region Directions
/************************************************************************************************************************/
/// <summary>Constants for each of the diagonal directions.</summary>
/// <remarks>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/playing/directional-sets">
/// Directional Animation Sets</see>
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer/Diagonals
///
public static class Diagonals
{
/************************************************************************************************************************/
/// <summary>1 / (Square Root of 2).</summary>
public const float OneOverSqrt2 = 0.70710678118f;
/// <summary>A vector with a magnitude of 1 pointing up to the right.</summary>
/// <remarks>The value is approximately (0.7, 0.7).</remarks>
public static Vector2 UpRight => new(OneOverSqrt2, OneOverSqrt2);
/// <summary>A vector with a magnitude of 1 pointing down to the right.</summary>
/// <remarks>The value is approximately (0.7, -0.7).</remarks>
public static Vector2 DownRight => new(OneOverSqrt2, -OneOverSqrt2);
/// <summary>A vector with a magnitude of 1 pointing down to the left.</summary>
/// <remarks>The value is approximately (-0.7, -0.7).</remarks>
public static Vector2 DownLeft => new(-OneOverSqrt2, -OneOverSqrt2);
/// <summary>A vector with a magnitude of 1 pointing up to the left.</summary>
/// <remarks>The value is approximately (-0.707, 0.707).</remarks>
public static Vector2 UpLeft => new(-OneOverSqrt2, OneOverSqrt2);
/************************************************************************************************************************/
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override int ClipCount
=> 8;
/************************************************************************************************************************/
/// <summary>Up, Right, Down, Left, or their diagonals.</summary>
/// <remarks>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/playing/directional-sets">
/// Directional Animation Sets</see>
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer/Direction
///
public new enum Direction
{
/// <summary><see cref="Vector2.up"/>.</summary>
Up,
/// <summary><see cref="Vector2.right"/>.</summary>
Right,
/// <summary><see cref="Vector2.down"/>.</summary>
Down,
/// <summary><see cref="Vector2.left"/>.</summary>
Left,
/// <summary><see cref="Vector2"/>(0.7..., 0.7...).</summary>
UpRight,
/// <summary><see cref="Vector2"/>(0.7..., -0.7...).</summary>
DownRight,
/// <summary><see cref="Vector2"/>(-0.7..., -0.7...).</summary>
DownLeft,
/// <summary><see cref="Vector2"/>(-0.7..., 0.7...).</summary>
UpLeft,
}
/************************************************************************************************************************/
protected override string GetDirectionName(int direction)
=> ((Direction)direction).ToString();
/************************************************************************************************************************/
/// <summary>Returns the animation associated with the specified `direction`.</summary>
public AnimationClip GetClip(Direction direction)
=> direction switch
{
Direction.Up => Up,
Direction.Right => Right,
Direction.Down => Down,
Direction.Left => Left,
Direction.UpRight => _UpRight,
Direction.DownRight => _DownRight,
Direction.DownLeft => _DownLeft,
Direction.UpLeft => _UpLeft,
_ => throw AnimancerUtilities.CreateUnsupportedArgumentException(direction),
};
public override AnimationClip GetClip(int direction)
=> GetClip((Direction)direction);
/************************************************************************************************************************/
/// <summary>Sets the animation associated with the specified `direction`.</summary>
public void SetClip(Direction direction, AnimationClip clip)
{
switch (direction)
{
case Direction.Up: Up = clip; break;
case Direction.Right: Right = clip; break;
case Direction.Down: Down = clip; break;
case Direction.Left: Left = clip; break;
case Direction.UpRight: UpRight = clip; break;
case Direction.DownRight: DownRight = clip; break;
case Direction.DownLeft: DownLeft = clip; break;
case Direction.UpLeft: UpLeft = clip; break;
default: throw AnimancerUtilities.CreateUnsupportedArgumentException(direction);
}
}
public override void SetClip(int direction, AnimationClip clip)
=> SetClip((Direction)direction, clip);
/************************************************************************************************************************/
/// <summary>Returns a vector representing the specified `direction`.</summary>
public static Vector2 DirectionToVector(Direction direction)
=> direction switch
{
Direction.Up => Vector2.up,
Direction.Right => Vector2.right,
Direction.Down => Vector2.down,
Direction.Left => Vector2.left,
Direction.UpRight => Diagonals.UpRight,
Direction.DownRight => Diagonals.DownRight,
Direction.DownLeft => Diagonals.DownLeft,
Direction.UpLeft => Diagonals.UpLeft,
_ => throw AnimancerUtilities.CreateUnsupportedArgumentException(direction),
};
public override Vector2 GetDirection(int direction)
=> DirectionToVector((Direction)direction);
/************************************************************************************************************************/
/// <summary>Returns the direction closest to the specified `vector`.</summary>
public new static Direction VectorToDirection(Vector2 vector)
{
var angle = Mathf.Atan2(vector.y, vector.x);
var octant = Mathf.RoundToInt(8 * angle / (2 * Mathf.PI) + 8) % 8;
return octant switch
{
0 => Direction.Right,
1 => Direction.UpRight,
2 => Direction.Up,
3 => Direction.UpLeft,
4 => Direction.Left,
5 => Direction.DownLeft,
6 => Direction.Down,
7 => Direction.DownRight,
_ => throw new ArgumentOutOfRangeException("Invalid octant"),
};
}
/************************************************************************************************************************/
/// <summary>
/// Returns a copy of the `vector` pointing in the closest direction
/// which this set type has an animation for.
/// </summary>
public new static Vector2 SnapVectorToDirection(Vector2 vector)
{
var magnitude = vector.magnitude;
var direction = VectorToDirection(vector);
vector = DirectionToVector(direction) * magnitude;
return vector;
}
public override Vector2 Snap(Vector2 vector)
=> SnapVectorToDirection(vector);
/************************************************************************************************************************/
/// <inheritdoc/>
public override int SetClipByName(AnimationClip clip)
{
var name = clip.name;
var directionCount = ClipCount;
for (int i = directionCount - 1; i >= 0; i--)
{
if (name.Contains(GetDirectionName(i)))
{
SetClip(i, clip);
return i;
}
}
return -1;
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8b5b6ac0c7ebd7b41b307d920db7b245
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,402 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
using UnityEngine;
namespace Animancer
{
/// <summary>A <see cref="DirectionalAnimations3D{T}"/> using <see cref="int"/> as the group type.</summary>
///
/// <remarks>
/// <strong>Sample:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/samples/sprites/character-3d">
/// Directional Character 3D</see>
/// </remarks>
///
/// https://kybernetik.com.au/animancer/api/Animancer/DirectionalAnimations3D
///
[AddComponentMenu(Strings.MenuPrefix + "Directional Animations 3D")]
[AnimancerHelpUrl(typeof(DirectionalAnimations3D))]
public class DirectionalAnimations3D : DirectionalAnimations3D<int> { }
/************************************************************************************************************************/
/// <summary>
/// A component which manages a screen-facing billboard and plays animations from a
/// <see cref="DirectionalAnimationSet"/> to make it look like a <see cref="Sprite"/>
/// based character is facing a particular direction in 3D space.
/// </summary>
///
/// <remarks>
/// <strong>Sample:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/samples/sprites/character-3d">
/// Directional Character 3D</see>
/// </remarks>
///
/// https://kybernetik.com.au/animancer/api/Animancer/DirectionalAnimations3D_1
///
[AnimancerHelpUrl(typeof(DirectionalAnimations3D<>))]
public class DirectionalAnimations3D<TGroup> : MonoBehaviour
{
/************************************************************************************************************************/
#region Fields and Properties
/************************************************************************************************************************/
[SerializeField]
[Tooltip("The object to rotate according to the " + nameof(Mode))]
private Transform _Transform;
/// <summary>[<see cref="SerializeField"/>]
/// The object to rotate according to the <see cref="Mode"/>.
/// </summary>
/// <remarks>Uses this <see cref="Component.transform"/> by default.</remarks>
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;
/// <summary>[<see cref="SerializeField"/>]
/// The <see cref="UnityEngine.Camera"/> to make the <see cref="Transform"/> face towards.
/// </summary>
/// <remarks>
/// Leave this <c>null</c> to automatically use the <see cref="Camera.main"/>.
/// </remarks>
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;
/// <summary>[<see cref="SerializeField"/>]
/// The <see cref="AnimancerComponent"/> to play animations on.
/// </summary>
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;
/// <summary>[<see cref="SerializeField"/>]
/// The animations to choose between based on the <see cref="Forward"/> direction.
/// </summary>
/// <remarks>Forwards in 3D space corresponds to the Up animation.</remarks>
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;
/// <summary>[<see cref="SerializeField"/>]
/// The World-Space direction this character is facing used to select which animation to play.
/// </summary>
public Vector3 Forward
{
get => _Forward;
set
{
_Forward = value;
if (!enabled)
PlayCurrentAnimation(TimeSynchronizer.CurrentGroup);
}
}
/************************************************************************************************************************/
/// <summary>Functions used to face the <see cref="Transform"/> towards the <see cref="Camera"/>.</summary>
public enum BillboardMode
{
/// <summary>Don't control the <see cref="Transform"/>.</summary>
None,
/// <summary>Copy the <see cref="Camera"/> 's rotation.</summary>
MatchRotation,
/// <summary>Face the <see cref="Camera"/>'s position.</summary>
FacePosition,
/// <summary>As <see cref="MatchRotation"/>, but only rotate around the Y axis.</summary>
UprightMatchRotation,
/// <summary>As <see cref="FacePosition"/>, but only rotate around the Y axis.</summary>
UprightFacePosition,
/// <summary>
/// As <see cref="UprightMatchRotation"/>,
/// and also scale on the Y axis to maintain the same screen size
/// regardless of the <see cref="Camera"/>'s Euler X Angle.</summary>
/// <remarks>Only use this mode with an Orthographic Camera</remarks>
UprightMatchRotationStretched,
/// <summary>
/// As <see cref="UprightFacePosition"/>,
/// and also scale on the Y axis to maintain the same screen size
/// regardless of the <see cref="Camera"/>'s Euler X Angle.</summary>
/// <remarks>Only use this mode with an Orthographic Camera</remarks>
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;
/// <summary>[<see cref="SerializeField"/>]
/// The function used to face the <see cref="Transform"/> towards the <see cref="Camera"/>.
/// </summary>
public BillboardMode Mode
{
get => _Mode;
set
{
_Mode = value;
ResetScaleIfNotStretched();
}
}
/************************************************************************************************************************/
/// <summary>
/// Maintains the <see cref="AnimancerState.NormalizedTime"/> when swapping between animations.
/// </summary>
public readonly TimeSynchronizer<TGroup>
TimeSynchronizer = new(default, true);
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Methods
/************************************************************************************************************************/
/// <summary>
/// Finds missing references,
/// samples the current animation,
/// and resets the scale to 1 if not using a stretched mode.
/// </summary>
protected virtual void OnValidate()
{
gameObject.GetComponentInParentOrChildren(ref _Transform);
gameObject.GetComponentInParentOrChildren(ref _Animancer);
if (TryGetCurrentAnimation(out var animation))
AnimancerUtilities.EditModeSampleAnimation(animation, _Animancer);
ResetScaleIfNotStretched();
}
/************************************************************************************************************************/
/// <summary>
/// Finds missing references,
/// samples the current animation,
/// and resets the scale to 1 if not using a stretched mode.
/// </summary>
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<Renderer>();
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);
}
/************************************************************************************************************************/
/// <summary>
/// Applies the <see cref="Mode"/> then plays the appropriate animation
/// based on the current rotation and <see cref="Forward"/> direction.
/// </summary>
protected virtual void Update()
{
UpdateTransform();
PlayCurrentAnimation(TimeSynchronizer.CurrentGroup);
}
/************************************************************************************************************************/
/// <summary>Applies the <see cref="Mode"/>.</summary>
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;
}
}
/************************************************************************************************************************/
/// <summary>
/// Scales the <see cref="Transform"/> on the Y axis to maintain the same screen size
/// regardless of the <see cref="Camera"/>'s Euler X Angle.
/// </summary>
/// <remarks>This calculation only makes sense with an orthographic camera.</remarks>
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);
}
/// <summary>
/// Resets the <see cref="Transform.localScale"/> to 1 if not using a stretched <see cref="Mode"/>.
/// </summary>
private void ResetScaleIfNotStretched()
{
if (_Transform == null)
return;
switch (_Mode)
{
case BillboardMode.UprightMatchRotationStretched:
case BillboardMode.UprightFacePositionStretched:
break;
default:
_Transform.localScale = Vector3.one;
break;
}
}
/************************************************************************************************************************/
/// <summary>
/// Sets the <see cref="Animations"/> and plays the appropriate animation
/// based on the current rotation and <see cref="Forward"/> direction.
/// </summary>
public void SetAnimations(DirectionalAnimationSet animations, TGroup group = default)
{
_Animations = animations;
PlayCurrentAnimation(group);
}
/************************************************************************************************************************/
/// <summary>
/// Plays the appropriate animation based on the current rotation and <see cref="Forward"/> direction.
/// </summary>
/// <remarks>
/// If the `group` is the same as the previous, the new animation will be given the same
/// <see cref="AnimancerState.NormalizedTime"/> as the previous.
/// </remarks>
public void PlayCurrentAnimation(TGroup group)
{
if (TryGetCurrentAnimation(out var animation))
{
TimeSynchronizer.StoreTime(_Animancer);
_Animancer.Play(animation);
TimeSynchronizer.SyncTime(_Animancer, group);
}
}
/************************************************************************************************************************/
/// <summary>
/// Tries to get an appropriate animation based on the current rotation and <see cref="Forward"/> direction.
/// </summary>
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
/************************************************************************************************************************/
}
}
@@ -0,0 +1,13 @@
fileFormatVersion: 2
guid: 39d4b0d1540fd634c9ad4c6a16a999c5
labels:
- Example
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,100 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Animancer
{
/// <summary>A <see cref="ClipTransition"/> which gets its clip from a <see cref="DirectionalAnimationSet"/>.</summary>
///
/// <remarks>
/// <para></para>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/playing/directional-sets">
/// Directional Animation Sets</see>
/// <para></para>
/// <strong>Example:</strong><code>
/// // Leave the Clip field empty in the Inspector and assign its AnimationSet instead.
/// [SerializeField] private DirectionalClipTransition _Transition;
///
/// ...
///
/// // Then you can just call SetDirection and Play it like any other transition.
/// // All of the transition's details like Fade Duration and Events will be applied to whichever clip is plays.
/// _Transition.SetDirection(Vector2.right);
/// _Animancer.Play(_Transition);
/// </code></remarks>
///
/// https://kybernetik.com.au/animancer/api/Animancer/DirectionalClipTransition
///
[Serializable]
public class DirectionalClipTransition : ClipTransition,
ICopyable<DirectionalClipTransition>
{
/************************************************************************************************************************/
[SerializeField]
[Tooltip("The animations which used to determine the " + nameof(Clip))]
private DirectionalAnimationSet _AnimationSet;
/// <summary>[<see cref="SerializeField"/>]
/// The <see cref="DirectionalAnimationSet"/> used to determine the <see cref="ClipTransition.Clip"/>.
/// </summary>
public ref DirectionalAnimationSet AnimationSet
=> ref _AnimationSet;
/// <inheritdoc/>
public override UnityEngine.Object MainObject
=> _AnimationSet;
/// <summary>The name of the serialized backing field of <see cref="AnimationSet"/>.</summary>
public const string AnimationSetField = nameof(_AnimationSet);
/************************************************************************************************************************/
/// <summary>Sets the <see cref="ClipTransition.Clip"/> from the <see cref="AnimationSet"/>.</summary>
public void SetDirection(Vector2 direction)
=> Clip = _AnimationSet.GetClip(direction);
/// <summary>Sets the <see cref="ClipTransition.Clip"/> from the <see cref="AnimationSet"/>.</summary>
public void SetDirection(int direction)
=> Clip = _AnimationSet.GetClip(direction);
/// <summary>Sets the <see cref="ClipTransition.Clip"/> from the <see cref="AnimationSet"/>.</summary>
public void SetDirection(DirectionalAnimationSet.Direction direction)
=> Clip = _AnimationSet.GetClip(direction);
/// <summary>Sets the <see cref="ClipTransition.Clip"/> from the <see cref="AnimationSet"/>.</summary>
public void SetDirection(DirectionalAnimationSet8.Direction direction)
=> Clip = _AnimationSet.GetClip((int)direction);
/************************************************************************************************************************/
/// <inheritdoc/>
public override void GatherAnimationClips(ICollection<AnimationClip> clips)
{
base.GatherAnimationClips(clips);
clips.GatherFromSource(_AnimationSet);
}
/************************************************************************************************************************/
/// <inheritdoc/>
public virtual void CopyFrom(DirectionalClipTransition copyFrom, CloneContext context)
{
base.CopyFrom(copyFrom, context);
if (copyFrom == null)
{
_AnimationSet = default;
return;
}
_AnimationSet = copyFrom._AnimationSet;
}
/************************************************************************************************************************/
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5125aedb0106a4443a684797d8c73344
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: