LightGlueUIManager.cs 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597
  1. using LightGlue.Unity.Game;
  2. using LightGlue.Unity.Config;
  3. using System.Collections.Generic;
  4. using TMPro;
  5. using UnityEngine;
  6. using UnityEngine.SceneManagement;
  7. using UnityEngine.UI;
  8. namespace LightGlue.Unity.UI
  9. {
  10. /// <summary>
  11. /// LightGlueUI 全局管理器:
  12. /// - 单例(防止切场景重复生成 UI)
  13. /// - 提供 Show/Hide/Toggle 控制 UI 显示
  14. /// - 可选 DontDestroyOnLoad(常驻)或随场景销毁
  15. /// </summary>
  16. public sealed class LightGlueUIManager : MonoBehaviour
  17. {
  18. public enum UIType
  19. {
  20. Demo = 0,
  21. Plugin = 1,
  22. }
  23. public static LightGlueUIManager Instance { get; private set; }
  24. [Header("Roma 系统预制体")]
  25. [Tooltip("RomaManager 预制,由 GameManager 在 Awake 时生成(跨场景持久化)。若未配置则不会创建。")]
  26. [SerializeField]
  27. private GameObject romaSystemPrefab;
  28. [Header("BleUI(可选,仅 Demo 使用)")]
  29. [Tooltip("是否在生成 LightGlueUI 后,自动在 BLEContainer 下挂载 BleUI 预制。")]
  30. [SerializeField]
  31. private bool enableBleUi = false;
  32. [Tooltip("BleUI 预制体(建议放在 Assets/Demo/Prefabs/BleUI.prefab)。未配置则不会创建。")]
  33. [SerializeField]
  34. private GameObject bleUiPrefab;
  35. [Tooltip("LightGlueUI 里用于挂载 BleUI 的容器节点名。")]
  36. [SerializeField]
  37. private string bleContainerName = "BLEContainer";
  38. [Header("Root")]
  39. [Tooltip("UI 根节点(默认使用当前 GameObject)。隐藏/显示时会对该节点 SetActive。")]
  40. public GameObject uiRoot;
  41. [Header("Optional Anchors")]
  42. [Tooltip("用于挂载 BLE UI 的容器节点(可选)。若不填,外部可按名称查找。")]
  43. public Transform bleContainer;
  44. [Header("Type Visibility")]
  45. [Tooltip("当前 UI 类型(Demo / Plugin)。可由 GameManager 在启动时设置。")]
  46. public UIType currentUIType = UIType.Demo;
  47. [Tooltip("仅 Demo 模式显示的节点列表。")]
  48. public List<GameObject> showInDemo = new List<GameObject>();
  49. [Tooltip("仅 Plugin 模式显示的节点列表。")]
  50. public List<GameObject> showInPlugin = new List<GameObject>();
  51. [Tooltip("切换类型时是否自动隐藏另一类型的节点。")]
  52. public bool hideOtherTypeNodes = true;
  53. [Header("Lifecycle")]
  54. [Tooltip("是否常驻(DontDestroyOnLoad)。关闭则随场景销毁。")]
  55. public bool dontDestroyOnLoad = false;
  56. [Tooltip("若已有实例存在,是否自动销毁当前重复实例。建议保持 true,避免叠 UI/重复 EventSystem。")]
  57. public bool destroyDuplicateInstance = true;
  58. [Header("Startup")]
  59. [Tooltip("启动时是否自动显示 UI。")]
  60. public bool showOnStart = true;
  61. [Tooltip("Plugin 模式下生成时默认隐藏 UI(即使 showOnStart=true)。插件侧可按需再 ShowUI。")]
  62. public bool hideUiOnStartInPlugin = true;
  63. [Header("Scene Auto Hide")]
  64. [Tooltip("跨场景切换时自动 HideUI(常用于 DontDestroyOnLoad 的 UI,避免切到别的场景仍遮挡)。")]
  65. public bool autoHideUiOnSceneChanged = true;
  66. [Tooltip("跨场景切换时同时隐藏 CursorSettings 面板。")]
  67. public bool autoHideCursorSettingsOnSceneChanged = true;
  68. [Header("Scene Visibility (Optional)")]
  69. [Tooltip("UI 显示白名单:切到这些场景时自动 ShowUI;不在名单中则按默认策略隐藏。\n留空=不启用白名单(切场景默认隐藏)。")]
  70. public List<string> showUiInScenes = new List<string>();
  71. [Tooltip("受 showUiInScenes 条件控制的节点:命中白名单场景显示,否则隐藏。\n(独立于 uiRoot,可用于只在特定场景显示部分 UI)")]
  72. public List<GameObject> showInScenesNodes = new List<GameObject>();
  73. [Tooltip("CursorSettings 面板显示白名单:切到这些场景时允许保持显示(否则隐藏)。\n留空=不启用白名单(切场景默认隐藏)。")]
  74. public List<string> showCursorSettingsInScenes = new List<string>();
  75. [Header("Cursor Settings (optional)")]
  76. [Tooltip("LightGlueCursorSettings 预制,由 LightGlueUIManager 在 Awake 时生成;Button 可通过本类显示/隐藏。")]
  77. [SerializeField]
  78. private GameObject lightGlueCursorSettingsPrefab;
  79. [Tooltip("是否让 CursorSettings 常驻(DontDestroyOnLoad)。通常建议 true。")]
  80. [SerializeField]
  81. private bool cursorSettingsDontDestroyOnLoad = true;
  82. [Header("Scene Loading (optional)")]
  83. [SerializeField]
  84. private string sceneToLoad = "GameScene";
  85. public bool IsVisible => (uiRoot != null ? uiRoot.activeSelf : gameObject.activeSelf);
  86. private GameObject _cursorSettingsInstance;
  87. [Header("Reference Resize (统一与 Python --resize 一致)")]
  88. [Tooltip("参考图像 resize 下拉框。若不绑定,将在运行时尝试克隆一个现有 TMP_Dropdown 作为样式,并生成到 UIRoot 下。")]
  89. [SerializeField]
  90. private TMP_Dropdown resizeDropdown;
  91. [Tooltip("resize 预设列表(格式:\"320x240\")。")]
  92. [SerializeField]
  93. private List<string> resizePresets = new List<string> { "320x240", "640x480" };
  94. private bool _resizeDropdownInitialized;
  95. #region 生成RomaUI管理插件部分
  96. public static void Create(UIType uiType = UIType.Plugin, GameObject parentObj = null)
  97. {
  98. if (Instance != null) { return;};
  99. // 这里按你项目资源路径调整
  100. GameObject o = Object.Instantiate(Resources.Load<GameObject>("LightGlueUI"));
  101. Object.DontDestroyOnLoad(o);
  102. CanvasScaler canvasScaler = o.GetComponent<CanvasScaler>();
  103. if (canvasScaler != null)
  104. {
  105. Destroy(canvasScaler);
  106. }
  107. RectTransform rectTransform = o.GetComponent<RectTransform>();
  108. rectTransform.anchorMin = Vector2.zero; // 左下角对齐父级
  109. rectTransform.anchorMax = Vector2.one; // 右上角对齐父级
  110. rectTransform.offsetMin = Vector2.zero; // 移除左下角偏移
  111. rectTransform.offsetMax = Vector2.zero; // 移除右上角偏移
  112. rectTransform.localScale = Vector3.one; // 确保缩放为 1
  113. Instance = o.GetComponent<LightGlueUIManager>();
  114. // 挂到你们 ViewMgr 层级(按你实际节点修改)
  115. if (parentObj != null)
  116. {
  117. if (parentObj != null) o.transform.SetParent(parentObj.transform, false);
  118. }
  119. Instance.SetUIType(uiType);
  120. Instance.OnCreated();
  121. }
  122. // 初始化运行时
  123. private void OnCreated()
  124. {
  125. // 确保全局 RomaManager 只创建一次(跨场景)
  126. if (Roma.RomaManager.Instance == null && romaSystemPrefab != null)
  127. {
  128. GameObject romaSystemObj = Instantiate(romaSystemPrefab);
  129. //setViewMgrParent(romaSystemObj,this.gameObject);
  130. Debug.Log("[GameManager] RomaSystem prefab instantiated.");
  131. }
  132. // 可选:在 LightGlueUI 下挂载 BleUI(Demo 专用,插件用户可不配置)
  133. TryAttachBleUi();
  134. }
  135. private void TryAttachBleUi()
  136. {
  137. if (currentUIType != LightGlueUIManager.UIType.Demo) return;
  138. if (!enableBleUi) return;
  139. if (bleUiPrefab == null) return;
  140. var uiMgr = LightGlueUIManager.Instance != null ? LightGlueUIManager.Instance : FindObjectOfType<LightGlueUIManager>();
  141. if (uiMgr == null || uiMgr.uiRoot == null) return;
  142. Transform container = uiMgr.bleContainer != null ? uiMgr.bleContainer : FindChildByName(uiMgr.uiRoot.transform, bleContainerName);
  143. if (container == null) return;
  144. // 防重复:避免直接引用 BLEUI 类型(插件导出时可能不包含 BLEUI.cs)
  145. // 这里按 BleUI 预制名判断是否已挂载过实例。
  146. string bleUiName = bleUiPrefab.name;
  147. if (HasChildNamed(container, bleUiName)) return;
  148. var inst = Instantiate(bleUiPrefab, container);
  149. inst.name = bleUiPrefab.name;
  150. Debug.Log("[GameManager] BleUI prefab attached under BLEContainer.");
  151. }
  152. private static Transform FindChildByName(Transform root, string childName)
  153. {
  154. if (root == null || string.IsNullOrWhiteSpace(childName)) return null;
  155. if (root.name == childName) return root;
  156. for (int i = 0; i < root.childCount; i++)
  157. {
  158. Transform t = root.GetChild(i);
  159. var found = FindChildByName(t, childName);
  160. if (found != null) return found;
  161. }
  162. return null;
  163. }
  164. private static bool HasChildNamed(Transform root, string name)
  165. {
  166. if (root == null || string.IsNullOrWhiteSpace(name)) return false;
  167. for (int i = 0; i < root.childCount; i++)
  168. {
  169. var t = root.GetChild(i);
  170. if (t == null) continue;
  171. if (t.name == name || t.name == $"{name}(Clone)") return true;
  172. }
  173. return false;
  174. }
  175. #endregion
  176. private void Awake()
  177. {
  178. if (uiRoot == null) uiRoot = gameObject;
  179. if (Instance != null && Instance != this)
  180. {
  181. if (destroyDuplicateInstance)
  182. {
  183. Destroy(gameObject);
  184. return;
  185. }
  186. Instance = this;
  187. }
  188. else
  189. {
  190. Instance = this;
  191. }
  192. if (dontDestroyOnLoad)
  193. DontDestroyOnLoad(gameObject);
  194. EnsureCursorSettingsInstance();
  195. }
  196. private void OnEnable()
  197. {
  198. SceneManager.activeSceneChanged += HandleActiveSceneChanged;
  199. }
  200. private void OnDisable()
  201. {
  202. SceneManager.activeSceneChanged -= HandleActiveSceneChanged;
  203. }
  204. private void Start()
  205. {
  206. // Plugin 模式:默认隐藏,避免插件启动即遮挡画面
  207. if (currentUIType == UIType.Plugin && hideUiOnStartInPlugin)
  208. {
  209. HideUI();
  210. if (autoHideCursorSettingsOnSceneChanged) HideCursorSettingsPanel();
  211. }
  212. else
  213. {
  214. if (showOnStart) ShowUI();
  215. else HideUI();
  216. }
  217. ApplyUITypeVisibility();
  218. ApplySceneVisibilityNodes(SceneManager.GetActiveScene().name);
  219. EnsureResizeDropdown();
  220. }
  221. private void EnsureResizeDropdown()
  222. {
  223. if (_resizeDropdownInitialized) return;
  224. _resizeDropdownInitialized = true;
  225. ReferenceImageResizeService.EnsureInitialized();
  226. if (resizeDropdown == null)
  227. {
  228. resizeDropdown = TryCloneAnyTmpDropdown();
  229. }
  230. if (resizeDropdown == null)
  231. {
  232. Debug.LogWarning("[LightGlueUI] 未找到/未生成 resizeDropdown(TMP_Dropdown)。请在 LightGlueUIManager Inspector 绑定一个 TMP_Dropdown,或确保 UI 中存在可克隆的 TMP_Dropdown。");
  233. return;
  234. }
  235. var options = new List<TMP_Dropdown.OptionData>();
  236. for (int i = 0; i < resizePresets.Count; i++)
  237. {
  238. string s = resizePresets[i];
  239. if (string.IsNullOrWhiteSpace(s)) continue;
  240. options.Add(new TMP_Dropdown.OptionData(s.Trim()));
  241. }
  242. if (options.Count == 0)
  243. {
  244. options.Add(new TMP_Dropdown.OptionData("320x240"));
  245. }
  246. resizeDropdown.onValueChanged.RemoveListener(OnResizeDropdownChanged);
  247. resizeDropdown.ClearOptions();
  248. resizeDropdown.AddOptions(options);
  249. int currentIndex = FindPresetIndex(ReferenceImageResizeService.Width, ReferenceImageResizeService.Height, resizePresets);
  250. if (currentIndex < 0) currentIndex = 0;
  251. resizeDropdown.SetValueWithoutNotify(currentIndex);
  252. resizeDropdown.onValueChanged.AddListener(OnResizeDropdownChanged);
  253. }
  254. private TMP_Dropdown TryCloneAnyTmpDropdown()
  255. {
  256. if (uiRoot == null) uiRoot = gameObject;
  257. Transform parent = uiRoot.transform;
  258. TMP_Dropdown sample = null;
  259. var dropdowns = uiRoot.GetComponentsInChildren<TMP_Dropdown>(true);
  260. if (dropdowns != null && dropdowns.Length > 0)
  261. {
  262. // 优先找名字含 "resolution" 的下拉框(现有UI里通常有)
  263. for (int i = 0; i < dropdowns.Length; i++)
  264. {
  265. var d = dropdowns[i];
  266. if (d != null && d.name.ToLowerInvariant().Contains("resolution"))
  267. {
  268. sample = d;
  269. break;
  270. }
  271. }
  272. if (sample == null)
  273. sample = dropdowns[0];
  274. }
  275. if (sample == null) return null;
  276. var inst = Instantiate(sample.gameObject, sample.transform.parent != null ? sample.transform.parent : parent);
  277. inst.name = "ResizeDropdown";
  278. // 尽量让它在 sample 旁边,避免遮挡
  279. var rt = inst.GetComponent<RectTransform>();
  280. var sampleRt = sample.GetComponent<RectTransform>();
  281. if (rt != null && sampleRt != null)
  282. {
  283. rt.anchorMin = sampleRt.anchorMin;
  284. rt.anchorMax = sampleRt.anchorMax;
  285. rt.pivot = sampleRt.pivot;
  286. rt.sizeDelta = sampleRt.sizeDelta;
  287. rt.anchoredPosition = sampleRt.anchoredPosition + new Vector2(0, - (sampleRt.sizeDelta.y + 10f));
  288. rt.localScale = sampleRt.localScale;
  289. }
  290. var dd = inst.GetComponent<TMP_Dropdown>();
  291. if (dd == null) return null;
  292. // 尝试改一下 label,方便辨识(不保证所有模板都有 captionText)
  293. if (dd.captionText != null)
  294. dd.captionText.text = "Resize";
  295. return dd;
  296. }
  297. private static int FindPresetIndex(int w, int h, List<string> presets)
  298. {
  299. if (presets == null) return -1;
  300. string target = $"{w}x{h}";
  301. for (int i = 0; i < presets.Count; i++)
  302. {
  303. string s = presets[i];
  304. if (string.IsNullOrWhiteSpace(s)) continue;
  305. if (NormalizePreset(s) == target) return i;
  306. }
  307. return -1;
  308. }
  309. private static string NormalizePreset(string s)
  310. {
  311. s = s.Trim().ToLowerInvariant().Replace(" ", "");
  312. s = s.Replace("*", "x").Replace("×", "x");
  313. return s;
  314. }
  315. private void OnResizeDropdownChanged(int index)
  316. {
  317. if (resizePresets == null || resizePresets.Count == 0) return;
  318. if (index < 0 || index >= resizePresets.Count) return;
  319. string preset = resizePresets[index];
  320. if (string.IsNullOrWhiteSpace(preset)) return;
  321. preset = NormalizePreset(preset);
  322. var parts = preset.Split('x');
  323. if (parts.Length != 2) return;
  324. if (!int.TryParse(parts[0], out int w)) return;
  325. if (!int.TryParse(parts[1], out int h)) return;
  326. ReferenceImageResizeService.Set(w, h, persistToNetworkConfig: true);
  327. }
  328. private void OnDestroy()
  329. {
  330. SceneManager.activeSceneChanged -= HandleActiveSceneChanged;
  331. if (Instance == this) Instance = null;
  332. }
  333. private void HandleActiveSceneChanged(Scene oldScene, Scene newScene)
  334. {
  335. // 默认策略:切场景隐藏(不配置白名单时就是这个行为)
  336. // 白名单策略:配置 showUiInScenes / showCursorSettingsInScenes 后,命中名单则显示,否则隐藏
  337. if (autoHideUiOnSceneChanged)
  338. {
  339. // Plugin + 默认隐藏:切场景时永远不自动显示(由插件侧自行 ShowUI)
  340. if (currentUIType == UIType.Plugin && hideUiOnStartInPlugin)
  341. {
  342. HideUI();
  343. ApplySceneVisibilityNodes(newScene.name);
  344. }
  345. else if (IsSceneInList(showUiInScenes, newScene.name))
  346. {
  347. ShowUI();
  348. ApplySceneVisibilityNodes(newScene.name);
  349. }
  350. else
  351. {
  352. HideUI();
  353. ApplySceneVisibilityNodes(newScene.name);
  354. }
  355. }
  356. else
  357. {
  358. // 即使不做 autoHideUiOnSceneChanged,也刷新一次 showInScenesNodes
  359. ApplySceneVisibilityNodes(newScene.name);
  360. }
  361. if (autoHideCursorSettingsOnSceneChanged)
  362. {
  363. if (!IsSceneInList(showCursorSettingsInScenes, newScene.name))
  364. HideCursorSettingsPanel();
  365. }
  366. }
  367. private void ApplySceneVisibilityNodes(string sceneName)
  368. {
  369. if (showInScenesNodes == null || showInScenesNodes.Count <= 0) return;
  370. bool shouldShow = IsSceneInList(showUiInScenes, sceneName);
  371. for (int i = 0; i < showInScenesNodes.Count; i++)
  372. {
  373. var go = showInScenesNodes[i];
  374. if (go != null) go.SetActive(shouldShow);
  375. }
  376. }
  377. private static bool IsSceneInList(List<string> sceneNames, string sceneName)
  378. {
  379. if (sceneNames == null || sceneNames.Count <= 0) return false;
  380. if (string.IsNullOrWhiteSpace(sceneName)) return false;
  381. for (int i = 0; i < sceneNames.Count; i++)
  382. {
  383. var n = sceneNames[i];
  384. if (string.IsNullOrWhiteSpace(n)) continue;
  385. if (n.Trim() == sceneName) return true;
  386. }
  387. return false;
  388. }
  389. public void ShowUI()
  390. {
  391. if (uiRoot != null) uiRoot.SetActive(true);
  392. }
  393. public void HideUI()
  394. {
  395. if (uiRoot != null) uiRoot.SetActive(false);
  396. }
  397. public void ToggleUI()
  398. {
  399. if (uiRoot == null) return;
  400. uiRoot.SetActive(!uiRoot.activeSelf);
  401. }
  402. /// <summary>
  403. /// 运行时切换是否常驻(仅对当前实例生效)。
  404. /// 注意:Unity 不支持“撤销 DontDestroyOnLoad”,因此关闭常驻仅更新标志位,不会把对象自动移回场景。
  405. /// </summary>
  406. public void SetPersistent(bool persistent)
  407. {
  408. dontDestroyOnLoad = persistent;
  409. if (persistent)
  410. DontDestroyOnLoad(gameObject);
  411. }
  412. private void EnsureCursorSettingsInstance()
  413. {
  414. if (LightGlueCursorSettings.Instance != null)
  415. {
  416. _cursorSettingsInstance = LightGlueCursorSettings.Instance.gameObject;
  417. return;
  418. }
  419. if (lightGlueCursorSettingsPrefab == null)
  420. return;
  421. _cursorSettingsInstance = Instantiate(lightGlueCursorSettingsPrefab);
  422. if (cursorSettingsDontDestroyOnLoad && _cursorSettingsInstance != null)
  423. DontDestroyOnLoad(_cursorSettingsInstance);
  424. Debug.Log("[LightGlueUI] LightGlueCursorSettings prefab instantiated.");
  425. }
  426. private LightGlueCursorSettings GetCursorSettings()
  427. {
  428. if (_cursorSettingsInstance != null)
  429. {
  430. var c = _cursorSettingsInstance.GetComponent<LightGlueCursorSettings>();
  431. if (c != null) return c;
  432. }
  433. return LightGlueCursorSettings.Instance;
  434. }
  435. /// <summary> 显示光标设置面板(供 Button 调用)。 </summary>
  436. public void ShowCursorSettingsPanel()
  437. {
  438. var settings = GetCursorSettings();
  439. if (settings != null) settings.ShowPanel();
  440. }
  441. /// <summary> 隐藏光标设置面板(供 Button 调用)。 </summary>
  442. public void HideCursorSettingsPanel()
  443. {
  444. var settings = GetCursorSettings();
  445. if (settings != null) settings.HidePanel();
  446. }
  447. /// <summary> 切换光标设置面板显示/隐藏(供 Button 调用)。 </summary>
  448. public void ToggleCursorSettingsPanel()
  449. {
  450. var settings = GetCursorSettings();
  451. if (settings != null) settings.TogglePanel();
  452. }
  453. /// <summary> 当前光标设置面板是否显示。 </summary>
  454. public bool IsCursorSettingsPanelVisible()
  455. {
  456. var settings = GetCursorSettings();
  457. return settings != null && settings.IsPanelVisible();
  458. }
  459. /// <summary> 加载配置的场景(可在 Inspector 设置 sceneToLoad)。 </summary>
  460. public void EntGame()
  461. {
  462. SceneManager.LoadScene(sceneToLoad);
  463. }
  464. /// <summary> 加载指定场景名。 </summary>
  465. public void EntGame(string sceneName)
  466. {
  467. SceneManager.LoadScene(sceneName);
  468. }
  469. public void SetUIType(UIType uiType)
  470. {
  471. currentUIType = uiType;
  472. ApplyUITypeVisibility();
  473. }
  474. private void ApplyUITypeVisibility()
  475. {
  476. List<GameObject> showList = currentUIType == UIType.Demo ? showInDemo : showInPlugin;
  477. List<GameObject> hideList = currentUIType == UIType.Demo ? showInPlugin : showInDemo;
  478. if (showList != null)
  479. {
  480. for (int i = 0; i < showList.Count; i++)
  481. {
  482. var go = showList[i];
  483. if (go != null) go.SetActive(true);
  484. }
  485. }
  486. if (!hideOtherTypeNodes || hideList == null) return;
  487. for (int i = 0; i < hideList.Count; i++)
  488. {
  489. var go = hideList[i];
  490. if (go != null) go.SetActive(false);
  491. }
  492. }
  493. }
  494. }