// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik // #if UNITY_EDITOR && UNITY_IMGUI #pragma warning disable CS0649 // Field is never assigned to, and will always have its default value. using Animancer.Units; using System; using System.Collections.Generic; using UnityEditor; using UnityEngine; namespace Animancer.Editor.Previews { /// Persistent settings for the . /// /// Documentation: /// /// Previews /// /// https://kybernetik.com.au/animancer/api/Animancer.Editor.Previews/TransitionPreviewSettings [Serializable, InternalSerializableType] public class TransitionPreviewSettings : AnimancerSettingsGroup { /************************************************************************************************************************/ /// public override string DisplayName => "Transition Previews"; /// public override int Index => 3; /************************************************************************************************************************/ private static TransitionPreviewSettings Instance => AnimancerSettingsGroup.Instance; /************************************************************************************************************************/ /// Draws the Inspector GUI for these settings. public static void DoInspectorGUI() { AnimancerSettings.SerializedObject.Update(); EditorGUI.indentLevel++; DoMiscGUI(); DoEnvironmentGUI(); DoModelsGUI(); DoHierarchyGUI(); EditorGUI.indentLevel--; AnimancerSettings.SerializedObject.ApplyModifiedProperties(); } /************************************************************************************************************************/ #region Misc /************************************************************************************************************************/ private static void DoMiscGUI() { Instance.DoPropertyField(nameof(_AutoClose)); } /************************************************************************************************************************/ [SerializeField] [Tooltip("Should this window automatically close if the target object is destroyed?")] private bool _AutoClose = true; /// Should this window automatically close if the target object is destroyed? public static bool AutoClose => Instance._AutoClose; /************************************************************************************************************************/ [SerializeField] [Tooltip("Should the scene lighting be enabled?")] private bool _SceneLighting = false; /// Should the scene lighting be enabled? public static bool SceneLighting { get => Instance._SceneLighting; set { if (SceneLighting == value) return; var property = Instance.GetSerializedProperty(nameof(_SceneLighting)); property.boolValue = value; property.serializedObject.ApplyModifiedProperties(); } } /************************************************************************************************************************/ [SerializeField] [Tooltip("Should the skybox be visible?")] private bool _ShowSkybox = false; /// Should the skybox be visible? public static bool ShowSkybox { get => Instance._ShowSkybox; set { if (ShowSkybox == value) return; var property = Instance.GetSerializedProperty(nameof(_ShowSkybox)); property.boolValue = value; property.serializedObject.ApplyModifiedProperties(); } } /************************************************************************************************************************/ [SerializeField] [Seconds(Rule = Validate.Value.IsNotNegative)] [DefaultValue(0.02f)] [Tooltip("The amount of time that will be added by a single frame step")] private float _FrameStep = 0.02f; /// The amount of time that will be added by a single frame step (in seconds). public static float FrameStep => AnimancerSettingsGroup.Instance._FrameStep; /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Environment /************************************************************************************************************************/ [SerializeField] [Tooltip("If set, the default preview scene lighting will be replaced with this prefab.")] private GameObject _SceneEnvironment; /// If set, the default preview scene lighting will be replaced with this prefab. public static GameObject SceneEnvironment => Instance._SceneEnvironment; /************************************************************************************************************************/ private static void DoEnvironmentGUI() { EditorGUI.BeginChangeCheck(); var property = Instance.DoPropertyField(nameof(_SceneEnvironment)); if (EditorGUI.EndChangeCheck()) { property.serializedObject.ApplyModifiedProperties(); TransitionPreviewWindow.InstanceScene.OnEnvironmentPrefabChanged(); } } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Models /************************************************************************************************************************/ private static void DoModelsGUI() { var property = ModelsProperty; var count = property.arraySize = EditorGUILayout.DelayedIntField(nameof(Models), property.arraySize); // Drag and Drop to add model. var area = GUILayoutUtility.GetLastRect(); HandleModelDragAndDrop(area); if (count == 0) return; property.isExpanded = EditorGUI.Foldout(area, property.isExpanded, GUIContent.none, true); if (!property.isExpanded) return; EditorGUI.indentLevel++; var model = property.GetArrayElementAtIndex(0); for (int i = 0; i < count; i++) { GUILayout.BeginHorizontal(); EditorGUILayout.ObjectField(model); if (GUILayout.Button(AnimancerIcons.ClearIcon("Remove model"), AnimancerGUI.NoPaddingButtonStyle)) { Serialization.RemoveArrayElement(property, i); property.serializedObject.ApplyModifiedProperties(); AnimancerGUI.Deselect(); GUIUtility.ExitGUI(); return; } GUILayout.Space(EditorStyles.objectField.margin.right); GUILayout.EndHorizontal(); model.Next(false); } EditorGUI.indentLevel--; } /************************************************************************************************************************/ private static DragAndDropHandler _ModelDropHandler; private static void HandleModelDragAndDrop(Rect area) { _ModelDropHandler ??= (gameObject, isDrop) => { if (!EditorUtility.IsPersistent(gameObject) || Models.Contains(gameObject) || gameObject.GetComponentInChildren() == null) return false; if (isDrop) { var modelsProperty = ModelsProperty; modelsProperty.serializedObject.Update(); var i = modelsProperty.arraySize; modelsProperty.arraySize = i + 1; modelsProperty.GetArrayElementAtIndex(i).objectReferenceValue = gameObject; modelsProperty.serializedObject.ApplyModifiedProperties(); } return true; }; _ModelDropHandler.Handle(area); } /************************************************************************************************************************/ [SerializeField] private List _Models; /// The models previously used in the . /// Accessing this property removes missing and duplicate models from the list. public static List Models { get { var instance = Instance; AnimancerEditorUtilities.RemoveMissingAndDuplicates(ref instance._Models); return instance._Models; } } private static SerializedProperty ModelsProperty => Instance.GetSerializedProperty(nameof(_Models)); /************************************************************************************************************************/ /// Adds a `model` to the list of preview models. public static void AddModel(GameObject model) { if (model == GetOrCreateDefaultHumanoid(null) || model == GetOrCreateDefaultSprite(null)) return; if (EditorUtility.IsPersistent(model)) { AddModel(Models, model); AnimancerSettings.SetDirty(); } else { AddModel(TemporarySettings.PreviewModels, model); } } private static void AddModel(List models, GameObject model) { // Remove if it was already there so that when we add it, it will be moved to the end. var index = models.LastIndexOf(model);// Search backwards because it's more likely to be near the end. if (index >= 0 && index < models.Count) models.RemoveAt(index); models.Add(model); } /************************************************************************************************************************/ private static GameObject _DefaultHumanoid; /// /// Returns the default preview object for Humanoid animations /// if it has already been loaded. /// public static GameObject GetDefaultHumanoidIfAlreadyLoaded() => _DefaultHumanoid; /// Returns the default preview object for Humanoid animations. /// A `parent` is only required if Animancer's or Unity's default objects fail to load. public static GameObject GetOrCreateDefaultHumanoid(Transform parent) { if (_DefaultHumanoid != null) return _DefaultHumanoid; // Try to load Animancer Humanoid. var path = AssetDatabase.GUIDToAssetPath("f976ca0fb1329b44a8bc3dcca706751a"); if (!string.IsNullOrEmpty(path)) { _DefaultHumanoid = AssetDatabase.LoadAssetAtPath(path); if (_DefaultHumanoid != null) return _DefaultHumanoid; } // Otherwise try to load Unity's DefaultAvatar. _DefaultHumanoid = EditorGUIUtility.Load("Avatar/DefaultAvatar.fbx") as GameObject; if (_DefaultHumanoid != null) return _DefaultHumanoid; if (parent == null) return null; // Otherwise just create an empty object. _DefaultHumanoid = EditorUtility.CreateGameObjectWithHideFlags( "DummyAvatar", HideFlags.HideAndDontSave, typeof(Animator)); _DefaultHumanoid.transform.parent = parent; return _DefaultHumanoid; } /************************************************************************************************************************/ private static GameObject _DefaultSprite; /// /// Returns the default preview object for animations /// if it has already been created. /// public static GameObject GetDefaultSpriteIfAlreadyCreated() => _DefaultSprite; /// Returns the default preview object for animations. /// A `parent` is required to create the object. public static GameObject GetOrCreateDefaultSprite(Transform parent) { if (_DefaultSprite == null && parent != null) { _DefaultSprite = EditorUtility.CreateGameObjectWithHideFlags( "DefaultSprite", HideFlags.HideAndDontSave, typeof(Animator), typeof(SpriteRenderer)); _DefaultSprite.transform.parent = parent; } return _DefaultSprite; } /************************************************************************************************************************/ /// /// Tries to choose the most appropriate model to use /// based on the properties animated by the `animationClipSource`. /// public static Transform TrySelectBestModel( object animationClipSource, Transform parent) { if (animationClipSource.IsNullOrDestroyed()) return null; using (SetPool.Instance.Acquire(out var clips)) { clips.GatherFromSource(animationClipSource); if (clips.Count == 0) return null; var model = TrySelectBestModel(clips, TemporarySettings.PreviewModels); if (model != null) return model; model = TrySelectBestModel(clips, Models); if (model != null) return model; foreach (var clip in clips) { var type = AnimationBindings.GetAnimationType(clip); switch (type) { case AnimationType.Humanoid: return GetOrCreateDefaultHumanoid(parent).transform; case AnimationType.Sprite: return GetOrCreateDefaultSprite(parent).transform; } } return null; } } /************************************************************************************************************************/ private static Transform TrySelectBestModel(HashSet clips, List models) { var animatableBindings = new HashSet[models.Count]; for (int i = 0; i < models.Count; i++) { animatableBindings[i] = AnimationBindings.GetBindings(models[i]).ObjectBindings; } var bestMatchIndex = -1; var bestMatchCount = 0; foreach (var clip in clips) { var clipBindings = AnimationBindings.GetBindings(clip); for (int iModel = animatableBindings.Length - 1; iModel >= 0; iModel--) { var modelBindings = animatableBindings[iModel]; var matches = 0; for (int iBinding = 0; iBinding < clipBindings.Length; iBinding++) { if (modelBindings.Contains(clipBindings[iBinding])) matches++; } if (bestMatchCount < matches && matches > clipBindings.Length / 2) { bestMatchCount = matches; bestMatchIndex = iModel; // If it matches all bindings, use it. if (bestMatchCount == clipBindings.Length) goto FoundBestMatch; } } } FoundBestMatch: if (bestMatchIndex >= 0) return models[bestMatchIndex].transform; else return null; } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Scene Hierarchy /************************************************************************************************************************/ private static void DoHierarchyGUI() { GUILayout.BeginVertical(GUI.skin.box); GUILayout.Label("Preview Scene Hierarchy"); DoHierarchyGUI(TransitionPreviewWindow.InstanceScene.PreviewSceneRoot); GUILayout.EndVertical(); } /************************************************************************************************************************/ private static GUIStyle _HierarchyButtonStyle; private static void DoHierarchyGUI(Transform root) { var area = AnimancerGUI.LayoutSingleLineRect(); _HierarchyButtonStyle ??= new(EditorStyles.miniButton) { alignment = TextAnchor.MiddleLeft, }; if (GUI.Button(EditorGUI.IndentedRect(area), root.name, _HierarchyButtonStyle)) { Selection.activeTransform = root; GUIUtility.ExitGUI(); } var childCount = root.childCount; if (childCount == 0) return; var expandedHierarchy = TransitionPreviewWindow.InstanceScene.ExpandedHierarchy; var index = expandedHierarchy != null ? expandedHierarchy.IndexOf(root) : -1; var isExpanded = EditorGUI.Foldout(area, index >= 0, GUIContent.none); if (isExpanded) { if (index < 0) expandedHierarchy.Add(root); EditorGUI.indentLevel++; for (int i = 0; i < childCount; i++) DoHierarchyGUI(root.GetChild(i)); EditorGUI.indentLevel--; } else if (index >= 0) { expandedHierarchy.RemoveAt(index); } } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ } } #endif