diff --git a/Assets/_Project/Scripts/AnimationEvents/AnimationEvent.cs b/Assets/_Project/Scripts/AnimationEvents/AnimationEvent.cs deleted file mode 100644 index cdebffe..0000000 --- a/Assets/_Project/Scripts/AnimationEvents/AnimationEvent.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System; -using UnityEngine.Events; - -[Serializable] -public class AnimationEvent { - public string eventName; - public UnityEvent OnAnimationEvent; -} \ No newline at end of file diff --git a/Assets/_Project/Scripts/AnimationEvents/AnimationEventReceiver.cs b/Assets/_Project/Scripts/AnimationEvents/AnimationEventReceiver.cs deleted file mode 100644 index 827325f..0000000 --- a/Assets/_Project/Scripts/AnimationEvents/AnimationEventReceiver.cs +++ /dev/null @@ -1,11 +0,0 @@ -using UnityEngine; -using System.Collections.Generic; - -public class AnimationEventReceiver : MonoBehaviour { - [SerializeField] List animationEvents = new(); - - public void OnAnimationEventTriggered(string eventName) { - AnimationEvent matchingEvent = animationEvents.Find(se => se.eventName == eventName); - matchingEvent?.OnAnimationEvent?.Invoke(); - } -} \ No newline at end of file diff --git a/Assets/_Project/Scripts/AnimationEvents/AnimationEventStateBehaviour.cs b/Assets/_Project/Scripts/AnimationEvents/AnimationEventStateBehaviour.cs deleted file mode 100644 index f3e4951..0000000 --- a/Assets/_Project/Scripts/AnimationEvents/AnimationEventStateBehaviour.cs +++ /dev/null @@ -1,30 +0,0 @@ -using UnityEngine; -using UnityEngine.Events; - -public class AnimationEventStateBehaviour : StateMachineBehaviour { - public string eventName; - [Range(0f, 1f)] public float triggerTime; - - bool hasTriggered; - AnimationEventReceiver receiver; - - public override void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { - hasTriggered = false; - receiver = animator.GetComponent(); - } - - public override void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { - float currentTime = stateInfo.normalizedTime % 1f; - - if (!hasTriggered && currentTime >= triggerTime) { - NotifyReceiver(animator); - hasTriggered = true; - } - } - - void NotifyReceiver(Animator animator) { - if (receiver != null) { - receiver.OnAnimationEventTriggered(eventName); - } - } -} diff --git a/Assets/_Project/Scripts/AnimationEvents/Editor/AnimationEventDrawer.cs b/Assets/_Project/Scripts/AnimationEvents/Editor/AnimationEventDrawer.cs deleted file mode 100644 index 671575f..0000000 --- a/Assets/_Project/Scripts/AnimationEvents/Editor/AnimationEventDrawer.cs +++ /dev/null @@ -1,28 +0,0 @@ -#if UNITY_EDITOR -using UnityEditor; -using UnityEngine; - -[CustomPropertyDrawer(typeof(AnimationEvent))] -public class AnimationEventDrawer : PropertyDrawer { - public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { - EditorGUI.BeginProperty(position, label, property); - - SerializedProperty stateNameProperty = property.FindPropertyRelative("eventName"); - SerializedProperty stateEventProperty = property.FindPropertyRelative("OnAnimationEvent"); - - Rect stateNameRect = new(position.x, position.y, position.width, EditorGUIUtility.singleLineHeight); - Rect stateEventRect = new(position.x, position.y + EditorGUIUtility.singleLineHeight + 2, position.width, - EditorGUI.GetPropertyHeight(stateEventProperty)); - - EditorGUI.PropertyField(stateNameRect, stateNameProperty); - EditorGUI.PropertyField(stateEventRect, stateEventProperty, true); - - EditorGUI.EndProperty(); - } - - public override float GetPropertyHeight(SerializedProperty property, GUIContent label) { - SerializedProperty stateEventProperty = property.FindPropertyRelative("OnAnimationEvent"); - return EditorGUIUtility.singleLineHeight + EditorGUI.GetPropertyHeight(stateEventProperty) + 4; - } -} -#endif diff --git a/Assets/_Project/Scripts/AnimationEvents/Editor/AnimationEventStateBehaviourEditor.cs b/Assets/_Project/Scripts/AnimationEvents/Editor/AnimationEventStateBehaviourEditor.cs deleted file mode 100644 index 286788b..0000000 --- a/Assets/_Project/Scripts/AnimationEvents/Editor/AnimationEventStateBehaviourEditor.cs +++ /dev/null @@ -1,281 +0,0 @@ -using System; -using System.Linq; -using System.Reflection; -using UnityEngine; -using UnityEngine.Animations; -using UnityEngine.Playables; - -#if UNITY_EDITOR -using UnityEditor; -using UnityEditor.Animations; -/// -/// Custom editor for the AnimationEventStateBehaviour class, providing a GUI for previewing animation states -/// and handling animation events within the Unity editor. Enables users to preview animations and manage -/// animation events directly in the editor. -/// -[UnityEditor.CustomEditor(typeof(AnimationEventStateBehaviour))] -public class AnimationEventStateBehaviourEditor : Editor { - Motion previewClip; - float previewTime; - bool isPreviewing; - - PlayableGraph playableGraph; - AnimationMixerPlayable mixer; - - public override void OnInspectorGUI() { - DrawDefaultInspector(); - - AnimationEventStateBehaviour stateBehaviour = (AnimationEventStateBehaviour) target; - - if (Validate(stateBehaviour, out string errorMessage)) { - GUILayout.Space(10); - - if (isPreviewing) { - if (GUILayout.Button("Stop Preview")) { - EnforceTPose(); - isPreviewing = false; - AnimationMode.StopAnimationMode(); - playableGraph.Destroy(); - } else { - PreviewAnimationClip(stateBehaviour); - } - } else if (GUILayout.Button("Preview")) { - isPreviewing = true; - AnimationMode.StartAnimationMode(); - } - - GUILayout.Label($"Previewing at {previewTime:F2}s", EditorStyles.helpBox); - } else { - EditorGUILayout.HelpBox(errorMessage, MessageType.Info); - } - } - - void PreviewAnimationClip(AnimationEventStateBehaviour stateBehaviour) { - AnimatorController animatorController = GetValidAnimatorController(out string errorMessage); - if (animatorController == null) return; - - ChildAnimatorState matchingState = animatorController.layers - .Select(layer => FindMatchingState(layer.stateMachine, stateBehaviour)) - .FirstOrDefault(state => state.state != null); - - if (matchingState.state == null) return; - - Motion motion = matchingState.state.motion; - - // Handle BlendTree logic - if (motion is BlendTree blendTree) { - SampleBlendTreeAnimation(stateBehaviour, stateBehaviour.triggerTime); - return; - } - - // If it's a simple AnimationClip, sample it directly - if (motion is AnimationClip clip) { - previewTime = stateBehaviour.triggerTime * clip.length; - AnimationMode.SampleAnimationClip(Selection.activeGameObject, clip, previewTime); - } - } - - void SampleBlendTreeAnimation(AnimationEventStateBehaviour stateBehaviour, float normalizedTime) { - Animator animator = Selection.activeGameObject.GetComponent(); - - if (playableGraph.IsValid()) { - playableGraph.Destroy(); - } - - playableGraph = PlayableGraph.Create("BlendTreePreviewGraph"); - mixer = AnimationMixerPlayable.Create(playableGraph, 1, true); - - var output = AnimationPlayableOutput.Create(playableGraph, "Animation", animator); - output.SetSourcePlayable(mixer); - - AnimatorController animatorController = GetValidAnimatorController(out string errorMessage); - if (animatorController == null) return; - - ChildAnimatorState matchingState = animatorController.layers - .Select(layer => FindMatchingState(layer.stateMachine, stateBehaviour)) - .FirstOrDefault(state => state.state != null); - - // If the matching state is not a BlendTree, bail out - if (matchingState.state.motion is not BlendTree blendTree) return; - - // Determine the maximum threshold value in the blend tree - float maxThreshold = blendTree.children.Max(child => child.threshold); - - AnimationClipPlayable[] clipPlayables = new AnimationClipPlayable[blendTree.children.Length]; - float[] weights = new float[blendTree.children.Length]; - float totalWeight = 0f; - - // Scale target weight according to max threshold - float targetWeight = Mathf.Clamp(normalizedTime * maxThreshold, blendTree.minThreshold, maxThreshold); - - for (int i = 0; i < blendTree.children.Length; i++) { - ChildMotion child = blendTree.children[i]; - float weight = CalculateWeightForChild(blendTree, child, targetWeight); - weights[i] = weight; - totalWeight += weight; - - AnimationClip clip = GetAnimationClipFromMotion(child.motion); - clipPlayables[i] = AnimationClipPlayable.Create(playableGraph, clip); - } - - // Normalize weights so they sum to 1 - for (int i = 0; i < weights.Length; i++) { - weights[i] /= totalWeight; - } - - mixer.SetInputCount(clipPlayables.Length); - for (int i = 0; i < clipPlayables.Length; i++) { - mixer.ConnectInput(i, clipPlayables[i], 0); - mixer.SetInputWeight(i, weights[i]); - } - - AnimationMode.SamplePlayableGraph(playableGraph, 0, normalizedTime); - } - - - float CalculateWeightForChild(BlendTree blendTree, ChildMotion child, float targetWeight) { - float weight = 0f; - - if (blendTree.blendType == BlendTreeType.Simple1D) { - // Find the neighbors around the target weight - ChildMotion? lowerNeighbor = null; - ChildMotion? upperNeighbor = null; - - foreach (var motion in blendTree.children) { - if (motion.threshold <= targetWeight && (lowerNeighbor == null || motion.threshold > lowerNeighbor.Value.threshold)) { - lowerNeighbor = motion; - } - - if (motion.threshold >= targetWeight && (upperNeighbor == null || motion.threshold < upperNeighbor.Value.threshold)) { - upperNeighbor = motion; - } - } - - if (lowerNeighbor.HasValue && upperNeighbor.HasValue) { - if (Mathf.Approximately(child.threshold, lowerNeighbor.Value.threshold)) { - weight = 1.0f - Mathf.InverseLerp(lowerNeighbor.Value.threshold, upperNeighbor.Value.threshold, targetWeight); - } else if (Mathf.Approximately(child.threshold, upperNeighbor.Value.threshold)) { - weight = Mathf.InverseLerp(lowerNeighbor.Value.threshold, upperNeighbor.Value.threshold, targetWeight); - } - } else { - // Handle edge cases where there is no valid interpolation range - weight = Mathf.Approximately(targetWeight, child.threshold) ? 1f : 0f; - } - } else if (blendTree.blendType == BlendTreeType.FreeformCartesian2D || blendTree.blendType == BlendTreeType.FreeformDirectional2D) { - Vector2 targetPos = new( - GetBlendParameterValue(blendTree, blendTree.blendParameter), - GetBlendParameterValue(blendTree, blendTree.blendParameterY) - ); - float distance = Vector2.Distance(targetPos, child.position); - weight = Mathf.Clamp01(1.0f / (distance + 0.001f)); - } - - return weight; - } - - - float GetBlendParameterValue(BlendTree blendTree, string parameterName) { - var methodInfo = typeof(BlendTree).GetMethod("GetInputBlendValue", BindingFlags.NonPublic | BindingFlags.Instance); - if (methodInfo == null) { - Debug.LogError("Failed to find GetInputBlendValue method via reflection."); - return 0f; - } - - return (float) methodInfo.Invoke(blendTree, new object[] { parameterName }); - } - - ChildAnimatorState FindMatchingState(AnimatorStateMachine stateMachine, AnimationEventStateBehaviour stateBehaviour) { - foreach (var state in stateMachine.states) { - if (state.state.behaviours.Contains(stateBehaviour)) { - return state; - } - } - - foreach (var subStateMachine in stateMachine.stateMachines) { - var matchingState = FindMatchingState(subStateMachine.stateMachine, stateBehaviour); - if (matchingState.state != null) { - return matchingState; - } - } - - return default; - } - - bool Validate(AnimationEventStateBehaviour stateBehaviour, out string errorMessage) { - AnimatorController animatorController = GetValidAnimatorController(out errorMessage); - if (animatorController == null) return false; - - ChildAnimatorState matchingState = animatorController.layers - .Select(layer => FindMatchingState(layer.stateMachine, stateBehaviour)) - .FirstOrDefault(state => state.state != null); - - previewClip = GetAnimationClipFromMotion(matchingState.state?.motion); - if (previewClip == null) { - errorMessage = "No valid AnimationClip found for the current state."; - return false; - } - - return true; - } - - AnimationClip GetAnimationClipFromMotion(Motion motion) { - if (motion is AnimationClip clip) { - return clip; - } - - if (motion is BlendTree blendTree) { - return blendTree.children - .Select(child => GetAnimationClipFromMotion(child.motion)) - .FirstOrDefault(childClip => childClip != null); - } - - return null; - } - - AnimatorController GetValidAnimatorController(out string errorMessage) { - errorMessage = string.Empty; - - GameObject targetGameObject = Selection.activeGameObject; - if (targetGameObject == null) { - errorMessage = "Please select a GameObject with an Animator to preview."; - return null; - } - - Animator animator = targetGameObject.GetComponent(); - if (animator == null) { - errorMessage = "The selected GameObject does not have an Animator component."; - return null; - } - - AnimatorController animatorController = animator.runtimeAnimatorController as AnimatorController; - if (animatorController == null) { - errorMessage = "The selected Animator does not have a valid AnimatorController."; - return null; - } - - return animatorController; - } - - [MenuItem("GameObject/Enforce T-Pose", false, 0)] - static void EnforceTPose() { - GameObject selected = Selection.activeGameObject; - if (!selected || !selected.TryGetComponent(out Animator animator) || !animator.avatar) return; - - SkeletonBone[] skeletonBones = animator.avatar.humanDescription.skeleton; - - foreach (HumanBodyBones hbb in Enum.GetValues(typeof(HumanBodyBones))) { - if (hbb == HumanBodyBones.LastBone) continue; - - Transform boneTransform = animator.GetBoneTransform(hbb); - if (!boneTransform) continue; - - SkeletonBone skeletonBone = skeletonBones.FirstOrDefault(sb => sb.name == boneTransform.name); - if (skeletonBone.name == null) continue; - - if (hbb == HumanBodyBones.Hips) boneTransform.localPosition = skeletonBone.position; - boneTransform.localRotation = skeletonBone.rotation; - } - } -} - -#endif diff --git a/LICENSE b/LICENSE.md similarity index 100% rename from LICENSE rename to LICENSE.md diff --git a/LICENSE.md.meta b/LICENSE.md.meta new file mode 100644 index 0000000..91fdb1f --- /dev/null +++ b/LICENSE.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 5865749fb2f477441a10b65d50d016e2 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/manifest.json b/Packages/manifest.json deleted file mode 100644 index 197e54b..0000000 --- a/Packages/manifest.json +++ /dev/null @@ -1,67 +0,0 @@ -{ - "dependencies": { - "com.gitamend.improvedtimers": "git+https://github.com/adammyhre/Unity-Improved-Timers.git", - "com.gitamend.unityutils": "git+https://github.com/adammyhre/Unity-Utils.git", - "com.unity.ai.navigation": "2.0.4", - "com.unity.burst": "1.8.18", - "com.unity.cinemachine": "2.10.1", - "com.unity.collab-proxy": "2.5.2", - "com.unity.collections": "2.5.1", - "com.unity.dt.app-ui": "1.1.0", - "com.unity.editorcoroutines": "1.0.0", - "com.unity.ext.nunit": "2.0.5", - "com.unity.ide.rider": "3.0.31", - "com.unity.ide.visualstudio": "2.0.22", - "com.unity.inputsystem": "1.11.1", - "com.unity.mathematics": "1.3.2", - "com.unity.multiplayer.center": "1.0.0", - "com.unity.nuget.mono-cecil": "1.11.4", - "com.unity.postprocessing": "3.4.0", - "com.unity.recorder": "5.1.1", - "com.unity.render-pipelines.core": "17.0.3", - "com.unity.render-pipelines.universal": "17.0.3", - "com.unity.render-pipelines.universal-config": "17.0.3", - "com.unity.rendering.light-transport": "1.0.1", - "com.unity.searcher": "4.9.2", - "com.unity.shadergraph": "17.0.3", - "com.unity.test-framework": "1.4.5", - "com.unity.test-framework.performance": "3.0.3", - "com.unity.timeline": "1.8.7", - "com.unity.ugui": "2.0.0", - "com.unity.visualscripting": "1.9.4", - "com.unity.modules.accessibility": "1.0.0", - "com.unity.modules.ai": "1.0.0", - "com.unity.modules.androidjni": "1.0.0", - "com.unity.modules.animation": "1.0.0", - "com.unity.modules.assetbundle": "1.0.0", - "com.unity.modules.audio": "1.0.0", - "com.unity.modules.cloth": "1.0.0", - "com.unity.modules.director": "1.0.0", - "com.unity.modules.hierarchycore": "1.0.0", - "com.unity.modules.imageconversion": "1.0.0", - "com.unity.modules.imgui": "1.0.0", - "com.unity.modules.jsonserialize": "1.0.0", - "com.unity.modules.particlesystem": "1.0.0", - "com.unity.modules.physics": "1.0.0", - "com.unity.modules.physics2d": "1.0.0", - "com.unity.modules.screencapture": "1.0.0", - "com.unity.modules.subsystems": "1.0.0", - "com.unity.modules.terrain": "1.0.0", - "com.unity.modules.terrainphysics": "1.0.0", - "com.unity.modules.tilemap": "1.0.0", - "com.unity.modules.ui": "1.0.0", - "com.unity.modules.uielements": "1.0.0", - "com.unity.modules.umbra": "1.0.0", - "com.unity.modules.unityanalytics": "1.0.0", - "com.unity.modules.unitywebrequest": "1.0.0", - "com.unity.modules.unitywebrequestassetbundle": "1.0.0", - "com.unity.modules.unitywebrequestaudio": "1.0.0", - "com.unity.modules.unitywebrequesttexture": "1.0.0", - "com.unity.modules.unitywebrequestwww": "1.0.0", - "com.unity.modules.vehicles": "1.0.0", - "com.unity.modules.video": "1.0.0", - "com.unity.modules.vr": "1.0.0", - "com.unity.modules.wind": "1.0.0", - "com.unity.modules.xr": "1.0.0" - } -} diff --git a/README.md.meta b/README.md.meta new file mode 100644 index 0000000..fe7dbb4 --- /dev/null +++ b/README.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 256daaa39158b6b49ad920bb089e9b04 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime.meta b/Runtime.meta new file mode 100644 index 0000000..91e93c7 --- /dev/null +++ b/Runtime.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: edcbdc38ddc746242a69b286a3ddcf22 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/AnimationEvent.cs b/Runtime/AnimationEvent.cs new file mode 100644 index 0000000..68418fa --- /dev/null +++ b/Runtime/AnimationEvent.cs @@ -0,0 +1,10 @@ +using System; +using UnityEngine.Events; + +namespace ImprovedUnityAnimationEvents { + [Serializable] + public class AnimationEvent { + public string eventName; + public UnityEvent OnAnimationEvent; + } +} diff --git a/Assets/_Project/Scripts/AnimationEvents/AnimationEvent.cs.meta b/Runtime/AnimationEvent.cs.meta similarity index 100% rename from Assets/_Project/Scripts/AnimationEvents/AnimationEvent.cs.meta rename to Runtime/AnimationEvent.cs.meta diff --git a/Runtime/AnimationEventReceiver.cs b/Runtime/AnimationEventReceiver.cs new file mode 100644 index 0000000..05abe02 --- /dev/null +++ b/Runtime/AnimationEventReceiver.cs @@ -0,0 +1,13 @@ +using UnityEngine; +using System.Collections.Generic; + +namespace ImprovedUnityAnimationEvents { + public class AnimationEventReceiver : MonoBehaviour { + [SerializeField] List animationEvents = new(); + + public void OnAnimationEventTriggered(string eventName) { + AnimationEvent matchingEvent = animationEvents.Find(se => se.eventName == eventName); + matchingEvent?.OnAnimationEvent?.Invoke(); + } + } +} diff --git a/Assets/_Project/Scripts/AnimationEvents/AnimationEventReceiver.cs.meta b/Runtime/AnimationEventReceiver.cs.meta similarity index 100% rename from Assets/_Project/Scripts/AnimationEvents/AnimationEventReceiver.cs.meta rename to Runtime/AnimationEventReceiver.cs.meta diff --git a/Runtime/AnimationEventStateBehaviour.cs b/Runtime/AnimationEventStateBehaviour.cs new file mode 100644 index 0000000..4e50ea1 --- /dev/null +++ b/Runtime/AnimationEventStateBehaviour.cs @@ -0,0 +1,32 @@ +using UnityEngine; +using UnityEngine.Events; + +namespace ImprovedUnityAnimationEvents { + public class AnimationEventStateBehaviour : StateMachineBehaviour { + public string eventName; + [Range(0f, 1f)] public float triggerTime; + + bool hasTriggered; + AnimationEventReceiver receiver; + + public override void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { + hasTriggered = false; + receiver = animator.GetComponent(); + } + + public override void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { + float currentTime = stateInfo.normalizedTime % 1f; + + if (!hasTriggered && currentTime >= triggerTime) { + NotifyReceiver(animator); + hasTriggered = true; + } + } + + void NotifyReceiver(Animator animator) { + if (receiver != null) { + receiver.OnAnimationEventTriggered(eventName); + } + } + } +} diff --git a/Assets/_Project/Scripts/AnimationEvents/AnimationEventStateBehaviour.cs.meta b/Runtime/AnimationEventStateBehaviour.cs.meta similarity index 100% rename from Assets/_Project/Scripts/AnimationEvents/AnimationEventStateBehaviour.cs.meta rename to Runtime/AnimationEventStateBehaviour.cs.meta diff --git a/Assets/_Project/Scripts/AnimationEvents/Editor.meta b/Runtime/Editor.meta similarity index 77% rename from Assets/_Project/Scripts/AnimationEvents/Editor.meta rename to Runtime/Editor.meta index 8e79b30..718c153 100644 --- a/Assets/_Project/Scripts/AnimationEvents/Editor.meta +++ b/Runtime/Editor.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: a76d967b7753ac84487d62daad0326ce +guid: 0a9476d8535844b45ae4d726bcdef8f9 folderAsset: yes DefaultImporter: externalObjects: {} diff --git a/Runtime/Editor/AnimationEventDrawer.cs b/Runtime/Editor/AnimationEventDrawer.cs new file mode 100644 index 0000000..617b0c8 --- /dev/null +++ b/Runtime/Editor/AnimationEventDrawer.cs @@ -0,0 +1,30 @@ +#if UNITY_EDITOR +using UnityEditor; +using UnityEngine; + +namespace ImprovedUnityAnimationEvents { + [CustomPropertyDrawer(typeof(AnimationEvent))] + public class AnimationEventDrawer : PropertyDrawer { + public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { + EditorGUI.BeginProperty(position, label, property); + + SerializedProperty stateNameProperty = property.FindPropertyRelative("eventName"); + SerializedProperty stateEventProperty = property.FindPropertyRelative("OnAnimationEvent"); + + Rect stateNameRect = new(position.x, position.y, position.width, EditorGUIUtility.singleLineHeight); + Rect stateEventRect = new(position.x, position.y + EditorGUIUtility.singleLineHeight + 2, position.width, + EditorGUI.GetPropertyHeight(stateEventProperty)); + + EditorGUI.PropertyField(stateNameRect, stateNameProperty); + EditorGUI.PropertyField(stateEventRect, stateEventProperty, true); + + EditorGUI.EndProperty(); + } + + public override float GetPropertyHeight(SerializedProperty property, GUIContent label) { + SerializedProperty stateEventProperty = property.FindPropertyRelative("OnAnimationEvent"); + return EditorGUIUtility.singleLineHeight + EditorGUI.GetPropertyHeight(stateEventProperty) + 4; + } + } +} +#endif diff --git a/Assets/_Project/Scripts/AnimationEvents/Editor/AnimationEventDrawer.cs.meta b/Runtime/Editor/AnimationEventDrawer.cs.meta similarity index 100% rename from Assets/_Project/Scripts/AnimationEvents/Editor/AnimationEventDrawer.cs.meta rename to Runtime/Editor/AnimationEventDrawer.cs.meta diff --git a/Runtime/Editor/AnimationEventStateBehaviourEditor.cs b/Runtime/Editor/AnimationEventStateBehaviourEditor.cs new file mode 100644 index 0000000..91b6be5 --- /dev/null +++ b/Runtime/Editor/AnimationEventStateBehaviourEditor.cs @@ -0,0 +1,283 @@ +using System; +using System.Linq; +using System.Reflection; +using UnityEngine; +using UnityEngine.Animations; +using UnityEngine.Playables; + +#if UNITY_EDITOR +using UnityEditor; +using UnityEditor.Animations; +/// +/// Custom editor for the AnimationEventStateBehaviour class, providing a GUI for previewing animation states +/// and handling animation events within the Unity editor. Enables users to preview animations and manage +/// animation events directly in the editor. +/// +namespace ImprovedUnityAnimationEvents { + [UnityEditor.CustomEditor(typeof(AnimationEventStateBehaviour))] + public class AnimationEventStateBehaviourEditor : Editor { + Motion previewClip; + float previewTime; + bool isPreviewing; + + PlayableGraph playableGraph; + AnimationMixerPlayable mixer; + + public override void OnInspectorGUI() { + DrawDefaultInspector(); + + AnimationEventStateBehaviour stateBehaviour = (AnimationEventStateBehaviour) target; + + if (Validate(stateBehaviour, out string errorMessage)) { + GUILayout.Space(10); + + if (isPreviewing) { + if (GUILayout.Button("Stop Preview")) { + EnforceTPose(); + isPreviewing = false; + AnimationMode.StopAnimationMode(); + playableGraph.Destroy(); + } else { + PreviewAnimationClip(stateBehaviour); + } + } else if (GUILayout.Button("Preview")) { + isPreviewing = true; + AnimationMode.StartAnimationMode(); + } + + GUILayout.Label($"Previewing at {previewTime:F2}s", EditorStyles.helpBox); + } else { + EditorGUILayout.HelpBox(errorMessage, MessageType.Info); + } + } + + void PreviewAnimationClip(AnimationEventStateBehaviour stateBehaviour) { + AnimatorController animatorController = GetValidAnimatorController(out string errorMessage); + if (animatorController == null) return; + + ChildAnimatorState matchingState = animatorController.layers + .Select(layer => FindMatchingState(layer.stateMachine, stateBehaviour)) + .FirstOrDefault(state => state.state != null); + + if (matchingState.state == null) return; + + Motion motion = matchingState.state.motion; + + // Handle BlendTree logic + if (motion is BlendTree blendTree) { + SampleBlendTreeAnimation(stateBehaviour, stateBehaviour.triggerTime); + return; + } + + // If it's a simple AnimationClip, sample it directly + if (motion is AnimationClip clip) { + previewTime = stateBehaviour.triggerTime * clip.length; + AnimationMode.SampleAnimationClip(Selection.activeGameObject, clip, previewTime); + } + } + + void SampleBlendTreeAnimation(AnimationEventStateBehaviour stateBehaviour, float normalizedTime) { + Animator animator = Selection.activeGameObject.GetComponent(); + + if (playableGraph.IsValid()) { + playableGraph.Destroy(); + } + + playableGraph = PlayableGraph.Create("BlendTreePreviewGraph"); + mixer = AnimationMixerPlayable.Create(playableGraph, 1, true); + + var output = AnimationPlayableOutput.Create(playableGraph, "Animation", animator); + output.SetSourcePlayable(mixer); + + AnimatorController animatorController = GetValidAnimatorController(out string errorMessage); + if (animatorController == null) return; + + ChildAnimatorState matchingState = animatorController.layers + .Select(layer => FindMatchingState(layer.stateMachine, stateBehaviour)) + .FirstOrDefault(state => state.state != null); + + // If the matching state is not a BlendTree, bail out + if (matchingState.state.motion is not BlendTree blendTree) return; + + // Determine the maximum threshold value in the blend tree + float maxThreshold = blendTree.children.Max(child => child.threshold); + + AnimationClipPlayable[] clipPlayables = new AnimationClipPlayable[blendTree.children.Length]; + float[] weights = new float[blendTree.children.Length]; + float totalWeight = 0f; + + // Scale target weight according to max threshold + float targetWeight = Mathf.Clamp(normalizedTime * maxThreshold, blendTree.minThreshold, maxThreshold); + + for (int i = 0; i < blendTree.children.Length; i++) { + ChildMotion child = blendTree.children[i]; + float weight = CalculateWeightForChild(blendTree, child, targetWeight); + weights[i] = weight; + totalWeight += weight; + + AnimationClip clip = GetAnimationClipFromMotion(child.motion); + clipPlayables[i] = AnimationClipPlayable.Create(playableGraph, clip); + } + + // Normalize weights so they sum to 1 + for (int i = 0; i < weights.Length; i++) { + weights[i] /= totalWeight; + } + + mixer.SetInputCount(clipPlayables.Length); + for (int i = 0; i < clipPlayables.Length; i++) { + mixer.ConnectInput(i, clipPlayables[i], 0); + mixer.SetInputWeight(i, weights[i]); + } + + AnimationMode.SamplePlayableGraph(playableGraph, 0, normalizedTime); + } + + + float CalculateWeightForChild(BlendTree blendTree, ChildMotion child, float targetWeight) { + float weight = 0f; + + if (blendTree.blendType == BlendTreeType.Simple1D) { + // Find the neighbors around the target weight + ChildMotion? lowerNeighbor = null; + ChildMotion? upperNeighbor = null; + + foreach (var motion in blendTree.children) { + if (motion.threshold <= targetWeight && (lowerNeighbor == null || motion.threshold > lowerNeighbor.Value.threshold)) { + lowerNeighbor = motion; + } + + if (motion.threshold >= targetWeight && (upperNeighbor == null || motion.threshold < upperNeighbor.Value.threshold)) { + upperNeighbor = motion; + } + } + + if (lowerNeighbor.HasValue && upperNeighbor.HasValue) { + if (Mathf.Approximately(child.threshold, lowerNeighbor.Value.threshold)) { + weight = 1.0f - Mathf.InverseLerp(lowerNeighbor.Value.threshold, upperNeighbor.Value.threshold, targetWeight); + } else if (Mathf.Approximately(child.threshold, upperNeighbor.Value.threshold)) { + weight = Mathf.InverseLerp(lowerNeighbor.Value.threshold, upperNeighbor.Value.threshold, targetWeight); + } + } else { + // Handle edge cases where there is no valid interpolation range + weight = Mathf.Approximately(targetWeight, child.threshold) ? 1f : 0f; + } + } else if (blendTree.blendType == BlendTreeType.FreeformCartesian2D || blendTree.blendType == BlendTreeType.FreeformDirectional2D) { + Vector2 targetPos = new( + GetBlendParameterValue(blendTree, blendTree.blendParameter), + GetBlendParameterValue(blendTree, blendTree.blendParameterY) + ); + float distance = Vector2.Distance(targetPos, child.position); + weight = Mathf.Clamp01(1.0f / (distance + 0.001f)); + } + + return weight; + } + + + float GetBlendParameterValue(BlendTree blendTree, string parameterName) { + var methodInfo = typeof(BlendTree).GetMethod("GetInputBlendValue", BindingFlags.NonPublic | BindingFlags.Instance); + if (methodInfo == null) { + Debug.LogError("Failed to find GetInputBlendValue method via reflection."); + return 0f; + } + + return (float) methodInfo.Invoke(blendTree, new object[] { parameterName }); + } + + ChildAnimatorState FindMatchingState(AnimatorStateMachine stateMachine, AnimationEventStateBehaviour stateBehaviour) { + foreach (var state in stateMachine.states) { + if (state.state.behaviours.Contains(stateBehaviour)) { + return state; + } + } + + foreach (var subStateMachine in stateMachine.stateMachines) { + var matchingState = FindMatchingState(subStateMachine.stateMachine, stateBehaviour); + if (matchingState.state != null) { + return matchingState; + } + } + + return default; + } + + bool Validate(AnimationEventStateBehaviour stateBehaviour, out string errorMessage) { + AnimatorController animatorController = GetValidAnimatorController(out errorMessage); + if (animatorController == null) return false; + + ChildAnimatorState matchingState = animatorController.layers + .Select(layer => FindMatchingState(layer.stateMachine, stateBehaviour)) + .FirstOrDefault(state => state.state != null); + + previewClip = GetAnimationClipFromMotion(matchingState.state?.motion); + if (previewClip == null) { + errorMessage = "No valid AnimationClip found for the current state."; + return false; + } + + return true; + } + + AnimationClip GetAnimationClipFromMotion(Motion motion) { + if (motion is AnimationClip clip) { + return clip; + } + + if (motion is BlendTree blendTree) { + return blendTree.children + .Select(child => GetAnimationClipFromMotion(child.motion)) + .FirstOrDefault(childClip => childClip != null); + } + + return null; + } + + AnimatorController GetValidAnimatorController(out string errorMessage) { + errorMessage = string.Empty; + + GameObject targetGameObject = Selection.activeGameObject; + if (targetGameObject == null) { + errorMessage = "Please select a GameObject with an Animator to preview."; + return null; + } + + Animator animator = targetGameObject.GetComponent(); + if (animator == null) { + errorMessage = "The selected GameObject does not have an Animator component."; + return null; + } + + AnimatorController animatorController = animator.runtimeAnimatorController as AnimatorController; + if (animatorController == null) { + errorMessage = "The selected Animator does not have a valid AnimatorController."; + return null; + } + + return animatorController; + } + + [MenuItem("GameObject/Enforce T-Pose", false, 0)] + static void EnforceTPose() { + GameObject selected = Selection.activeGameObject; + if (!selected || !selected.TryGetComponent(out Animator animator) || !animator.avatar) return; + + SkeletonBone[] skeletonBones = animator.avatar.humanDescription.skeleton; + + foreach (HumanBodyBones hbb in Enum.GetValues(typeof(HumanBodyBones))) { + if (hbb == HumanBodyBones.LastBone) continue; + + Transform boneTransform = animator.GetBoneTransform(hbb); + if (!boneTransform) continue; + + SkeletonBone skeletonBone = skeletonBones.FirstOrDefault(sb => sb.name == boneTransform.name); + if (skeletonBone.name == null) continue; + + if (hbb == HumanBodyBones.Hips) boneTransform.localPosition = skeletonBone.position; + boneTransform.localRotation = skeletonBone.rotation; + } + } + } +} + +#endif diff --git a/Assets/_Project/Scripts/AnimationEvents/Editor/AnimationEventStateBehaviourEditor.cs.meta b/Runtime/Editor/AnimationEventStateBehaviourEditor.cs.meta similarity index 100% rename from Assets/_Project/Scripts/AnimationEvents/Editor/AnimationEventStateBehaviourEditor.cs.meta rename to Runtime/Editor/AnimationEventStateBehaviourEditor.cs.meta diff --git a/Runtime/Editor/ImprovedUnityAnimationEvents.Editor.asmdef b/Runtime/Editor/ImprovedUnityAnimationEvents.Editor.asmdef new file mode 100644 index 0000000..a78ab33 --- /dev/null +++ b/Runtime/Editor/ImprovedUnityAnimationEvents.Editor.asmdef @@ -0,0 +1,18 @@ +{ + "name": "ImprovedUnityAnimationEvents.Editor", + "rootNamespace": "ImprovedUnityAnimationEvents", + "references": [ + "GUID:919e0586bef85cc4e95c0f6c823c98d4" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Runtime/Editor/ImprovedUnityAnimationEvents.Editor.asmdef.meta b/Runtime/Editor/ImprovedUnityAnimationEvents.Editor.asmdef.meta new file mode 100644 index 0000000..8575e75 --- /dev/null +++ b/Runtime/Editor/ImprovedUnityAnimationEvents.Editor.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: d09e765e7609ba14bbcb267d0a7068f2 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/ImprovedUnityAnimationEvents.asmdef b/Runtime/ImprovedUnityAnimationEvents.asmdef new file mode 100644 index 0000000..242abf0 --- /dev/null +++ b/Runtime/ImprovedUnityAnimationEvents.asmdef @@ -0,0 +1,14 @@ +{ + "name": "ImprovedUnityAnimationEvents", + "rootNamespace": "ImprovedUnityAnimationEvents", + "references": [], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Runtime/ImprovedUnityAnimationEvents.asmdef.meta b/Runtime/ImprovedUnityAnimationEvents.asmdef.meta new file mode 100644 index 0000000..5d96b28 --- /dev/null +++ b/Runtime/ImprovedUnityAnimationEvents.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 919e0586bef85cc4e95c0f6c823c98d4 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/package.json b/package.json new file mode 100644 index 0000000..b98e599 --- /dev/null +++ b/package.json @@ -0,0 +1,11 @@ +{ + "name": "com.gitamend.improved-unity-animation-events", + "version": "1.0.0", + "displayName": "Improved Unity Animation Events", + "description": "This system offers enhanced flexibility and control over Unity\u2019s Animation Event system, particularly valuable for managing complex animation events in both runtime and editor-time workflows.", + "unity": "2022.1", + "author": { + "name": "Git Amend", + "url": "https://www.youtube.com/@git-amend" + } +} \ No newline at end of file diff --git a/package.json.meta b/package.json.meta new file mode 100644 index 0000000..5af8815 --- /dev/null +++ b/package.json.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 1a8b013e07bae574ab5c824c238a295e +PackageManifestImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: