From d4b7848dde122985194d679c2c869a381d379953 Mon Sep 17 00:00:00 2001 From: LiRuoChen <571244399@qq.com> Date: Fri, 12 Jun 2026 09:17:30 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E9=94=9A=E7=82=B9=E7=AA=97?= =?UTF-8?q?=E5=8F=A3=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Runtime/VisualForm.meta | 8 + Runtime/VisualForm/AnchorDock.cs | 535 ++++++++++++++++++++++++ Runtime/VisualForm/AnchorDock.cs.meta | 11 + Runtime/VisualForm/AnchorWindow.cs | 147 +++++++ Runtime/VisualForm/AnchorWindow.cs.meta | 11 + Runtime/VisualForm/WindowPool.cs | 115 +++++ Runtime/VisualForm/WindowPool.cs.meta | 11 + Runtime/VisualForm/WindowState.cs | 224 ++++++++++ Runtime/VisualForm/WindowState.cs.meta | 11 + Runtime/VisualForm/WorldAnchor.cs | 71 ++++ Runtime/VisualForm/WorldAnchor.cs.meta | 11 + Runtime/VisualForm/XVFormBase.cs | 51 +++ Runtime/VisualForm/XVFormBase.cs.meta | 11 + 13 files changed, 1217 insertions(+) create mode 100644 Runtime/VisualForm.meta create mode 100644 Runtime/VisualForm/AnchorDock.cs create mode 100644 Runtime/VisualForm/AnchorDock.cs.meta create mode 100644 Runtime/VisualForm/AnchorWindow.cs create mode 100644 Runtime/VisualForm/AnchorWindow.cs.meta create mode 100644 Runtime/VisualForm/WindowPool.cs create mode 100644 Runtime/VisualForm/WindowPool.cs.meta create mode 100644 Runtime/VisualForm/WindowState.cs create mode 100644 Runtime/VisualForm/WindowState.cs.meta create mode 100644 Runtime/VisualForm/WorldAnchor.cs create mode 100644 Runtime/VisualForm/WorldAnchor.cs.meta create mode 100644 Runtime/VisualForm/XVFormBase.cs create mode 100644 Runtime/VisualForm/XVFormBase.cs.meta diff --git a/Runtime/VisualForm.meta b/Runtime/VisualForm.meta new file mode 100644 index 0000000..2d1f31e --- /dev/null +++ b/Runtime/VisualForm.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: bbfebc997f9b7fc42931055da4745128 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/VisualForm/AnchorDock.cs b/Runtime/VisualForm/AnchorDock.cs new file mode 100644 index 0000000..9ed8697 --- /dev/null +++ b/Runtime/VisualForm/AnchorDock.cs @@ -0,0 +1,535 @@ +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.UI; + +namespace XericUI.VisualForm +{ + /// + /// 锚点坞 - 挂载在Canvas上的主要布局组件管理器。 + /// 负责收集所有世界锚点,每帧将世界坐标转换到屏幕坐标,并管理中间窗口状态。 + /// + [AddComponentMenu("Xeric UI Vessel/AnchorWindow/AnchorDock")] + public class AnchorDock : MonoBehaviour + { + [Header("配置")] + [SerializeField, Tooltip("指定摄像机,不填则自动检测")] + private Camera m_OverrideCamera; + + [SerializeField, Tooltip("坐标变化阈值,移动超过此值才会标记坐标脏")] + private float m_PositionThreshold = 0.1f; + + [Space] + [SerializeField, Tooltip("预注册的锚点列表(坞可直接标记要使用的窗口锚点,双方引用有一个可不填)")] + private List m_TargetAnchors = new List(); + + [SerializeField, Tooltip("预分配的中间类对象池容量")] + private int m_StatePoolPreallocate = 16; + + #region 静态单例 + + private static AnchorDock s_Main; + + /// + /// 主要的静态单例锚点坞引用 + /// + public static AnchorDock Main => s_Main; + + /// + /// 待注册锚点列表 — 锚点在OnEnable时找不到坞,将自己暂存于此,等待坞后续处理 + /// + internal static readonly HashSet s_PendingAnchors = new HashSet(); + + #endregion + + #region 内部状态 + + private Canvas _canvas; + private Camera _renderCamera; + private RectTransform _rectTransform; + + /// + /// 锚点集合(HashSet用于O(1)去重和查找) + /// + private readonly HashSet _anchorSet = new HashSet(); + + /// + /// 锚点脏标记 - 有新增或移除时为true,延迟到下一帧处理 + /// + private bool _anchorsDirty; + + /// + /// 待新增锚点列表(脏更新期间收集) + /// + private readonly List _pendingAdds = new List(); + + /// + /// 待移除锚点列表(脏更新期间收集) + /// + private readonly List _pendingRemoves = new List(); + + /// + /// 活跃的窗口状态字典:锚点 -> 中间窗口状态类 + /// + private readonly Dictionary _activeStates = new Dictionary(); + + /// + /// 中间类对象池 + /// + private readonly Queue _statePool = new Queue(); + + /// + /// 子对象哈希集 - 用于O(1)快速检查对象是否在子项中 + /// + private readonly HashSet _childrenHashSet = new HashSet(); + + /// + /// 子对象哈希集脏标记 + /// + private bool _childrenSetDirty = true; + + /// + /// 摄像机变换脏标记 + /// + private bool _cameraDirty; + + /// + /// 上帧摄像机世界坐标 + /// + private Vector3 _lastCameraPosition; + + /// + /// 上帧摄像机世界旋转 + /// + private Quaternion _lastCameraRotation; + + #endregion + + #region 公开属性 + + /// + /// 当前使用的渲染摄像机 + /// + public Camera RenderCamera => _renderCamera; + + /// + /// Canvas组件引用 + /// + public Canvas Canvas => _canvas; + + /// + /// RectTransform引用 + /// + public RectTransform RectTransform + { + get + { + if (_rectTransform == null) + _rectTransform = GetComponent(); + return _rectTransform; + } + } + + /// + /// 坐标变化阈值 + /// + public float PositionThreshold => m_PositionThreshold; + + #endregion + + #region 生命周期 + + protected virtual void Awake() + { + // 在Awake阶段注册单例 + s_Main = this; + } + + protected virtual void OnEnable() + { + _canvas = GetComponentInParent(); + if (_canvas == null) + _canvas = GetComponent(); + + _rectTransform = GetComponent(); + + // 预分配对象池 + for (int i = 0; i < m_StatePoolPreallocate; i++) + { + var state = new WindowState(); + _statePool.Enqueue(state); + } + + // 注册预置的锚点 + foreach (var anchor in m_TargetAnchors) + { + if (anchor != null && !_anchorSet.Contains(anchor)) + { + anchor.TargetDock = this; + // 如果锚点已激活则直接添加 + if (anchor.isActiveAndEnabled) + AddAnchor(anchor); + } + } + + // 初始化摄像机引用 + RefreshCameraReference(); + _lastCameraPosition = _renderCamera ? _renderCamera.transform.position : Vector3.zero; + _lastCameraRotation = _renderCamera ? _renderCamera.transform.rotation : Quaternion.identity; + + // 标记需要重建子对象哈希集 + MarkChildrenSetDirty(); + } + + protected virtual void OnDisable() + { + // 清除静态单例 + if (s_Main == this) + s_Main = null; + + // 清理所有中间类 + foreach (var state in _activeStates.Values) + { + state.OnEnd(); + } + + foreach (var state in _statePool) + { + state.OnEnd(); + } + + _activeStates.Clear(); + _statePool.Clear(); + _anchorSet.Clear(); + _pendingAdds.Clear(); + _pendingRemoves.Clear(); + } + + protected virtual void LateUpdate() + { + // 0. 处理待注册锚点(在OnEnable阶段未找到坞的锚点) + ProcessPendingAnchors(); + + // 1. 处理锚点脏更新(新增/移除窗口) + ProcessDirtyAnchors(); + + // 2. 检查摄像机变化 + CheckCameraDirty(); + + // 3. 遍历所有中间类,检查坐标脏(比较上帧和本帧位置差异) + foreach (var kvp in _activeStates) + { + var state = kvp.Value; + if (state == null || !state.IsEnabled) continue; + state.CheckPositionDirty(m_PositionThreshold); + } + + // 4. 遍历所有中间类,根据脏标记更新坐标 + foreach (var kvp in _activeStates) + { + var state = kvp.Value; + if (state == null || !state.IsEnabled) continue; + + bool needUpdate = _cameraDirty || state.CoordinateDirty; + if (needUpdate) + { + UpdateStatePosition(state); + } + } + + // 5. 复位脏标记 + _cameraDirty = false; + } + + #endregion + + #region 锚点管理 + + /// + /// 添加世界锚点到管理列表 + /// + /// 要添加的锚点 + public void AddAnchor(WorldAnchor anchor) + { + if (anchor == null || _anchorSet.Contains(anchor)) + return; + + _anchorSet.Add(anchor); + _pendingAdds.Add(anchor); + _anchorsDirty = true; + } + + /// + /// 从管理列表移除世界锚点 + /// + /// 要移除的锚点 + public void RemoveAnchor(WorldAnchor anchor) + { + if (anchor == null || !_anchorSet.Contains(anchor)) + return; + + _anchorSet.Remove(anchor); + _pendingRemoves.Add(anchor); + _anchorsDirty = true; + } + + /// + /// 获取指定锚点对应的中间窗口状态类 + /// + /// 锚点引用 + /// 对应的WindowState,不存在则返回null + public WindowState GetWindowState(WorldAnchor anchor) + { + if (anchor == null) return null; + _activeStates.TryGetValue(anchor, out var state); + return state; + } + + /// + /// 处理静态待注册锚点列表 — 这些锚点在OnEnable时未找到坞,现在坞已就绪,正式注册它们 + /// + private void ProcessPendingAnchors() + { + if (s_PendingAnchors.Count == 0) return; + + foreach (var anchor in s_PendingAnchors) + { + if (anchor == null) continue; + anchor.TargetDock = this; + AddAnchor(anchor); + } + s_PendingAnchors.Clear(); + } + + /// + /// 处理锚点脏更新:创建/销毁WindowState + /// + private void ProcessDirtyAnchors() + { + if (!_anchorsDirty) return; + _anchorsDirty = false; + + // 处理移除 + for (int i = 0; i < _pendingRemoves.Count; i++) + { + var anchor = _pendingRemoves[i]; + if (_activeStates.TryGetValue(anchor, out var state)) + { + state.OnDisable(); + state.OnEnd(); + _statePool.Enqueue(state); + _activeStates.Remove(anchor); + } + } + _pendingRemoves.Clear(); + + // 确保子对象哈希集在创建WindowState前是最新的 + if (_childrenSetDirty) + RebuildChildrenSet(); + + // 处理新增 + for (int i = 0; i < _pendingAdds.Count; i++) + { + var anchor = _pendingAdds[i]; + if (_activeStates.ContainsKey(anchor)) continue; + + var state = GetPooledState(); + state.OnStart(anchor, this); + _activeStates[anchor] = state; + + // 如果锚点当前处于激活状态,调用Enable + if (anchor.isActiveAndEnabled) + state.OnEnable(); + } + _pendingAdds.Clear(); + } + + #endregion + + #region 坐标更新 + + /// + /// 刷新摄像机引用:检查Canvas的renderMode,选择合适的摄像机 + /// + public void RefreshCameraReference() + { + if (m_OverrideCamera != null) + { + _renderCamera = m_OverrideCamera; + return; + } + + if (_canvas == null) + { + _canvas = GetComponent(); + if (_canvas == null) + _canvas = GetComponentInParent(); + } + + if (_canvas != null && _canvas.renderMode == RenderMode.ScreenSpaceCamera) + { + _renderCamera = _canvas.worldCamera; + } + + // ScreenSpaceOverlay 或找不到摄像机时使用MainCamera + if (_renderCamera == null) + _renderCamera = Camera.main; + } + + /// + /// 检查摄像机变换是否发生变化 + /// + private void CheckCameraDirty() + { + if (_renderCamera == null) + { + RefreshCameraReference(); + return; + } + + var camTransform = _renderCamera.transform; + var currentPos = camTransform.position; + var currentRot = camTransform.rotation; + + if (currentPos != _lastCameraPosition || currentRot != _lastCameraRotation) + { + _cameraDirty = true; + _lastCameraPosition = currentPos; + _lastCameraRotation = currentRot; + } + } + + /// + /// 更新指定中间类的屏幕坐标 + /// + private void UpdateStatePosition(WindowState state) + { + if (state.Anchor == null || _renderCamera == null) return; + + var target = state.GetTargetTransform(); + if (target == null) return; + + Vector3 worldPos = state.Anchor.CachedTransform.position; + Vector2 localPoint = WorldToScreenPoint(worldPos, _renderCamera, RectTransform); + + // target是容器的子对象,设置localPosition + if (target is RectTransform rectTarget) + { + rectTarget.localPosition = new Vector3(localPoint.x, localPoint.y, rectTarget.localPosition.z); + } + else + { + target.localPosition = new Vector3(localPoint.x, localPoint.y, target.localPosition.z); + } + + state.CoordinateDirty = false; + } + + #endregion + + #region 子对象哈希集 + + /// + /// 标记子对象哈希集需要重建 + /// + public void MarkChildrenSetDirty() + { + _childrenSetDirty = true; + } + + /// + /// 重建子对象哈希集,用于O(1)快速查找 + /// + public void RebuildChildrenSet() + { + _childrenHashSet.Clear(); + + var t = transform; + for (int i = 0; i < t.childCount; i++) + { + _childrenHashSet.Add(t.GetChild(i).gameObject); + } + + _childrenSetDirty = false; + } + + /// + /// 检查指定对象是否在当前容器的子项中(O(1)哈希查找) + /// + /// 要检查的对象 + /// 是否存在于子项中 + public bool IsChildObject(GameObject obj) + { + if (obj == null) return false; + + if (_childrenSetDirty) + RebuildChildrenSet(); + + return _childrenHashSet.Contains(obj); + } + + #endregion + + #region 对象池管理 + + /// + /// 从对象池获取一个WindowState实例 + /// + private WindowState GetPooledState() + { + if (_statePool.Count > 0) + return _statePool.Dequeue(); + + return new WindowState(); + } + + #endregion + + #region 静态工具方法 + + /// + /// 将世界空间坐标转换到Canvas容器内的本地坐标 + /// + /// 世界空间坐标 + /// 使用的摄像机 + /// 目标Canvas容器的RectTransform + /// 容器内的本地坐标 + public static Vector2 WorldToScreenPoint(Vector3 worldPos, Camera camera, RectTransform container) + { + if (camera == null || container == null) + return Vector2.zero; + + Vector2 screenPoint = RectTransformUtility.WorldToScreenPoint(camera, worldPos); + + if (container.TryGetComponent(out var canvas)) + { + if (canvas.renderMode != RenderMode.ScreenSpaceOverlay && camera != null) + { + RectTransformUtility.ScreenPointToLocalPointInRectangle( + container, screenPoint, camera, out Vector2 localPoint); + return localPoint; + } + } + + RectTransformUtility.ScreenPointToLocalPointInRectangle( + container, screenPoint, null, out Vector2 overlayPoint); + return overlayPoint; + } + + /// + /// 获取Canvas使用的渲染摄像机 + /// + /// 目标Canvas + /// 渲染摄像机(如果Canvas是Overlay模式则返回MainCamera) + public static Camera GetCanvasRenderCamera(Canvas canvas) + { + if (canvas == null) return Camera.main; + + if (canvas.renderMode == RenderMode.ScreenSpaceCamera && canvas.worldCamera != null) + return canvas.worldCamera; + + return Camera.main; + } + + #endregion + } +} diff --git a/Runtime/VisualForm/AnchorDock.cs.meta b/Runtime/VisualForm/AnchorDock.cs.meta new file mode 100644 index 0000000..41dd44c --- /dev/null +++ b/Runtime/VisualForm/AnchorDock.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 21778cbeb1d1b2f478dc597145635887 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/VisualForm/AnchorWindow.cs b/Runtime/VisualForm/AnchorWindow.cs new file mode 100644 index 0000000..58f8971 --- /dev/null +++ b/Runtime/VisualForm/AnchorWindow.cs @@ -0,0 +1,147 @@ +using UnityEngine; + +namespace XericUI.VisualForm +{ + /// + /// 锚点窗口 - 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; + + /// + /// 窗口对象引用(可以是实例或预制体) + /// + public GameObject WindowObject + { + get => m_WindowObject; + set => m_WindowObject = value; + } + + /// + /// UI启用开关,设为false时窗口实例将被隐藏 + /// + public bool EnableUI + { + get => m_EnableUI; + set + { + if (m_EnableUI == value) return; + m_EnableUI = value; + ApplyUIState(); + } + } + + /// + /// 判断窗口对象是否为预制体(不在锚点坞的子项中则为预制体) + /// 此标记由WindowState在初始化时设置 + /// + [System.NonSerialized] + public bool IsPrefab; + + /// + /// 获取当前有效的窗口Transform(实例或自身) + /// + public Transform GetWindowTransform() + { + if (m_WindowObject != null) + return m_WindowObject.transform; + return transform; + } + + #region 工具方法 + + /// + /// 从所属锚点坞获取自己的中间窗口状态类 + /// + /// WindowState实例,不存在则返回null + public WindowState GetWindowState() + { + if (TargetDock == null) + return null; + return TargetDock.GetWindowState(this); + } + + /// + /// 获取窗口实例对象(预制体实例化后的对象,或子项中已有的对象) + /// + /// 窗口GameObject实例 + public GameObject GetWindowInstance() + { + var state = GetWindowState(); + return state?.WindowInstance; + } + + /// + /// 获取窗口当前的屏幕坐标(容器内本地坐标) + /// + /// 屏幕本地坐标 + public Vector2 GetScreenPosition() + { + var state = GetWindowState(); + if (state == null) return Vector2.zero; + + var instance = state.WindowInstance; + if (instance == null) return Vector2.zero; + + var rt = instance.transform as RectTransform; + if (rt != null) + return rt.localPosition; + + return instance.transform.localPosition; + } + + /// + /// 获取锚点当前的世界坐标 + /// + /// 世界坐标 + public Vector3 GetWorldPosition() + { + var state = GetWindowState(); + if (state != null) + return state.LastWorldPosition; + return CachedTransform.position; + } + + /// + /// 检查窗口实例是否处于活动状态 + /// + 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); + } + + #endregion + } +} diff --git a/Runtime/VisualForm/AnchorWindow.cs.meta b/Runtime/VisualForm/AnchorWindow.cs.meta new file mode 100644 index 0000000..ba6dc50 --- /dev/null +++ b/Runtime/VisualForm/AnchorWindow.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 65b8e58a984465e48814956733bb6156 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/VisualForm/WindowPool.cs b/Runtime/VisualForm/WindowPool.cs new file mode 100644 index 0000000..acb304b --- /dev/null +++ b/Runtime/VisualForm/WindowPool.cs @@ -0,0 +1,115 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace XericUI.VisualForm +{ + /// + /// 窗口实例对象池 - 管理预制体实例的复用,减少频繁的创建和销毁开销 + /// + public static class WindowPool + { + /// + /// 待命字典:键为预制体原型引用,值为隐藏的待复用实例队列 + /// + private static readonly Dictionary> s_StandbyDict = new Dictionary>(); + + /// + /// 从对象池获取一个实例。如果池中有则复用,否则实例化新的 + /// + /// 预制体原型 + /// 父级Transform + /// 获取到的实例 + public static GameObject Get(GameObject prefab, Transform parent) + { + if (prefab == null) + { + Debug.LogError("[WindowPool] 预制体为空,无法获取实例"); + return null; + } + + if (s_StandbyDict.TryGetValue(prefab, out var queue) && queue.Count > 0) + { + var instance = queue.Dequeue(); + if (instance != null) + { + instance.transform.SetParent(parent, false); + instance.SetActive(true); + return instance; + } + } + + // 池中无可用实例,创建新的 + var newInstance = Object.Instantiate(prefab, parent, false); + newInstance.name = prefab.name; + return newInstance; + } + + /// + /// 将实例回收到对象池中 + /// + /// 预制体原型 + /// 要回收的实例 + public static void Release(GameObject prefab, GameObject instance) + { + if (prefab == null || instance == null) + return; + + instance.SetActive(false); + + if (!s_StandbyDict.TryGetValue(prefab, out var queue)) + { + queue = new Queue(); + s_StandbyDict[prefab] = queue; + } + + queue.Enqueue(instance); + } + + /// + /// 清空指定预制体的对象池 + /// + /// 预制体原型 + public static void Clear(GameObject prefab) + { + if (prefab == null) return; + + if (s_StandbyDict.TryGetValue(prefab, out var queue)) + { + while (queue.Count > 0) + { + var instance = queue.Dequeue(); + if (instance != null) + Object.Destroy(instance); + } + s_StandbyDict.Remove(prefab); + } + } + + /// + /// 清空所有对象池 + /// + public static void ClearAll() + { + foreach (var kvp in s_StandbyDict) + { + while (kvp.Value.Count > 0) + { + var instance = kvp.Value.Dequeue(); + if (instance != null) + Object.Destroy(instance); + } + } + s_StandbyDict.Clear(); + } + + /// + /// 检查指定预制体在池中是否有可用实例 + /// + /// 预制体原型 + /// 是否有可用实例 + public static bool HasAvailable(GameObject prefab) + { + return prefab != null && s_StandbyDict.TryGetValue(prefab, out var queue) && queue.Count > 0; + } + } +} diff --git a/Runtime/VisualForm/WindowPool.cs.meta b/Runtime/VisualForm/WindowPool.cs.meta new file mode 100644 index 0000000..2cb875a --- /dev/null +++ b/Runtime/VisualForm/WindowPool.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ecf3ad30a3eb01f40a4508ff21ba31dd +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 new file mode 100644 index 0000000..88ab84c --- /dev/null +++ b/Runtime/VisualForm/WindowState.cs @@ -0,0 +1,224 @@ +using UnityEngine; + +namespace XericUI.VisualForm +{ + /// + /// 中间窗口状态类 - 管理锚点窗口和锚点坞之间的中间产物。 + /// 负责窗口实例的生命周期管理、坐标脏标记和屏幕定位。 + /// 使用对象池管理,避免频繁创建销毁。 + /// + /// 生命周期: + /// OnStart - 从对象池取出时调用(初始化/复用) + /// OnEnable - 锚点激活时调用(脏更新触发) + /// OnDisable- 锚点隐藏时调用(脏更新触发) + /// OnEnd - 回收到对象池时调用(清理/回收) + /// + public class WindowState + { + /// + /// 关联的世界锚点 + /// + public WorldAnchor Anchor { get; private set; } + + /// + /// 关联的锚点坞 + /// + public AnchorDock Dock { get; private set; } + + /// + /// 原始窗口对象(来自AnchorWindow的引用) + /// + private GameObject _windowObject; + + /// + /// 实例化的窗口对象(如果是预制体则为此实例,否则为_windowObject本身) + /// + private GameObject _instance; + + /// + /// 是否为预制体 + /// + private bool _isPrefab; + + /// + /// 是否为预制体(公开属性) + /// + public bool IsPrefab => _isPrefab; + + /// + /// 窗口实例对象(预制体实例化后的对象,或子项中已有的对象) + /// + public GameObject WindowInstance => _instance; + + /// + /// 上帧锚点世界坐标 + /// + public Vector3 LastWorldPosition => _lastPosition; + + /// + /// 当前是否处于激活状态 + /// + public bool IsEnabled { get; private set; } + + /// + /// 坐标脏标记 - 当锚点世界坐标发生变化时标记 + /// + public bool CoordinateDirty { get; set; } + + /// + /// 上帧世界坐标(用于比较变化量) + /// + private Vector3 _lastPosition; + + #region 生命周期 + + /// + /// 从对象池取出时的初始化(Start事件) + /// + /// 关联的锚点 + /// 关联的锚点坞 + public void OnStart(WorldAnchor anchor, AnchorDock dock) + { + ResetState(); + + Anchor = anchor; + Dock = dock; + + // 解析窗口实例(原型对象或预制体实例化) + ResolveWindowInstance(); + + // 初始化位置记录 + if (anchor != null) + _lastPosition = anchor.CachedTransform.position; + } + + /// + /// 解析窗口实例:自动判断窗口对象是原型(已在子项中)还是预制体,并完成实例获取或创建。 + /// 如果锚点是AnchorWindow且携带窗口对象,则根据是否在坞子项中决定处理方向: + /// - 在子项中 → 直接使用该对象作为实例(原型模式) + /// - 不在子项中 → 从WindowPool获取或实例化预制体 + /// 如果锚点不是AnchorWindow或无窗口对象,则不创建实例。 + /// + public void ResolveWindowInstance() + { + if (Dock == null || Anchor == null) return; + + var anchorWindow = Anchor as AnchorWindow; + if (anchorWindow == null || anchorWindow.WindowObject == null) + return; + + _windowObject = anchorWindow.WindowObject; + + // 通过锚点坞的哈希检查窗口对象是否在子项中 + _isPrefab = !Dock.IsChildObject(_windowObject); + + if (_isPrefab) + { + // 预制体:从对象池获取或创建实例,挂到坞下 + _instance = WindowPool.Get(_windowObject, Dock.transform); + } + else + { + // 原型:已在子项中的实例对象,直接引用 + _instance = _windowObject; + } + + // 同步标记到AnchorWindow + anchorWindow.IsPrefab = _isPrefab; + } + + /// + /// 锚点激活时的处理(Enable事件,由脏更新触发) + /// + public void OnEnable() + { + IsEnabled = true; + + // 如果实例尚不存在(如纯WorldAnchor没有窗口对象),尝试解析 + if (_instance == null) + ResolveWindowInstance(); + + if (_instance != null) + _instance.SetActive(true); + + // 激活时强制标记坐标脏,确保下一帧更新位置 + CoordinateDirty = true; + } + + /// + /// 锚点隐藏时的处理(Disable事件,由脏更新触发) + /// + public void OnDisable() + { + IsEnabled = false; + + if (_instance != null) + _instance.SetActive(false); + } + + /// + /// 回收到对象池时的清理(End事件) + /// + public void OnEnd() + { + // 回收预制体实例到对象池 + if (_isPrefab && _instance != null) + { + WindowPool.Release(_windowObject, _instance); + } + + ResetState(); + } + + #endregion + + #region 状态管理 + + /// + /// 复位所有状态(用于对象池复用) + /// + private void ResetState() + { + Anchor = null; + Dock = null; + _windowObject = null; + _instance = null; + _isPrefab = false; + IsEnabled = false; + CoordinateDirty = false; + _lastPosition = Vector3.zero; + } + + /// + /// 检查锚点坐标是否发生变化,超过阈值则标记脏 + /// 由锚点坞在每帧调用 + /// + /// 变化阈值 + public void CheckPositionDirty(float threshold) + { + if (Anchor == null) return; + + var currentPos = Anchor.CachedTransform.position; + if (Vector3.Distance(_lastPosition, currentPos) > threshold) + { + CoordinateDirty = true; + _lastPosition = currentPos; + } + } + + /// + /// 获取当前需要定位的Transform目标(实例化的窗口对象或锚点自身) + /// + /// 目标Transform + public Transform GetTargetTransform() + { + if (_instance != null) + return _instance.transform; + if (Anchor != null) + return Anchor.CachedTransform; + return null; + } + + #endregion + } +} diff --git a/Runtime/VisualForm/WindowState.cs.meta b/Runtime/VisualForm/WindowState.cs.meta new file mode 100644 index 0000000..4d88156 --- /dev/null +++ b/Runtime/VisualForm/WindowState.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ddaed09299c4aa44ba0d760add4f3597 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/VisualForm/WorldAnchor.cs b/Runtime/VisualForm/WorldAnchor.cs new file mode 100644 index 0000000..a178eb4 --- /dev/null +++ b/Runtime/VisualForm/WorldAnchor.cs @@ -0,0 +1,71 @@ +using UnityEngine; + +namespace XericUI.VisualForm +{ + /// + /// 世界对象锚点 - 挂载在场景对象上,将世界空间位置传递给锚点坞进行屏幕空间定位 + /// + [AddComponentMenu("Xeric UI Vessel/AnchorWindow/WorldAnchor")] + public class WorldAnchor : XVFormBase + { + [SerializeField, Tooltip("目标锚点坞,不填则自动在父级中查找")] + private AnchorDock m_TargetDock; + + /// + /// 目标锚点坞引用 + /// + public AnchorDock TargetDock + { + get => m_TargetDock; + set => m_TargetDock = value; + } + + /// + /// 缓存Transform组件 + /// + [System.NonSerialized] + private Transform _cachedTransform; + + /// + /// 缓存Transform,避免重复GetComponent + /// + public Transform CachedTransform + { + get + { + if (_cachedTransform == null) + _cachedTransform = transform; + return _cachedTransform; + } + } + + protected void OnEnable() + { + // 尝试自动查找锚点坞 + if (m_TargetDock == null) + m_TargetDock = GetComponentInParent(); + + // 回退到静态单例 + if (m_TargetDock == null) + m_TargetDock = AnchorDock.Main; + + if (m_TargetDock != null) + { + m_TargetDock.AddAnchor(this); + } + else + { + // 暂存到全局待注册列表,等待坞就绪后处理 + AnchorDock.s_PendingAnchors.Add(this); + } + } + + protected void OnDisable() + { + if (m_TargetDock != null) + m_TargetDock.RemoveAnchor(this); + else + AnchorDock.s_PendingAnchors.Remove(this); + } + } +} diff --git a/Runtime/VisualForm/WorldAnchor.cs.meta b/Runtime/VisualForm/WorldAnchor.cs.meta new file mode 100644 index 0000000..df34473 --- /dev/null +++ b/Runtime/VisualForm/WorldAnchor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fdf2bb8183505fd4dbcf6095985e80af +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/VisualForm/XVFormBase.cs b/Runtime/VisualForm/XVFormBase.cs new file mode 100644 index 0000000..935e093 --- /dev/null +++ b/Runtime/VisualForm/XVFormBase.cs @@ -0,0 +1,51 @@ +using UnityEngine; + +namespace XericUI.VisualForm +{ + /// + /// 虚拟窗体基类,提供基础的窗体标记功能 + /// + [AddComponentMenu("Xeric UI Vessel/AnchorWindow/XVFormBase")] + public class XVFormBase : MonoBehaviour + { + [SerializeField] + private string m_FormID; + + /// + /// 窗体唯一标识 + /// + public string FormID + { + get => m_FormID; + set => m_FormID = value; + } + + /// + /// 窗体是否处于活动状态 + /// + public bool IsFormActive => gameObject.activeInHierarchy && enabled; + + protected virtual void Awake() + { + TryGenerateFormID(); + } + + /// + /// 尝试自动生成FormID(如果当前为空) + /// + private void TryGenerateFormID() + { + if (string.IsNullOrEmpty(m_FormID)) + { + m_FormID = ((uint)GetInstanceID()).ToString(); + } + } + +#if UNITY_EDITOR + protected virtual void OnValidate() + { + TryGenerateFormID(); + } +#endif + } +} diff --git a/Runtime/VisualForm/XVFormBase.cs.meta b/Runtime/VisualForm/XVFormBase.cs.meta new file mode 100644 index 0000000..3ddb242 --- /dev/null +++ b/Runtime/VisualForm/XVFormBase.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ea57a5ab7132aa34f862d183e6910015 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: