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