using System; using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.SceneManagement; using UnityEngine.UI; using TMPro; using DG.Tweening; public class UI_FunctionHoverGuide : MonoBehaviour { [Header("Scene")] public bool onlyInUiUi = true; public string uiUiSceneName = "UI_UI"; [Header("Config")] public UI_FunctionHoverConfig config; public string configResourcesName = "UI_FunctionHoverConfig"; public bool debugLogs = false; public bool useManualHover = true; [Header("Function References")] public string functionRootName = "Function"; public string functionRootPath = "UI_Panel_Main_main/Content/Function"; public string targetBasePath = "UI_Panel_Main_main"; public string iconNameContains = "ICON"; public string textNameContains = "TEXT"; public RectTransform functionRoot; public Image functionIcon; public RawImage functionIconRaw; public TMP_Text functionTextTmp; public Text functionTextLegacy; [Header("Animation")] public float iconStartScale = 0.6f; public float iconPopDuration = 0.35f; public int iconFlashCount = 2; public float iconFlashInterval = 0.05f; public float textTypeInterval = 0.02f; public bool useUnscaledTime = true; [Header("Gyro")] public bool enableGyro = false; public float gyroMaxRotation = 4f; public float gyroSmooth = 10f; private Sprite defaultIcon; private Texture defaultIconTexture; private string defaultText; private Vector3 iconBaseScale = Vector3.one; private bool iconBaseScaleCached; private bool initialized; private Coroutine typeRoutine; private Sequence iconSeq; private UI_FunctionHoverEntry currentEntry; private Coroutine retryRoutine; private readonly List hoverTargets = new(); private class HoverTarget { public UI_FunctionHoverEntry entry; public RectTransform rect; public float area; } private void OnEnable() { SceneManager.sceneLoaded += OnSceneLoaded; TryInit(); } private void OnDisable() { SceneManager.sceneLoaded -= OnSceneLoaded; } private void Update() { if (!initialized || !useManualHover) return; if (hoverTargets.Count == 0) return; if (!IsUiUiScene()) return; UI_FunctionHoverEntry entry = GetHoveredEntry(); if (entry == currentEntry) return; if (entry != null) ApplyEntry(entry); else if (currentEntry != null) ClearEntry(currentEntry); } private void OnSceneLoaded(Scene scene, LoadSceneMode mode) { initialized = false; TryInit(); } private void TryInit() { if (initialized) return; if (onlyInUiUi && !IsUiUiScene()) return; if (config == null && !string.IsNullOrEmpty(configResourcesName)) config = Resources.Load(configResourcesName); if (config == null) { Debug.LogWarning("UI_FunctionHoverGuide: config is missing. Create a UI_FunctionHoverConfig asset and assign it."); return; } ResolveFunctionRefs(); if (functionRoot == null || functionIcon == null || (functionTextTmp == null && functionTextLegacy == null)) { StartRetry(); return; } CacheDefault(); EnsureWrapping(); EnsureGyro(); int wired = WireTargets(); if (debugLogs) Debug.Log($"UI_FunctionHoverGuide: wired targets = {wired}, functionRoot = {functionRoot?.name}"); if (wired == 0) { StartRetry(); return; } initialized = true; } private void StartRetry() { if (retryRoutine != null) return; retryRoutine = StartCoroutine(RetryInit()); } private IEnumerator RetryInit() { int tries = 0; while (!initialized) { ResolveFunctionRefs(); if (functionRoot != null && functionIcon != null && (functionTextTmp != null || functionTextLegacy != null)) { CacheDefault(); EnsureWrapping(); EnsureGyro(); int wired = WireTargets(); if (debugLogs) Debug.Log($"UI_FunctionHoverGuide: retry wired targets = {wired}"); if (wired > 0) { initialized = true; break; } } tries++; yield return new WaitForSecondsRealtime(0.25f); } retryRoutine = null; } private bool IsUiUiScene() { if (string.IsNullOrEmpty(uiUiSceneName)) return true; var active = SceneManager.GetActiveScene().name; return active.IndexOf(uiUiSceneName, StringComparison.OrdinalIgnoreCase) >= 0; } private void ResolveFunctionRefs() { if (functionRoot == null) { GameObject go = null; if (!string.IsNullOrWhiteSpace(functionRootPath)) go = GameObject.Find(functionRootPath); if (go == null && !string.IsNullOrWhiteSpace(targetBasePath)) go = GameObject.Find(CombinePath(targetBasePath, functionRootName)); if (go == null) go = FindByNameContains(functionRootName); if (go != null) functionRoot = go.GetComponent(); } if (functionRoot == null) return; if (functionIcon == null) { var icon = FindChildGraphic(functionRoot, iconNameContains); if (icon != null) functionIcon = icon; } if (functionIconRaw == null && functionIcon == null) { var raw = FindChildRawImage(functionRoot, iconNameContains); if (raw != null) functionIconRaw = raw; } if (functionTextTmp == null && functionTextLegacy == null) { var tmp = FindChildTmp(functionRoot, textNameContains); if (tmp != null) functionTextTmp = tmp; else functionTextLegacy = FindChildLegacyText(functionRoot, textNameContains); } } private void CacheDefault() { if (functionIcon != null) defaultIcon = functionIcon.sprite; if (functionIconRaw != null) defaultIconTexture = functionIconRaw.texture; var iconGraphic = functionIcon != null ? (Graphic)functionIcon : functionIconRaw; if (iconGraphic != null && iconGraphic.rectTransform != null) { var scale = iconGraphic.rectTransform.localScale; if (IsFinite(scale) && scale != Vector3.zero) { iconBaseScale = scale; iconBaseScaleCached = true; } } if (functionTextTmp != null) defaultText = functionTextTmp.text; else if (functionTextLegacy != null) defaultText = functionTextLegacy.text; } private void EnsureWrapping() { if (functionTextTmp != null) { functionTextTmp.textWrappingMode = TextWrappingModes.Normal; functionTextTmp.overflowMode = TextOverflowModes.Overflow; } if (functionTextLegacy != null) { functionTextLegacy.horizontalOverflow = HorizontalWrapMode.Wrap; functionTextLegacy.verticalOverflow = VerticalWrapMode.Overflow; } } private void EnsureGyro() { if (functionRoot == null) return; var gyro = functionRoot.GetComponent(); if (gyro != null) { if (Application.isPlaying) Destroy(gyro); else DestroyImmediate(gyro); } } private int WireTargets() { if (config == null || config.entries == null) return 0; hoverTargets.Clear(); int wired = 0; for (int i = 0; i < config.entries.Count; i++) { var entry = config.entries[i]; if (entry == null || string.IsNullOrWhiteSpace(entry.path)) continue; var target = FindByPath(entry.path); if (target == null) { if (debugLogs) Debug.LogWarning("UI_FunctionHoverGuide: target not found for path: " + entry.path); continue; } if (AttachHoverTargets(target.transform, entry)) wired++; } return wired; } public void ApplyEntry(UI_FunctionHoverEntry entry) { if (entry == null) return; if (currentEntry == entry) return; currentEntry = entry; if (functionIcon != null && entry.icon != null) functionIcon.sprite = entry.icon; if (functionIconRaw != null) functionIconRaw.texture = entry.icon != null ? entry.icon.texture : defaultIconTexture; PlayIconAnim(); PlayTypewriter(entry.text); } public void ClearEntry(UI_FunctionHoverEntry entry) { if (currentEntry != entry) return; currentEntry = null; if (functionIcon != null && defaultIcon != null) functionIcon.sprite = defaultIcon; if (functionIconRaw != null && defaultIconTexture != null) functionIconRaw.texture = defaultIconTexture; PlayIconAnim(); PlayTypewriter(defaultText); } private void PlayIconAnim() { Graphic iconGraphic = functionIcon != null ? (Graphic)functionIcon : functionIconRaw; if (iconGraphic == null) return; var rt = iconGraphic.rectTransform; var baseScale = iconBaseScaleCached ? iconBaseScale : Vector3.one; if (!iconBaseScaleCached && rt != null) baseScale = rt.localScale; if (!IsFinite(baseScale)) baseScale = Vector3.one; if (iconSeq != null && iconSeq.IsActive()) iconSeq.Kill(); var color = iconGraphic.color; iconGraphic.color = new Color(color.r, color.g, color.b, 0f); if (rt != null) rt.localScale = baseScale * iconStartScale; iconSeq = DOTween.Sequence(); for (int i = 0; i < Mathf.Max(1, iconFlashCount); i++) { iconSeq.Append(iconGraphic.DOFade(1f, iconFlashInterval)); iconSeq.Append(iconGraphic.DOFade(0.25f, iconFlashInterval)); } iconSeq.Append(iconGraphic.DOFade(1f, iconFlashInterval)); if (rt != null) iconSeq.Join(rt.DOScale(baseScale, iconPopDuration).SetEase(Ease.OutBack)); } private static bool IsFinite(Vector3 v) { return !(float.IsNaN(v.x) || float.IsNaN(v.y) || float.IsNaN(v.z) || float.IsInfinity(v.x) || float.IsInfinity(v.y) || float.IsInfinity(v.z)); } private void PlayTypewriter(string text) { if (string.IsNullOrEmpty(text)) text = string.Empty; if (typeRoutine != null) StopCoroutine(typeRoutine); if (functionTextTmp != null) typeRoutine = StartCoroutine(TypeTmp(functionTextTmp, text)); else if (functionTextLegacy != null) typeRoutine = StartCoroutine(TypeLegacy(functionTextLegacy, text)); } private IEnumerator TypeTmp(TMP_Text tmp, string text) { tmp.text = text; tmp.maxVisibleCharacters = 0; tmp.ForceMeshUpdate(); int total = tmp.textInfo.characterCount; for (int i = 0; i <= total; i++) { tmp.maxVisibleCharacters = i; yield return Wait(textTypeInterval); } } private IEnumerator TypeLegacy(Text txt, string text) { txt.text = string.Empty; for (int i = 0; i <= text.Length; i++) { txt.text = text.Substring(0, i); yield return Wait(textTypeInterval); } } private IEnumerator Wait(float t) { if (useUnscaledTime) yield return new WaitForSecondsRealtime(t); else yield return new WaitForSeconds(t); } private GameObject FindByPath(string path) { if (string.IsNullOrWhiteSpace(path)) return null; var go = GameObject.Find(path); if (go != null) return go; if (!string.IsNullOrWhiteSpace(targetBasePath)) { var combined = CombinePath(targetBasePath, path); go = GameObject.Find(combined); if (go != null) return go; } string normalized = NormalizePath(path); if (string.IsNullOrEmpty(normalized)) return null; var all = Resources.FindObjectsOfTypeAll(); for (int i = 0; i < all.Length; i++) { var t = all[i]; if (t == null) continue; if (!t.gameObject.scene.IsValid() || !t.gameObject.scene.isLoaded) continue; string full = NormalizePath(GetFullPath(t)); if (string.Equals(full, normalized, StringComparison.OrdinalIgnoreCase) || full.EndsWith("/" + normalized, StringComparison.OrdinalIgnoreCase)) return t.gameObject; } string[] parts = path.Split('/'); string last = parts[parts.Length - 1]; return FindByNameContains(last); } private GameObject FindByNameContains(string name) { if (string.IsNullOrWhiteSpace(name)) return null; var all = Resources.FindObjectsOfTypeAll(); string target = name.Replace(" ", ""); for (int i = 0; i < all.Length; i++) { var t = all[i]; if (t == null) continue; if (!t.gameObject.scene.IsValid() || !t.gameObject.scene.isLoaded) continue; string n = t.name.Replace(" ", ""); if (n.IndexOf(target, StringComparison.OrdinalIgnoreCase) >= 0) return t.gameObject; } return null; } private static string CombinePath(string basePath, string path) { if (string.IsNullOrWhiteSpace(basePath)) return path; if (string.IsNullOrWhiteSpace(path)) return basePath; string trimmedBase = basePath.Trim().TrimEnd('/', '\\'); string trimmedPath = path.Trim().TrimStart('/', '\\'); if (trimmedPath.StartsWith(trimmedBase, StringComparison.OrdinalIgnoreCase)) return trimmedPath; return trimmedBase + "/" + trimmedPath; } private static string NormalizePath(string path) { if (string.IsNullOrEmpty(path)) return string.Empty; return path.Replace('\\', '/').Replace(" ", ""); } private static string GetFullPath(Transform t) { if (t == null) return string.Empty; var parts = new List(); var cur = t; while (cur != null) { parts.Add(cur.name); cur = cur.parent; } parts.Reverse(); return string.Join("/", parts); } private static Image FindChildGraphic(Transform root, string nameContains) { var imgs = root.GetComponentsInChildren(true); if (imgs == null || imgs.Length == 0) return null; if (!string.IsNullOrEmpty(nameContains)) { string target = nameContains.Replace(" ", ""); for (int i = 0; i < imgs.Length; i++) { var img = imgs[i]; if (img == null) continue; string n = img.name.Replace(" ", ""); if (n.IndexOf(target, StringComparison.OrdinalIgnoreCase) >= 0) return img; } } return imgs[0]; } private static RawImage FindChildRawImage(Transform root, string nameContains) { var raws = root.GetComponentsInChildren(true); if (raws == null || raws.Length == 0) return null; if (!string.IsNullOrEmpty(nameContains)) { string target = nameContains.Replace(" ", ""); for (int i = 0; i < raws.Length; i++) { var img = raws[i]; if (img == null) continue; string n = img.name.Replace(" ", ""); if (n.IndexOf(target, StringComparison.OrdinalIgnoreCase) >= 0) return img; } } return raws[0]; } private static TMP_Text FindChildTmp(Transform root, string nameContains) { var tmps = root.GetComponentsInChildren(true); if (tmps == null || tmps.Length == 0) return null; if (!string.IsNullOrEmpty(nameContains)) { string target = nameContains.Replace(" ", ""); for (int i = 0; i < tmps.Length; i++) { var t = tmps[i]; if (t == null) continue; string n = t.name.Replace(" ", ""); if (n.IndexOf(target, StringComparison.OrdinalIgnoreCase) >= 0) return t; } } return tmps[0]; } private static Text FindChildLegacyText(Transform root, string nameContains) { var texts = root.GetComponentsInChildren(true); if (texts == null || texts.Length == 0) return null; if (!string.IsNullOrEmpty(nameContains)) { string target = nameContains.Replace(" ", ""); for (int i = 0; i < texts.Length; i++) { var t = texts[i]; if (t == null) continue; string n = t.name.Replace(" ", ""); if (n.IndexOf(target, StringComparison.OrdinalIgnoreCase) >= 0) return t; } } return texts[0]; } private static void EnsureRaycastTarget(GameObject go) { var graphics = go.GetComponentsInChildren(true); if (graphics != null && graphics.Length > 0) { for (int i = 0; i < graphics.Length; i++) { if (graphics[i] != null) graphics[i].raycastTarget = true; } return; } var img = go.AddComponent(); img.color = new Color(1f, 1f, 1f, 0f); img.raycastTarget = true; } private bool AttachHoverTargets(Transform root, UI_FunctionHoverEntry entry) { if (root == null || entry == null) return false; Transform attachRoot = root; Button button = root.GetComponent