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
}
}