// Animancer // https://kybernetik.com.au/animancer // Copyright 2021 Kybernetik //
using System.Collections.Generic;
using UnityEngine;
using Object = UnityEngine.Object;
#if UNITY_EDITOR
using System.IO;
using UnityEditor;
using UnityEditor.Animations;
#endif
namespace Animancer
{
/// A based .
///
/// Documentation: Transition Assets
///
/// When adding a to any derived classes, you can use
/// and .
///
/// If you are using s, consider using an
/// instead of referencing this asset
/// directly in order to avoid common issues with shared events.
///
/// https://kybernetik.com.au/animancer/api/Animancer/AnimancerTransitionAsset_1
///
[HelpURL(Strings.DocsURLs.APIDocumentation + "/" + nameof(AnimancerTransitionAsset) + "_1")]
public class AnimancerTransitionAsset : ScriptableObject, ITransition, IWrapper, IAnimationClipSource
where TTransition : ITransition
{
/************************************************************************************************************************/
[SerializeReference]
private TTransition _Transition;
/// []
/// The wrapped by this .
///
///
/// WARNING: the holds the most recently played state, so
/// if you are sharing this transition between multiple objects it will only remember one of them.
/// Consider using .
///
/// You can use or
/// to get or create the state for a
/// specific object.
///
public ref TTransition Transition => ref _Transition;
/// Returns the wrapped by this .
public virtual ITransition GetTransition() => _Transition;
///
object IWrapper.WrappedObject => GetTransition();
/************************************************************************************************************************/
#if UNITY_EDITOR
/// [Editor-Only] Assigns a default to the field.
protected virtual void Reset()
{
_Transition = Editor.TypeSelectionButton.CreateDefaultInstance();
}
#endif
/************************************************************************************************************************/
/// Can this transition create a valid ?
public virtual bool IsValid => GetTransition().IsValid();
///
public virtual float FadeDuration => GetTransition().FadeDuration;
///
public virtual object Key => GetTransition().Key;
///
public virtual FadeMode FadeMode => GetTransition().FadeMode;
///
public virtual AnimancerState CreateState() => GetTransition().CreateState();
///
public virtual void Apply(AnimancerState state)
{
GetTransition().Apply(state);
state.SetDebugName(name);
}
/************************************************************************************************************************/
/// []
/// Calls .
///
public virtual void GetAnimationClips(List clips) => clips.GatherFromSource(GetTransition());
/************************************************************************************************************************/
}
}
/************************************************************************************************************************/
#if UNITY_EDITOR
namespace Animancer.Editor
{
/// A custom editor for s.
///
/// This class contains several context menu functions for generating s based on
/// Animator Controller States.
///
[CustomEditor(typeof(AnimancerTransitionAsset<>), true), CanEditMultipleObjects]
internal class AnimancerTransitionAssetEditor : ScriptableObjectEditor
{
/************************************************************************************************************************/
/// Creates an from the .
[MenuItem("CONTEXT/" + nameof(AnimatorState) + "/Generate Transition")]
[MenuItem("CONTEXT/" + nameof(BlendTree) + "/Generate Transition")]
[MenuItem("CONTEXT/" + nameof(AnimatorStateTransition) + "/Generate Transition")]
[MenuItem("CONTEXT/" + nameof(AnimatorStateMachine) + "/Generate Transitions")]
private static void GenerateTransition(MenuCommand command)
{
var context = command.context;
if (context is AnimatorState state)
{
Selection.activeObject = GenerateTransition(state);
}
else if (context is BlendTree blendTree)
{
Selection.activeObject = GenerateTransition(null, blendTree);
}
else if (context is AnimatorStateTransition transition)
{
Selection.activeObject = GenerateTransition(transition);
}
else if (context is AnimatorStateMachine stateMachine)// Layer or Sub-State Machine.
{
Selection.activeObject = GenerateTransitions(stateMachine);
}
}
/************************************************************************************************************************/
/// Creates an from the `state`.
private static Object GenerateTransition(AnimatorState state)
=> GenerateTransition(state, state.motion);
/************************************************************************************************************************/
/// Creates an from the `motion`.
private static Object GenerateTransition(Object originalAsset, Motion motion)
{
if (motion is BlendTree blendTree)
{
return GenerateTransition(originalAsset as AnimatorState, blendTree);
}
else if (motion is AnimationClip || motion == null)
{
var asset = CreateInstance();
asset.Transition = new ClipTransition
{
Clip = (AnimationClip)motion,
};
GetDetailsFromState(originalAsset as AnimatorState, asset.Transition);
SaveTransition(originalAsset, asset);
return asset;
}
else
{
Debug.LogError($"Unsupported {nameof(Motion)} Type: {motion.GetType()}");
return null;
}
}
/************************************************************************************************************************/
/// Initializes the `transition` based on the `state`.
private static void GetDetailsFromState(AnimatorState state, ITransitionDetailed transition)
{
if (state == null ||
transition == null)
return;
transition.Speed = state.speed;
var isForwards = state.speed >= 0;
var defaultEndTime = AnimancerEvent.Sequence.GetDefaultNormalizedEndTime(state.speed);
var endTime = defaultEndTime;
var exitTransitions = state.transitions;
for (int i = 0; i < exitTransitions.Length; i++)
{
var exitTransition = exitTransitions[i];
if (exitTransition.hasExitTime)
{
if (isForwards)
{
if (endTime > exitTransition.exitTime)
endTime = exitTransition.exitTime;
}
else
{
if (endTime < exitTransition.exitTime)
endTime = exitTransition.exitTime;
}
}
}
if (endTime != defaultEndTime && AnimancerUtilities.TryGetWrappedObject(transition, out IHasEvents events))
{
if (events.SerializedEvents == null)
events.SerializedEvents = new AnimancerEvent.Sequence.Serializable();
events.SerializedEvents.SetNormalizedEndTime(endTime);
}
}
/************************************************************************************************************************/
/// Creates an from the `blendTree`.
private static Object GenerateTransition(AnimatorState state, BlendTree blendTree)
{
var asset = CreateTransition(blendTree);
if (asset == null)
return null;
if (state != null)
asset.name = state.name;
AnimancerUtilities.TryGetWrappedObject(asset, out ITransitionDetailed detailed);
GetDetailsFromState(state, detailed);
SaveTransition(blendTree, asset);
return asset;
}
/************************************************************************************************************************/
/// Creates an from the `transition`.
private static Object GenerateTransition(AnimatorStateTransition transition)
{
Object animancerTransition = null;
if (transition.destinationStateMachine != null)
animancerTransition = GenerateTransitions(transition.destinationStateMachine);
if (transition.destinationState != null)
animancerTransition = GenerateTransition(transition.destinationState);
return animancerTransition;
}
/************************************************************************************************************************/
/// Creates s from all states in the `stateMachine`.
private static Object GenerateTransitions(AnimatorStateMachine stateMachine)
{
Object transition = null;
foreach (var child in stateMachine.stateMachines)
transition = GenerateTransitions(child.stateMachine);
foreach (var child in stateMachine.states)
transition = GenerateTransition(child.state);
return transition;
}
/************************************************************************************************************************/
/// Creates an from the `blendTree`.
private static Object CreateTransition(BlendTree blendTree)
{
switch (blendTree.blendType)
{
case BlendTreeType.Simple1D:
var linearAsset = CreateInstance();
InitializeChildren(ref linearAsset.Transition, blendTree);
return linearAsset;
case BlendTreeType.SimpleDirectional2D:
case BlendTreeType.FreeformDirectional2D:
var directionalAsset = CreateInstance();
directionalAsset.Transition = new MixerTransition2D
{
Type = MixerTransition2D.MixerType.Directional
};
InitializeChildren(ref directionalAsset.Transition, blendTree);
return directionalAsset;
case BlendTreeType.FreeformCartesian2D:
var cartesianAsset = CreateInstance();
cartesianAsset.Transition = new MixerTransition2D
{
Type = MixerTransition2D.MixerType.Cartesian
};
InitializeChildren(ref cartesianAsset.Transition, blendTree);
return cartesianAsset;
case BlendTreeType.Direct:
var manualAsset = CreateInstance();
InitializeChildren(ref manualAsset.Transition, blendTree);
return manualAsset;
default:
Debug.LogError($"Unsupported {nameof(BlendTreeType)}: {blendTree.blendType}");
return null;
}
}
/************************************************************************************************************************/
/// Initializes the `transition` based on the .
private static void InitializeChildren(ref LinearMixerTransition transition, BlendTree blendTree)
{
var children = InitializeChildren(ref transition, blendTree);
transition.Thresholds = new float[children.Length];
for (int i = 0; i < children.Length; i++)
transition.Thresholds[i] = children[i].threshold;
}
/// Initializes the `transition` based on the .
private static void InitializeChildren(ref MixerTransition2D transition, BlendTree blendTree)
{
var children = InitializeChildren>(ref transition, blendTree);
transition.Thresholds = new Vector2[children.Length];
for (int i = 0; i < children.Length; i++)
transition.Thresholds[i] = children[i].position;
}
/// Initializes the `transition` based on the .
private static ChildMotion[] InitializeChildren(ref TTransition transition, BlendTree blendTree)
where TTransition : ManualMixerTransition, new()
where TState : ManualMixerState
{
transition = new TTransition();
var children = blendTree.children;
transition.Animations = new Object[children.Length];
float[] speeds = new float[children.Length];
var hasCustomSpeeds = false;
for (int i = 0; i < children.Length; i++)
{
var child = children[i];
transition.Animations[i] = child.motion is AnimationClip ?
child.motion :
(Object)GenerateTransition(blendTree, child.motion);
if ((speeds[i] = child.timeScale) != 1)
hasCustomSpeeds = true;
}
if (hasCustomSpeeds)
transition.Speeds = speeds;
return children;
}
/************************************************************************************************************************/
/// Saves the `transition` in the same folder as the `originalAsset`.
private static void SaveTransition(Object originalAsset, Object transition)
{
if (string.IsNullOrEmpty(transition.name))
transition.name = originalAsset.name;
var path = AssetDatabase.GetAssetPath(originalAsset);
path = Path.GetDirectoryName(path);
path = Path.Combine(path, transition.name + ".asset");
path = AssetDatabase.GenerateUniqueAssetPath(path);
AssetDatabase.CreateAsset(transition, path);
Debug.Log($"Saved {path}", transition);
}
/************************************************************************************************************************/
}
}
#endif