diff --git a/Runtime/VisualForm/AnchorDock.cs b/Runtime/VisualForm/AnchorDock.cs index 3883901..c73edb7 100644 --- a/Runtime/VisualForm/AnchorDock.cs +++ b/Runtime/VisualForm/AnchorDock.cs @@ -436,26 +436,33 @@ namespace XericUI.VisualForm } /// - /// 更新指定中间类的屏幕坐标 + /// 更新指定中间类中所有窗口条目的屏幕坐标(基础位置 + 条目偏移量) /// private void UpdateStatePosition(WindowState state) { if (state.Anchor == null || _worldCamera == null || _canvas == null) return; - - var target = state.GetTargetTransform(); - if (target == null) return; + if (state.Entries.Count == 0) return; Vector3 worldPos = state.Anchor.CachedTransform.position; - if (TryWorldToScreenPoint(worldPos, _worldCamera, _uiCamera, _canvas, RectTransform, out Vector2 localPoint)) - { - if (target is RectTransform rectTarget) - rectTarget.anchoredPosition = localPoint; - else - target.localPosition = new Vector3(localPoint.x, localPoint.y, target.localPosition.z); + if (!TryWorldToScreenPoint(worldPos, _worldCamera, _uiCamera, _canvas, RectTransform, out Vector2 basePoint)) + return; - state.CoordinateDirty = false; + // 为每个窗口条目设置位置:基础坐标 + 条目自身偏移量 + foreach (var entry in state.Entries) + { + if (entry.Instance == null) continue; + + Vector2 finalPoint = basePoint + entry.ScreenOffset; + var target = entry.Instance.transform; + + if (target is RectTransform rectTarget) + rectTarget.anchoredPosition = finalPoint; + else + target.localPosition = new Vector3(finalPoint.x, finalPoint.y, target.localPosition.z); } + + state.CoordinateDirty = false; } #endregion diff --git a/Runtime/VisualForm/AnchorWindow.cs b/Runtime/VisualForm/AnchorWindow.cs index 58f8971..5d19734 100644 --- a/Runtime/VisualForm/AnchorWindow.cs +++ b/Runtime/VisualForm/AnchorWindow.cs @@ -1,59 +1,50 @@ +using System.Collections.Generic; using UnityEngine; namespace XericUI.VisualForm { /// - /// 锚点窗口 - WorldAnchor的子类,持有UI窗口对象的引用。 + /// 锚点窗口 - WorldAnchor的子类,持有一组UI窗口条目的引用。 + /// 每个条目包含窗口对象、屏幕偏移量和激活状态,支持一个锚点同时生成多个定位窗口。 /// 窗口对象可以是实际的场景对象,也可以是预制体。 - /// 如果窗口锚点坞在子项中找不到对应对象,则视为预制体,需要通过实例化方法创建。 /// [AddComponentMenu("Xeric UI Vessel/AnchorWindow/AnchorWindow")] public class AnchorWindow : WorldAnchor { - [Header("窗口配置")] - [SerializeField, Tooltip("UI窗口对象(可以是子项中的实例或预制体资源)")] - private GameObject m_WindowObject; - - [SerializeField, Tooltip("启用UI显示开关")] - private bool m_EnableUI = true; + [Header("窗口条目")] + [SerializeField, Tooltip("窗口对象集合,每个条目可独立配置偏移和启用状态")] + private List m_WindowEntries = new List(); /// - /// 窗口对象引用(可以是实例或预制体) + /// 窗口条目列表 /// - public GameObject WindowObject - { - get => m_WindowObject; - set => m_WindowObject = value; - } + public List WindowEntries => m_WindowEntries; /// - /// UI启用开关,设为false时窗口实例将被隐藏 + /// 当前生效的窗口条目数(过滤掉WindowObject为null的条目) /// - public bool EnableUI + public int ValidEntryCount { - get => m_EnableUI; - set + get { - if (m_EnableUI == value) return; - m_EnableUI = value; - ApplyUIState(); + int count = 0; + foreach (var entry in m_WindowEntries) + if (entry != null && entry.WindowObject != null) + count++; + return count; } } /// - /// 判断窗口对象是否为预制体(不在锚点坞的子项中则为预制体) - /// 此标记由WindowState在初始化时设置 - /// - [System.NonSerialized] - public bool IsPrefab; - - /// - /// 获取当前有效的窗口Transform(实例或自身) + /// 获取第一个有效窗口对象的Transform(兼容旧API) /// public Transform GetWindowTransform() { - if (m_WindowObject != null) - return m_WindowObject.transform; + foreach (var entry in m_WindowEntries) + { + if (entry != null && entry.WindowObject != null) + return entry.WindowObject.transform; + } return transform; } @@ -62,52 +53,58 @@ namespace XericUI.VisualForm /// /// 从所属锚点坞获取自己的中间窗口状态类 /// - /// WindowState实例,不存在则返回null public WindowState GetWindowState() { - if (TargetDock == null) - return null; + if (TargetDock == null) return null; return TargetDock.GetWindowState(this); } /// - /// 获取窗口实例对象(预制体实例化后的对象,或子项中已有的对象) + /// 获取第一个有效窗口的实例对象 /// - /// 窗口GameObject实例 public GameObject GetWindowInstance() { var state = GetWindowState(); - return state?.WindowInstance; + if (state == null || state.Entries == null || state.Entries.Count == 0) + return null; + return state.Entries[0].Instance; } /// - /// 获取窗口当前的屏幕坐标(容器内本地坐标) + /// 获取所有窗口实例对象 + /// + public List GetAllWindowInstances() + { + var result = new List(); + var state = GetWindowState(); + if (state == null || state.Entries == null) return result; + + foreach (var e in state.Entries) + if (e.Instance != null) + result.Add(e.Instance); + return result; + } + + /// + /// 获取第一个窗口当前的屏幕坐标(容器内本地坐标) /// - /// 屏幕本地坐标 public Vector2 GetScreenPosition() { - var state = GetWindowState(); - if (state == null) return Vector2.zero; - - var instance = state.WindowInstance; + var instance = GetWindowInstance(); if (instance == null) return Vector2.zero; var rt = instance.transform as RectTransform; - if (rt != null) - return rt.localPosition; - + if (rt != null) return rt.localPosition; return instance.transform.localPosition; } /// /// 获取锚点当前的世界坐标 /// - /// 世界坐标 public Vector3 GetWorldPosition() { var state = GetWindowState(); - if (state != null) - return state.LastWorldPosition; + if (state != null) return state.LastWorldPosition; return CachedTransform.position; } @@ -117,29 +114,7 @@ namespace XericUI.VisualForm public bool IsWindowActive() { var state = GetWindowState(); - if (state == null) return false; - return state.IsEnabled && m_EnableUI; - } - - #endregion - - #region 内部方法 - - /// - /// 将UI开关状态应用到窗口实例 - /// - private void ApplyUIState() - { - var state = GetWindowState(); - if (state == null) return; - - var instance = state.WindowInstance; - if (instance == null) return; - - if (m_EnableUI && state.IsEnabled) - instance.SetActive(true); - else - instance.SetActive(false); + return state != null && state.IsEnabled; } #endregion diff --git a/Runtime/VisualForm/WindowEntry.cs b/Runtime/VisualForm/WindowEntry.cs new file mode 100644 index 0000000..24ca41b --- /dev/null +++ b/Runtime/VisualForm/WindowEntry.cs @@ -0,0 +1,63 @@ +using UnityEngine; + +namespace XericUI.VisualForm +{ + /// + /// 窗口条目数据 — 锚点窗口上每个窗口对象的配置集合。 + /// 包含窗口对象引用、屏幕二维偏移量和激活状态。 + /// 支持一个锚点同时生成多个定位窗口。 + /// + [System.Serializable] + public class WindowEntry + { + [SerializeField, Tooltip("窗口对象(可以是子项中的实例或预制体资源)")] + private GameObject m_WindowObject; + + [SerializeField, Tooltip("屏幕二维偏移量")] + private Vector2 m_ScreenOffset; + + [SerializeField, Tooltip("是否启用此窗口")] + private bool m_Enabled = true; + + /// + /// 窗口对象引用(实例或预制体) + /// + public GameObject WindowObject + { + get => m_WindowObject; + set => m_WindowObject = value; + } + + /// + /// 屏幕二维偏移量 + /// + public Vector2 ScreenOffset + { + get => m_ScreenOffset; + set => m_ScreenOffset = value; + } + + /// + /// 是否启用此窗口 + /// + public bool Enabled + { + get => m_Enabled; + set => m_Enabled = value; + } + + // --- 运行时状态(非序列化) --- + + /// + /// 实际生成的窗口实例(原型或预制体实例) + /// + [System.NonSerialized] + public GameObject Instance; + + /// + /// 此窗口对象是否被判定为预制体(不在坞子项中) + /// + [System.NonSerialized] + public bool IsPrefab; + } +} diff --git a/Runtime/VisualForm/WindowEntry.cs.meta b/Runtime/VisualForm/WindowEntry.cs.meta new file mode 100644 index 0000000..2c3a613 --- /dev/null +++ b/Runtime/VisualForm/WindowEntry.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 95453fd9f28009d4f8bdae1fe45919a2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/VisualForm/WindowState.cs b/Runtime/VisualForm/WindowState.cs index 88ab84c..32e4395 100644 --- a/Runtime/VisualForm/WindowState.cs +++ b/Runtime/VisualForm/WindowState.cs @@ -1,17 +1,18 @@ +using System.Collections.Generic; using UnityEngine; namespace XericUI.VisualForm { /// /// 中间窗口状态类 - 管理锚点窗口和锚点坞之间的中间产物。 - /// 负责窗口实例的生命周期管理、坐标脏标记和屏幕定位。 + /// 负责所有窗口条目的实例生命周期管理、坐标脏标记和屏幕定位。 /// 使用对象池管理,避免频繁创建销毁。 /// /// 生命周期: - /// OnStart - 从对象池取出时调用(初始化/复用) - /// OnEnable - 锚点激活时调用(脏更新触发) - /// OnDisable- 锚点隐藏时调用(脏更新触发) - /// OnEnd - 回收到对象池时调用(清理/回收) + /// OnStart - 从对象池取出时调用(初始化/复用,解析所有窗口条目) + /// OnEnable - 锚点激活时调用(脏更新触发,激活所有启用条目) + /// OnDisable- 锚点隐藏时调用(脏更新触发,隐藏所有条目) + /// OnEnd - 回收到对象池时调用(清理/回收所有条目实例) /// public class WindowState { @@ -26,34 +27,10 @@ namespace XericUI.VisualForm public AnchorDock Dock { get; private set; } /// - /// 原始窗口对象(来自AnchorWindow的引用) + /// 窗口条目列表(来自AnchorWindow的WindowEntries的运行时副本) + /// 每个条目维护自己的实例、偏移量和激活状态 /// - private GameObject _windowObject; - - /// - /// 实例化的窗口对象(如果是预制体则为此实例,否则为_windowObject本身) - /// - private GameObject _instance; - - /// - /// 是否为预制体 - /// - private bool _isPrefab; - - /// - /// 是否为预制体(公开属性) - /// - public bool IsPrefab => _isPrefab; - - /// - /// 窗口实例对象(预制体实例化后的对象,或子项中已有的对象) - /// - public GameObject WindowInstance => _instance; - - /// - /// 上帧锚点世界坐标 - /// - public Vector3 LastWorldPosition => _lastPosition; + public List Entries { get; private set; } = new List(); /// /// 当前是否处于激活状态 @@ -65,6 +42,11 @@ namespace XericUI.VisualForm /// public bool CoordinateDirty { get; set; } + /// + /// 上帧锚点世界坐标 + /// + public Vector3 LastWorldPosition => _lastPosition; + /// /// 上帧世界坐标(用于比较变化量) /// @@ -75,8 +57,6 @@ namespace XericUI.VisualForm /// /// 从对象池取出时的初始化(Start事件) /// - /// 关联的锚点 - /// 关联的锚点坞 public void OnStart(WorldAnchor anchor, AnchorDock dock) { ResetState(); @@ -84,8 +64,8 @@ namespace XericUI.VisualForm Anchor = anchor; Dock = dock; - // 解析窗口实例(原型对象或预制体实例化) - ResolveWindowInstance(); + // 解析所有窗口条目(原型对象或预制体实例化) + ResolveWindowEntries(); // 初始化位置记录 if (anchor != null) @@ -93,38 +73,42 @@ namespace XericUI.VisualForm } /// - /// 解析窗口实例:自动判断窗口对象是原型(已在子项中)还是预制体,并完成实例获取或创建。 - /// 如果锚点是AnchorWindow且携带窗口对象,则根据是否在坞子项中决定处理方向: - /// - 在子项中 → 直接使用该对象作为实例(原型模式) - /// - 不在子项中 → 从WindowPool获取或实例化预制体 - /// 如果锚点不是AnchorWindow或无窗口对象,则不创建实例。 + /// 解析所有窗口条目:遍历AnchorWindow的WindowEntries, + /// 为每个条目判断窗口对象是原型还是预制体,并完成实例获取或创建。 /// - public void ResolveWindowInstance() + public void ResolveWindowEntries() { if (Dock == null || Anchor == null) return; var anchorWindow = Anchor as AnchorWindow; - if (anchorWindow == null || anchorWindow.WindowObject == null) + if (anchorWindow == null || anchorWindow.WindowEntries.Count == 0) return; - _windowObject = anchorWindow.WindowObject; + Entries.Clear(); - // 通过锚点坞的哈希检查窗口对象是否在子项中 - _isPrefab = !Dock.IsChildObject(_windowObject); - - if (_isPrefab) + foreach (var srcEntry in anchorWindow.WindowEntries) { - // 预制体:从对象池获取或创建实例,挂到坞下 - _instance = WindowPool.Get(_windowObject, Dock.transform); - } - else - { - // 原型:已在子项中的实例对象,直接引用 - _instance = _windowObject; - } + if (srcEntry == null || srcEntry.WindowObject == null) + continue; - // 同步标记到AnchorWindow - anchorWindow.IsPrefab = _isPrefab; + var entry = srcEntry; // 直接复用WindowEntry引用,运行时状态写回 + + // 通过锚点坞的哈希检查窗口对象是否在子项中 + entry.IsPrefab = !Dock.IsChildObject(entry.WindowObject); + + if (entry.IsPrefab) + { + // 预制体:从对象池获取或创建实例,挂到坞下 + entry.Instance = WindowPool.Get(entry.WindowObject, Dock.transform); + } + else + { + // 原型:已在子项中的实例对象,直接引用 + entry.Instance = entry.WindowObject; + } + + Entries.Add(entry); + } } /// @@ -134,12 +118,16 @@ namespace XericUI.VisualForm { IsEnabled = true; - // 如果实例尚不存在(如纯WorldAnchor没有窗口对象),尝试解析 - if (_instance == null) - ResolveWindowInstance(); + // 确保窗口条目已解析 + if (Entries.Count == 0) + ResolveWindowEntries(); - if (_instance != null) - _instance.SetActive(true); + // 激活所有启用的条目实例 + foreach (var entry in Entries) + { + if (entry.Instance != null && entry.Enabled) + entry.Instance.SetActive(true); + } // 激活时强制标记坐标脏,确保下一帧更新位置 CoordinateDirty = true; @@ -152,8 +140,11 @@ namespace XericUI.VisualForm { IsEnabled = false; - if (_instance != null) - _instance.SetActive(false); + foreach (var entry in Entries) + { + if (entry.Instance != null) + entry.Instance.SetActive(false); + } } /// @@ -161,12 +152,17 @@ namespace XericUI.VisualForm /// public void OnEnd() { - // 回收预制体实例到对象池 - if (_isPrefab && _instance != null) + // 回收所有预制体实例到对象池 + foreach (var entry in Entries) { - WindowPool.Release(_windowObject, _instance); + if (entry.IsPrefab && entry.Instance != null) + WindowPool.Release(entry.WindowObject, entry.Instance); + + entry.Instance = null; + entry.IsPrefab = false; } + Entries.Clear(); ResetState(); } @@ -181,9 +177,6 @@ namespace XericUI.VisualForm { Anchor = null; Dock = null; - _windowObject = null; - _instance = null; - _isPrefab = false; IsEnabled = false; CoordinateDirty = false; _lastPosition = Vector3.zero; @@ -193,7 +186,6 @@ namespace XericUI.VisualForm /// 检查锚点坐标是否发生变化,超过阈值则标记脏 /// 由锚点坞在每帧调用 /// - /// 变化阈值 public void CheckPositionDirty(float threshold) { if (Anchor == null) return; @@ -207,18 +199,30 @@ namespace XericUI.VisualForm } /// - /// 获取当前需要定位的Transform目标(实例化的窗口对象或锚点自身) + /// 获取第一个有效窗口条目的Transform(兼容旧API) /// - /// 目标Transform public Transform GetTargetTransform() { - if (_instance != null) - return _instance.transform; + foreach (var entry in Entries) + { + if (entry.Instance != null) + return entry.Instance.transform; + } if (Anchor != null) return Anchor.CachedTransform; return null; } + /// + /// 获取所有需要定位的Transform + /// + public IEnumerable GetAllTargetTransforms() + { + foreach (var entry in Entries) + if (entry.Instance != null) + yield return entry.Instance.transform; + } + #endregion } }