You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
218 lines
9.1 KiB
C#
218 lines
9.1 KiB
C#
3 months ago
|
// Animancer // Copyright 2020 Kybernetik //
|
||
|
|
||
|
using System;
|
||
|
using UnityEngine;
|
||
|
using Object = UnityEngine.Object;
|
||
|
|
||
|
namespace Animancer
|
||
|
{
|
||
|
/// <summary>Various extension methods and utilities.</summary>
|
||
|
public static partial class AnimancerUtilities
|
||
|
{
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
/// <summary>[Animancer Extension] Loops the `value` so that <c>0 <= value < 1</c>.</summary>
|
||
|
/// <remarks>This is more efficient than using <see cref="Mathf.Repeat"/> with a length of 1.</remarks>
|
||
|
public static float Wrap01(this float value)
|
||
|
{
|
||
|
var valueAsDouble = (double)value;
|
||
|
return (float)(valueAsDouble - Math.Floor(valueAsDouble));
|
||
|
}
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
/// <summary>[Animancer Extension]
|
||
|
/// Adds the specified type of <see cref="IAnimancerComponent"/>, links it to the `animator`, and returns it.
|
||
|
/// </summary>
|
||
|
public static T AddAnimancerComponent<T>(this Animator animator) where T : Component, IAnimancerComponent
|
||
|
{
|
||
|
var animancer = animator.gameObject.AddComponent<T>();
|
||
|
animancer.Animator = animator;
|
||
|
return animancer;
|
||
|
}
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
/// <summary>[Animancer Extension]
|
||
|
/// Returns the <see cref="IAnimancerComponent"/> on the same <see cref="GameObject"/> as the `animator` if
|
||
|
/// there is one. Otherwise this method adds a new one and returns it.
|
||
|
/// </summary>
|
||
|
public static T GetOrAddAnimancerComponent<T>(this Animator animator) where T : Component, IAnimancerComponent
|
||
|
{
|
||
|
var animancer = animator.GetComponent<T>();
|
||
|
if (animancer != null)
|
||
|
return animancer;
|
||
|
else
|
||
|
return animator.AddAnimancerComponent<T>();
|
||
|
}
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
/// <summary>
|
||
|
/// Checks if any <see cref="AnimationClip"/> in the `source` has an animation event with the specified
|
||
|
/// `functionName`.
|
||
|
/// </summary>
|
||
|
public static bool HasEvent(IAnimationClipCollection source, string functionName)
|
||
|
{
|
||
|
var clips = ObjectPool.AcquireSet<AnimationClip>();
|
||
|
source.GatherAnimationClips(clips);
|
||
|
|
||
|
foreach (var clip in clips)
|
||
|
{
|
||
|
if (HasEvent(clip, functionName))
|
||
|
{
|
||
|
ObjectPool.Release(clips);
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
ObjectPool.Release(clips);
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
/// <summary>Checks if the `clip` has an animation event with the specified `functionName`.</summary>
|
||
|
public static bool HasEvent(AnimationClip clip, string functionName)
|
||
|
{
|
||
|
var events = clip.events;
|
||
|
var count = events.Length;
|
||
|
for (int i = 0; i < count; i++)
|
||
|
{
|
||
|
if (events[i].functionName == functionName)
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
/// <summary>[Pro-Only]
|
||
|
/// Calculates all thresholds in the `mixer` using the <see cref="AnimancerState.AverageVelocity"/> of each
|
||
|
/// state on the X and Z axes.
|
||
|
/// <para></para>
|
||
|
/// Note that this method requires the <c>Root Transform Position (XZ) -> Bake Into Pose</c> toggle to be
|
||
|
/// disabled in the Import Settings of each <see cref="AnimationClip"/> in the mixer.
|
||
|
/// </summary>
|
||
|
public static void CalculateThresholdsFromAverageVelocityXZ(this MixerState<Vector2> mixer)
|
||
|
{
|
||
|
mixer.ValidateThresholdCount();
|
||
|
|
||
|
var count = mixer.States.Length;
|
||
|
for (int i = 0; i < count; i++)
|
||
|
{
|
||
|
var state = mixer.States[i];
|
||
|
if (state == null)
|
||
|
continue;
|
||
|
|
||
|
var averageVelocity = state.AverageVelocity;
|
||
|
mixer.SetThreshold(i, new Vector2(averageVelocity.x, averageVelocity.z));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
/// <summary>[Editor-Conditional] Marks the `target` as dirty.</summary>
|
||
|
[System.Diagnostics.Conditional(Strings.EditorOnly)]
|
||
|
public static void SetDirty(Object target)
|
||
|
{
|
||
|
#if UNITY_EDITOR
|
||
|
UnityEditor.EditorUtility.SetDirty(target);
|
||
|
#endif
|
||
|
}
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
/// <summary>[Editor-Conditional]
|
||
|
/// If there are multiple components which inherit from <typeparamref name="T"/>, the first one is changed to
|
||
|
/// the type of the second and any after the first are destroyed. This allows you to change the type without
|
||
|
/// losing the values of any serialized fields they share.
|
||
|
/// <para></para>
|
||
|
/// The `currentComponent` is used to determine which <see cref="GameObject"/> to examine and the base
|
||
|
/// component type <typeparamref name="T"/>.
|
||
|
/// </summary>
|
||
|
/// <example><code>
|
||
|
/// protected void Reset()
|
||
|
/// {
|
||
|
/// AnimancerUtilities.IfMultiComponentThenChangeType(this);
|
||
|
/// }
|
||
|
/// </code></example>
|
||
|
[System.Diagnostics.Conditional(Strings.EditorOnly)]
|
||
|
public static void IfMultiComponentThenChangeType<T>(T currentComponent) where T : MonoBehaviour
|
||
|
{
|
||
|
#if UNITY_EDITOR
|
||
|
if (UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode)
|
||
|
return;
|
||
|
|
||
|
// If there is already another instance of this component on the same object, delete this new instance and
|
||
|
// change the original's type to match this one.
|
||
|
var components = currentComponent.GetComponents<T>();
|
||
|
if (components.Length > 1)
|
||
|
{
|
||
|
var oldComponent = components[0];
|
||
|
var newComponent = components[1];
|
||
|
|
||
|
if (oldComponent.GetType() != newComponent.GetType())
|
||
|
{
|
||
|
// All we have to do is change the Script field to the new type and Unity will immediately deserialize
|
||
|
// the existing data as that type, so any fields shared between both types will keep their data.
|
||
|
|
||
|
using (var serializedObject = new UnityEditor.SerializedObject(oldComponent))
|
||
|
{
|
||
|
var scriptProperty = serializedObject.FindProperty("m_Script");
|
||
|
scriptProperty.objectReferenceValue = UnityEditor.MonoScript.FromMonoBehaviour(newComponent);
|
||
|
serializedObject.ApplyModifiedProperties();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Destroy all components other than the first (the oldest).
|
||
|
UnityEditor.EditorApplication.delayCall += () =>
|
||
|
{
|
||
|
var i = 1;
|
||
|
for (; i < components.Length; i++)
|
||
|
{
|
||
|
Object.DestroyImmediate(components[i], true);
|
||
|
}
|
||
|
};
|
||
|
}
|
||
|
#endif
|
||
|
}
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
/// <summary>[Editor-Conditional]
|
||
|
/// Plays the specified `clip` if called in Edit Mode and optionally pauses it immediately.
|
||
|
/// </summary>
|
||
|
/// <remarks>
|
||
|
/// Before Unity 2018.3, playing animations in Edit Mode didn't work properly.
|
||
|
/// </remarks>
|
||
|
[System.Diagnostics.Conditional(Strings.EditorOnly)]
|
||
|
public static void EditModePlay(IAnimancerComponent animancer, AnimationClip clip, bool pauseImmediately = true)
|
||
|
{
|
||
|
#if UNITY_EDITOR
|
||
|
if (UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode ||
|
||
|
animancer == null || clip == null)
|
||
|
return;
|
||
|
|
||
|
// Delay for a frame in case this was called at a bad time (such as during OnValidate).
|
||
|
UnityEditor.EditorApplication.delayCall += () =>
|
||
|
{
|
||
|
if (UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode ||
|
||
|
animancer == null || clip == null)
|
||
|
return;
|
||
|
|
||
|
animancer.Playable.Play(clip);
|
||
|
|
||
|
if (pauseImmediately)
|
||
|
{
|
||
|
animancer.Playable.Evaluate();
|
||
|
animancer.Playable.PauseGraph();
|
||
|
}
|
||
|
};
|
||
|
#endif
|
||
|
}
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
}
|
||
|
}
|
||
|
|