From 59ffa462ccd4b40dfcd2a7f9e9df67824a3d9f01 Mon Sep 17 00:00:00 2001 From: originalnicodr Date: Tue, 13 Feb 2024 00:48:22 -0300 Subject: [PATCH] Added support to manually play animations of entities on the scene (only for Mono builds). Also refactored the AnimatorPanel with a pool of objects. --- src/Cinematic/AnimatorUI/AnimatorCell.cs | 153 ++++++++++++++++++++++ src/UI/Panels/AnimatorPanel.cs | 156 +++++++++++------------ 2 files changed, 229 insertions(+), 80 deletions(-) create mode 100644 src/Cinematic/AnimatorUI/AnimatorCell.cs diff --git a/src/Cinematic/AnimatorUI/AnimatorCell.cs b/src/Cinematic/AnimatorUI/AnimatorCell.cs new file mode 100644 index 0000000..101a1d2 --- /dev/null +++ b/src/Cinematic/AnimatorUI/AnimatorCell.cs @@ -0,0 +1,153 @@ +using UnityEngine; +using UniverseLib.UI; +using UniverseLib.UI.Models; +using UniverseLib.UI.Widgets.ScrollView; + +#if CPP +using Animator = UnityEngine.Behaviour; +#endif + +namespace UnityExplorer.UI.Panels +{ + public class AnimatorCell : ICell + { + private bool autoIgnoreMasterToggleSet; + public Toggle IgnoreMasterToggle; + public Toggle AnimatorToggle; + + public Text name; + public Animator animator; + + // ICell + public float DefaultHeight => 25f; + public GameObject UIRoot { get; set; } + public RectTransform Rect { get; set; } + + public bool Enabled => UIRoot.activeSelf; + public void Enable() => UIRoot.SetActive(true); + public void Disable() => UIRoot.SetActive(false); + +// IL2CPP games seem to have animation-related code stripped from their builds +#if MONO + ButtonRef playButton; + private bool manuallyPlayedAnimation; + private bool skippedStopFrames; + + private AnimationClip currentAnimation; + private AnimationClip defaultAnimation; + + public void DrawAnimatorPlayer(){ + if (playButton == null){ + List animations = animator.runtimeAnimatorController.animationClips.OrderBy(x=>x.name).Where(c => c.length > 0).Distinct().ToList(); + + //ExplorerCore.LogWarning(animations.Count); + AnimatorClipInfo[] playingAnimations = animator.GetCurrentAnimatorClipInfo(0); + currentAnimation = playingAnimations.Count() != 0 ? playingAnimations[0].clip : animations[0]; + + GameObject currentAnimationObj = UIFactory.CreateDropdown(UIRoot, $"Animations_{name}", out Dropdown dropdown, null, 14, (idx) => currentAnimation = animations[idx]); + UIFactory.SetLayoutElement(currentAnimationObj, minHeight: 25, minWidth: 100); + foreach (AnimationClip animation in animations) + dropdown.options.Add(new Dropdown.OptionData(animation.name)); + + playButton = UIFactory.CreateButton(UIRoot, "PlayButton", "Play", new Color(0.2f, 0.26f, 0.2f)); + UIFactory.SetLayoutElement(playButton.Component.gameObject, minHeight: 5, minWidth: 90); + playButton.OnClick += PlayButton_OnClick; + } + } + + private void PlayButton_OnClick(){ + // We save the last animation played by the game in case we want to go back to it + if (defaultAnimation == null){ + AnimatorClipInfo[] playingAnimations = animator.GetCurrentAnimatorClipInfo(0); + defaultAnimation = playingAnimations.Count() != 0 ? playingAnimations[0].clip : null; + } + + skippedStopFrames = false; + + AnimatorToggle.isOn = true; + manuallyPlayedAnimation = true; + animator.Play(currentAnimation.name); + } + + // Disables the animator when the animation we manually triggered isn't present on the subject anymore + public bool IsPlayingSelectedAnimation(){ + if (animator != null && currentAnimation != null && name.text == "Player"){ + if (manuallyPlayedAnimation && !GetAllCurrentAnimations().Contains(currentAnimation)){ + + if (!skippedStopFrames){ + skippedStopFrames = true; + return false; + } + + manuallyPlayedAnimation = false; + AnimatorToggle.isOn = false; + return true; + } + } + return false; + } + + private List GetAllCurrentAnimations(){ + List allAnimations = new List(); + for (int layer = 0; layer < animator.layerCount; layer++){ + allAnimations.AddRange(animator.GetCurrentAnimatorClipInfo(layer).Select(ainfo => ainfo.clip).ToList()); + } + return allAnimations; + } + + public void ResetAnimation(){ + if (defaultAnimation != null){ + manuallyPlayedAnimation = false; + + animator.Play(defaultAnimation.name); + AnimatorToggle.isOn = true; + defaultAnimation = null; + } + } +#endif + + // If it's the first time we are rendering the AnimatorCell, assign the ignore value automatically based on it's name + public void MaybeSetIgnoreMasterToggleSet(){ + if (animator != null && !autoIgnoreMasterToggleSet){ + // A weird canse insensitive "Contains" to identify the player + IgnoreMasterToggle.isOn = animator.gameObject.name.IndexOf("play", 0, StringComparison.OrdinalIgnoreCase) >= 0; + autoIgnoreMasterToggleSet = true; + } + } + + public virtual GameObject CreateContent(GameObject parent) + { + GameObject AnimatorToggleObj = UIFactory.CreateToggle(parent, $"AnimatorToggle", out AnimatorToggle, out Text animatorToggleText); + UIFactory.SetLayoutElement(AnimatorToggleObj, minHeight: 25); + AnimatorToggle.isOn = true; + AnimatorToggle.onValueChanged.AddListener(value => animator.enabled = value); + + UIRoot = AnimatorToggleObj; + UIRoot.SetActive(false); + + Rect = UIRoot.GetComponent(); + Rect.anchorMin = new Vector2(0, 1); + Rect.anchorMax = new Vector2(0, 1); + Rect.pivot = new Vector2(0.5f, 1); + Rect.sizeDelta = new Vector2(25, 25); + + UIFactory.SetLayoutElement(UIRoot, minWidth: 100, flexibleWidth: 9999, minHeight: 25, flexibleHeight: 0); + + name = UIFactory.CreateLabel(UIRoot, "NameLabel", "", TextAnchor.MiddleLeft, Color.white, false, 14); + UIFactory.SetLayoutElement(name.gameObject, minHeight: 25, minWidth: 30, flexibleWidth: 40); + +#if MONO + ButtonRef resetAnimation = UIFactory.CreateButton(UIRoot, "Reset Animation", "Reset"); + UIFactory.SetLayoutElement(resetAnimation.GameObject, minWidth: 50, minHeight: 25); + resetAnimation.OnClick += ResetAnimation; +#endif + + GameObject ignoresMasterTogglerObj = UIFactory.CreateToggle(UIRoot, $"AnimatorIgnoreMasterToggle", out IgnoreMasterToggle, out Text ignoreMasterToggleText); + UIFactory.SetLayoutElement(ignoresMasterTogglerObj, minHeight: 25); + IgnoreMasterToggle.isOn = false; + ignoreMasterToggleText.text = "Ignore Master Toggle "; + + return UIRoot; + } + } +} diff --git a/src/UI/Panels/AnimatorPanel.cs b/src/UI/Panels/AnimatorPanel.cs index 25f8a21..e722b56 100644 --- a/src/UI/Panels/AnimatorPanel.cs +++ b/src/UI/Panels/AnimatorPanel.cs @@ -4,6 +4,7 @@ using UnityExplorer.Inspectors; using UniverseLib.UI; using UniverseLib.UI.Models; using UniverseLib.UI.ObjectPool; +using UniverseLib.UI.Widgets.ScrollView; using UnityEngine; #if UNHOLLOWER using UnhollowerRuntimeLib; @@ -12,9 +13,13 @@ using UnhollowerRuntimeLib; using Il2CppInterop.Runtime.Injection; #endif +#if CPP +using Animator = UnityEngine.Behaviour; +#endif + namespace UnityExplorer.UI.Panels { - internal class AnimatorPanel : UEPanel + internal class AnimatorPanel : UEPanel, ICellPoolDataSource { public AnimatorPanel(UIBase owner) : base(owner) { @@ -23,106 +28,68 @@ namespace UnityExplorer.UI.Panels public override string Name => "Animator"; public override UIManager.Panels PanelType => UIManager.Panels.AnimatorPanel; public override int MinWidth => 500; - public override int MinHeight => 500; + public override int MinHeight => 200; public override Vector2 DefaultAnchorMin => new(0.4f, 0.4f); public override Vector2 DefaultAnchorMax => new(0.6f, 0.6f); public override bool NavButtonWanted => true; public override bool ShouldSaveActiveState => true; List UIElements = new List(); - internal Dictionary> animators = new Dictionary>(); - internal Dictionary> animatorToggles = new Dictionary>(); - internal Dictionary ignoresMasterToggler = new Dictionary(); + Dictionary> animationEndedFunctions = new Dictionary>(); + Toggle masterAnimatorToggle = new Toggle(); + private static ScrollPool animatorScrollPool; + internal List animators = new List(); + public int ItemCount => animators.Count; + + private static bool DoneScrollPoolInit; + + public override void SetActive(bool active) + { + base.SetActive(active); + + if (active && !DoneScrollPoolInit) + { + LayoutRebuilder.ForceRebuildLayoutImmediate(this.Rect); + animatorScrollPool.Initialize(this); + DoneScrollPoolInit = true; + } + + animatorScrollPool.Refresh(true, false); + } + private void FindAllAnimators(){ // Enable all animators on refresh if (animators.Count != 0) { masterAnimatorToggle.isOn = true; // Will also trigger "MasterToggleAnimators(true)" animators.Clear(); - animatorToggles.Clear(); - ignoresMasterToggler.Clear(); + animationEndedFunctions.Clear(); +#if MONO + foreach (AnimatorCell animatorCell in animatorScrollPool.CellPool) + { + animatorCell.ResetAnimation(); + } +#endif } Type searchType = ReflectionUtility.GetTypeByName("UnityEngine.Animator"); searchType = searchType is Type type ? type : searchType.GetActualType(); - List animatorsList = RuntimeHelper.FindObjectsOfTypeAll(searchType).Select(obj => obj.TryCast()) + animators = RuntimeHelper.FindObjectsOfTypeAll(searchType).Select(obj => obj.TryCast()) .Where(a => a.GetComponentsInChildren(false).Length != 0 && a.enabled && a.GetComponentsInChildren(false).Length != 0) + .OrderBy(x=>x.name) .ToList(); - foreach(Behaviour animator in animatorsList){ - GameObject gObj = animator.gameObject; - string key = getNameWithoutClone(gObj.name); - - if (!animators.ContainsKey(key)){ - animators.Add(key, new List()); - } - - animators[key].Add(animator); - } - - BuildAnimatorsTogglers(); - } - - private string getNameWithoutClone(string name){ - string newName = name; - while (newName.EndsWith("(Clone)")){ - newName = newName.Substring(0, newName.Length - 7); - } - - return newName; - } - - private void BuildAnimatorsTogglers(){ - foreach (var comp in UIElements){ - UnityEngine.Object.Destroy(comp); - //UIElements.Remove(comp); - } - - foreach (KeyValuePair> entry in animators) - { - // A weird canse insensitive "Contains" to identify the player - ignoresMasterToggler[entry.Key] = entry.Key.IndexOf("play", 0, StringComparison.OrdinalIgnoreCase) >= 0; - - GameObject horiGroup = UIFactory.CreateHorizontalGroup(ContentRoot, $"{entry.Key}_Group", false, false, true, false, 4, - default, new Color(1, 1, 1, 0), TextAnchor.MiddleLeft); - UIFactory.SetLayoutElement(horiGroup, minHeight: 25, flexibleWidth: 9999); - UIElements.Add(horiGroup); - - // is Affected by Master Toggler toggle - Toggle ignoresMasterTogglerToggle; - GameObject ignoresMasterTogglerObj = UIFactory.CreateToggle(horiGroup, $"Toggle {entry.Key}", out ignoresMasterTogglerToggle, out Text ignoresMasterTogglerText); - UIFactory.SetLayoutElement(ignoresMasterTogglerObj, minHeight: 25); - ignoresMasterTogglerToggle.isOn = ignoresMasterToggler[entry.Key]; - ignoresMasterTogglerToggle.onValueChanged.AddListener(value => ignoresMasterToggler[entry.Key] = value); - ignoresMasterTogglerText.text = entry.Key; - - foreach (Behaviour entityAnimator in entry.Value){ - Toggle animatorToggle; - GameObject animatorObj = UIFactory.CreateToggle(horiGroup, $"Toggle {entry.Key}", out animatorToggle, out Text animatorToggleText); - - if (!animatorToggles.ContainsKey(entry.Key)){ - animatorToggles.Add(entry.Key, new List()); - } - animatorToggles[entry.Key].Add(animatorToggle); - - UIFactory.SetLayoutElement(animatorObj, minHeight: 25); - animatorToggle.isOn = true; - animatorToggle.onValueChanged.AddListener(value => entityAnimator.enabled = value); - } - - } + animatorScrollPool.Refresh(true, false); } public void MasterToggleAnimators(bool enable){ // Load animators for the first time if there are not any if (animators.Count == 0) FindAllAnimators(); - foreach (KeyValuePair> entry in animators) + foreach (AnimatorCell animatorCell in animatorScrollPool.CellPool) { - if (!ignoresMasterToggler[entry.Key]){ - foreach (Toggle animatorToggle in animatorToggles[entry.Key]){ - animatorToggle.isOn = enable; - } + if (animatorCell.animator != null && !animatorCell.IgnoreMasterToggle.isOn){ + animatorCell.AnimatorToggle.isOn = enable; } } } @@ -131,6 +98,13 @@ namespace UnityExplorer.UI.Panels masterAnimatorToggle.isOn = !masterAnimatorToggle.isOn; } +#if MONO + public override void Update(){ + foreach (Animator animator in animators){ + animationEndedFunctions[animator](); + } + } +#endif // ~~~~~~~~ UI construction / callbacks ~~~~~~~~ protected override void ConstructPanelContent() @@ -154,11 +128,33 @@ namespace UnityExplorer.UI.Panels default, new Color(1, 1, 1, 0), TextAnchor.MiddleLeft); UIFactory.SetLayoutElement(firstGroup, minHeight: 25, flexibleWidth: 9999); - Text isPlayerLabel = UIFactory.CreateLabel(secondGroup, "IsAffectedByMasterToggler", "Ignores Master Toggler", TextAnchor.MiddleLeft, Color.white, false, 15); - UIFactory.SetLayoutElement(isPlayerLabel.gameObject, minHeight: 25, minWidth: 25, flexibleWidth: 0); - - Text enableLabel = UIFactory.CreateLabel(secondGroup, "EnableLabel", "Enable ", TextAnchor.MiddleRight, Color.white, false, 15); - UIFactory.SetLayoutElement(enableLabel.gameObject, minHeight: 25, minWidth: 75, flexibleWidth: 9999); + animatorScrollPool = UIFactory.CreateScrollPool(ContentRoot, "AnimatorsList", out GameObject scrollObj, + out GameObject scrollContent, new Color(0.03f, 0.03f, 0.03f)); + UIFactory.SetLayoutElement(scrollObj, flexibleWidth: 9999, flexibleHeight: 9999); } + + public void SetCell(AnimatorCell cell, int index) + { + if (index >= animators.Count) + { + cell.Disable(); + return; + } + + Animator animator = animators[index]; + if (animator == null) + return; + + cell.animator = animator; + cell.name.text = animator.gameObject.name; + + cell.MaybeSetIgnoreMasterToggleSet(); +#if MONO + cell.DrawAnimatorPlayer(); + animationEndedFunctions[animator] = cell.IsPlayingSelectedAnimation; +#endif + } + + public void OnCellBorrowed(AnimatorCell cell) { } } }