// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
#if UNITY_EDITOR && UNITY_IMGUI
using System;
using UnityEditor;
using UnityEngine;
using static Animancer.Editor.AnimancerGUI;
namespace Animancer.Editor.Previews
{
/// https://kybernetik.com.au/animancer/api/Animancer.Editor.Previews/TransitionPreviewWindow
partial class TransitionPreviewWindow
{
/// Animation details for the .
///
/// Documentation:
///
/// Previews
///
[Serializable]
internal class Animations
{
/************************************************************************************************************************/
public const string
PreviousAnimationKey = "Previous Animation",
NextAnimationKey = "Next Animation";
/************************************************************************************************************************/
[NonSerialized] private AnimationClip[] _OtherAnimations;
[SerializeField]
private AnimationClip _PreviousAnimation;
public AnimationClip PreviousAnimation => _PreviousAnimation;
[SerializeField]
private AnimationClip _NextAnimation;
public AnimationClip NextAnimation => _NextAnimation;
/************************************************************************************************************************/
private static AnimancerPreviewObject PreviewObject
=> _Instance._Scene.PreviewObject;
/************************************************************************************************************************/
public void DoGUI()
{
GUILayout.BeginVertical(GUI.skin.box);
EditorGUILayout.LabelField("Preview Details", "(Not Serialized)");
var previewObject = PreviewObject;
AnimancerPreviewObjectGUI.DoModelGUI(previewObject);
using (var label = PooledGUIContent.Acquire("Previous Animation",
"The animation for the preview to play before the target transition"))
{
DoAnimationFieldGUI(label, ref _PreviousAnimation, (clip) => _PreviousAnimation = clip);
}
var graph = previewObject.Graph;
DoCurrentAnimationGUI(graph);
using (var label = PooledGUIContent.Acquire("Next Animation",
"The animation for the preview to play after the target transition"))
{
DoAnimationFieldGUI(label, ref _NextAnimation, (clip) => _NextAnimation = clip);
}
if (graph != null)
{
using (new EditorGUI.DisabledScope(!Transition.IsValid()))
{
GUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
if (graph.IsGraphPlaying)
{
if (CompactMiniButton(AnimancerIcons.PauseIcon))
graph.PauseGraph();
}
else
{
if (CompactMiniButton(AnimancerIcons.StepBackwardIcon))
StepBackward();
if (CompactMiniButton(AnimancerIcons.PlayIcon))
PlaySequence(graph);
if (CompactMiniButton(AnimancerIcons.StepForwardIcon))
StepForward();
}
GUILayout.FlexibleSpace();
GUILayout.EndHorizontal();
}
}
GUILayout.EndVertical();
}
/************************************************************************************************************************/
public void GatherAnimations()
{
AnimationGatherer.GatherFromGameObject(
PreviewObject.OriginalObject.gameObject,
ref _OtherAnimations,
true);
if (_OtherAnimations.Length > 0 &&
(_PreviousAnimation == null || _NextAnimation == null))
{
var defaultClip = _OtherAnimations[0];
var defaultClipIsIdle = false;
for (int i = 0; i < _OtherAnimations.Length; i++)
{
var clip = _OtherAnimations[i];
if (defaultClipIsIdle && clip.name.Length > defaultClip.name.Length)
continue;
if (clip.name.IndexOf("idle", StringComparison.CurrentCultureIgnoreCase) >= 0)
{
defaultClip = clip;
break;
}
}
if (_PreviousAnimation == null)
_PreviousAnimation = defaultClip;
if (_NextAnimation == null)
_NextAnimation = defaultClip;
}
}
/************************************************************************************************************************/
private void DoAnimationFieldGUI(GUIContent label, ref AnimationClip clip, Action setClip)
{
var showDropdown = !_OtherAnimations.IsNullOrEmpty();
var area = LayoutSingleLineRect();
if (DoDropdownObjectFieldGUI(area, label, showDropdown, ref clip))
{
var menu = new GenericMenu();
menu.AddItem(new("None"), clip == null, () => setClip(null));
for (int i = 0; i < _OtherAnimations.Length; i++)
{
var animation = _OtherAnimations[i];
menu.AddItem(new(animation.name), animation == clip, () => setClip(animation));
}
menu.ShowAsContext();
}
}
/************************************************************************************************************************/
private void DoCurrentAnimationGUI(AnimancerGraph animancer)
{
string text;
if (animancer != null)
{
var transition = Transition;
if (transition.IsValid() && transition.Key != null)
text = animancer.States.GetOrCreate(transition).ToString();
else
text = transition?.ToString();
}
else
{
text = _Instance._TransitionProperty.Property.GetFriendlyPath();
}
if (text != null)
EditorGUILayout.LabelField("Current Animation", text);
}
/************************************************************************************************************************/
private void PlaySequence(AnimancerGraph animancer)
{
if (_PreviousAnimation != null && _PreviousAnimation.length > 0)
{
PreviewObject.Graph.Stop();
var fromState = animancer.States.GetOrCreate(PreviousAnimationKey, _PreviousAnimation, true);
animancer.Layers[0].Play(fromState);
OnPlayAnimation();
fromState.TimeD = 0;
var warnings = OptionalWarning.UnsupportedEvents.DisableTemporarily();
fromState.Events(this).EndEvent = new(1 / fromState.Length, PlayTransition);
warnings.Enable();
}
else
{
PlayTransition();
}
PreviewObject.Graph.UnpauseGraph();
}
private void PlayTransition()
{
var transition = Transition;
var animancer = PreviewObject.Graph;
animancer.States.TryGet(transition, out var oldState);
var targetState = animancer.Layers[0].Play(transition);
OnPlayAnimation();
if (oldState != null && oldState != targetState)
oldState.Destroy();
var warnings = OptionalWarning.UnsupportedEvents.DisableTemporarily();
targetState.Events(this).OnEnd = () =>
{
if (_NextAnimation != null)
{
var fadeDuration = AnimancerEvent.GetFadeOutDuration(
targetState,
AnimancerGraph.DefaultFadeDuration);
PlayOther(NextAnimationKey, _NextAnimation, 0, fadeDuration);
OnPlayAnimation();
}
else
{
animancer.Layers[0].IncrementCommandCount();
}
};
warnings.Enable();
}
/************************************************************************************************************************/
public void OnPlayAnimation()
{
var animancer = PreviewObject.Graph;
if (animancer == null ||
animancer.States.Current == null)
return;
var state = animancer.States.Current;
state.RecreatePlayableRecursive();
var events = state.SharedEvents;
if (events != null)
{
var warnings = OptionalWarning.UnsupportedEvents | OptionalWarning.ProOnly;
warnings = warnings.DisableTemporarily();
var normalizedEndTime = events.NormalizedEndTime;
state.Events(this).NormalizedEndTime = normalizedEndTime;
warnings.Enable();
}
}
/************************************************************************************************************************/
private void StepBackward()
=> StepTime(-TransitionPreviewSettings.FrameStep);
private void StepForward()
=> StepTime(TransitionPreviewSettings.FrameStep);
private void StepTime(float timeOffset)
{
if (!TryShowTransitionPaused(out _, out _, out var state))
return;
var length = state.Length;
if (length != 0)
timeOffset /= length;
NormalizedTime += timeOffset;
}
/************************************************************************************************************************/
[SerializeField]
private float _NormalizedTime;
public float NormalizedTime
{
get => _NormalizedTime;
set
{
if (!value.IsFinite())
return;
_NormalizedTime = value;
if (!TryShowTransitionPaused(out var animancer, out var transition, out var state))
return;
var length = state.Length;
var speed = state.Speed;
var time = value * length;
var fadeDuration = transition.FadeDuration * Math.Abs(speed);
var startTime = TimelineGUI.GetStartTime(transition.NormalizedStartTime, speed, length);
var normalizedEndTime = state.NormalizedEndTime;
var endTime = normalizedEndTime * length;
var fadeOutEnd = TimelineGUI.GetFadeOutEnd(speed, endTime, length);
if (speed < 0)
{
time = length - time;
startTime = length - startTime;
value = 1 - value;
normalizedEndTime = 1 - normalizedEndTime;
endTime = length - endTime;
fadeOutEnd = length - fadeOutEnd;
}
if (time < startTime)// Previous animation.
{
if (_PreviousAnimation != null)
{
PlayOther(PreviousAnimationKey, _PreviousAnimation, value);
value = 0;
}
}
else if (time < startTime + fadeDuration)// Fade from previous animation to the target.
{
if (_PreviousAnimation != null)
{
var fromState = PlayOther(PreviousAnimationKey, _PreviousAnimation, value);
state.IsPlaying = true;
state.Weight = (time - startTime) / fadeDuration;
fromState.Weight = 1 - state.Weight;
}
}
else if (_NextAnimation != null)
{
if (value < normalizedEndTime)
{
// Just the main state.
}
else
{
var toState = PlayOther(NextAnimationKey, _NextAnimation, value - normalizedEndTime);
if (time < fadeOutEnd)// Fade from the target transition to the next animation.
{
state.IsPlaying = true;
toState.Weight = (time - endTime) / (fadeOutEnd - endTime);
state.Weight = 1 - toState.Weight;
}
// Else just the next animation.
}
}
if (speed < 0)
value = 1 - value;
state.MoveTime(state.Weight > 0 ? value : 0, true);
animancer.Evaluate();
RepaintEverything();
}
}
/************************************************************************************************************************/
private bool TryShowTransitionPaused(
out AnimancerGraph animancer, out ITransitionDetailed transition, out AnimancerState state)
{
animancer = PreviewObject.Graph;
transition = Transition;
if (animancer == null || !transition.IsValid())
{
state = null;
return false;
}
state = animancer.Layers[0].Play(transition, 0);
OnPlayAnimation();
animancer.PauseGraph();
return true;
}
/************************************************************************************************************************/
private AnimancerState PlayOther(
object key,
AnimationClip animation,
float normalizedTime,
float fadeDuration = 0)
{
var animancer = PreviewObject.Graph;
var state = animancer.States.GetOrCreate(key, animation, true);
state = animancer.Layers[0].Play(state, fadeDuration);
OnPlayAnimation();
normalizedTime *= state.Length;
state.Time = normalizedTime.IsFinite() ? normalizedTime : 0;
return state;
}
/************************************************************************************************************************/
internal class WindowMatchStateTime : Updatable
{
/************************************************************************************************************************/
public override void Update()
{
if (_Instance == null ||
!AnimancerGraph.Current.IsGraphPlaying)
return;
var transition = Transition;
if (transition == null)
return;
if (AnimancerGraph.Current.States.TryGet(transition, out var state))
_Instance._Animations._NormalizedTime = state.NormalizedTime;
}
/************************************************************************************************************************/
public override string ToString()
=> nameof(WindowMatchStateTime);
/************************************************************************************************************************/
}
/************************************************************************************************************************/
}
}
}
#endif