新增锚点窗口组件
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bbfebc997f9b7fc42931055da4745128
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,535 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace XericUI.VisualForm
|
||||
{
|
||||
/// <summary>
|
||||
/// 锚点坞 - 挂载在Canvas上的主要布局组件管理器。
|
||||
/// 负责收集所有世界锚点,每帧将世界坐标转换到屏幕坐标,并管理中间窗口状态。
|
||||
/// </summary>
|
||||
[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<WorldAnchor> m_TargetAnchors = new List<WorldAnchor>();
|
||||
|
||||
[SerializeField, Tooltip("预分配的中间类对象池容量")]
|
||||
private int m_StatePoolPreallocate = 16;
|
||||
|
||||
#region 静态单例
|
||||
|
||||
private static AnchorDock s_Main;
|
||||
|
||||
/// <summary>
|
||||
/// 主要的静态单例锚点坞引用
|
||||
/// </summary>
|
||||
public static AnchorDock Main => s_Main;
|
||||
|
||||
/// <summary>
|
||||
/// 待注册锚点列表 — 锚点在OnEnable时找不到坞,将自己暂存于此,等待坞后续处理
|
||||
/// </summary>
|
||||
internal static readonly HashSet<WorldAnchor> s_PendingAnchors = new HashSet<WorldAnchor>();
|
||||
|
||||
#endregion
|
||||
|
||||
#region 内部状态
|
||||
|
||||
private Canvas _canvas;
|
||||
private Camera _renderCamera;
|
||||
private RectTransform _rectTransform;
|
||||
|
||||
/// <summary>
|
||||
/// 锚点集合(HashSet用于O(1)去重和查找)
|
||||
/// </summary>
|
||||
private readonly HashSet<WorldAnchor> _anchorSet = new HashSet<WorldAnchor>();
|
||||
|
||||
/// <summary>
|
||||
/// 锚点脏标记 - 有新增或移除时为true,延迟到下一帧处理
|
||||
/// </summary>
|
||||
private bool _anchorsDirty;
|
||||
|
||||
/// <summary>
|
||||
/// 待新增锚点列表(脏更新期间收集)
|
||||
/// </summary>
|
||||
private readonly List<WorldAnchor> _pendingAdds = new List<WorldAnchor>();
|
||||
|
||||
/// <summary>
|
||||
/// 待移除锚点列表(脏更新期间收集)
|
||||
/// </summary>
|
||||
private readonly List<WorldAnchor> _pendingRemoves = new List<WorldAnchor>();
|
||||
|
||||
/// <summary>
|
||||
/// 活跃的窗口状态字典:锚点 -> 中间窗口状态类
|
||||
/// </summary>
|
||||
private readonly Dictionary<WorldAnchor, WindowState> _activeStates = new Dictionary<WorldAnchor, WindowState>();
|
||||
|
||||
/// <summary>
|
||||
/// 中间类对象池
|
||||
/// </summary>
|
||||
private readonly Queue<WindowState> _statePool = new Queue<WindowState>();
|
||||
|
||||
/// <summary>
|
||||
/// 子对象哈希集 - 用于O(1)快速检查对象是否在子项中
|
||||
/// </summary>
|
||||
private readonly HashSet<GameObject> _childrenHashSet = new HashSet<GameObject>();
|
||||
|
||||
/// <summary>
|
||||
/// 子对象哈希集脏标记
|
||||
/// </summary>
|
||||
private bool _childrenSetDirty = true;
|
||||
|
||||
/// <summary>
|
||||
/// 摄像机变换脏标记
|
||||
/// </summary>
|
||||
private bool _cameraDirty;
|
||||
|
||||
/// <summary>
|
||||
/// 上帧摄像机世界坐标
|
||||
/// </summary>
|
||||
private Vector3 _lastCameraPosition;
|
||||
|
||||
/// <summary>
|
||||
/// 上帧摄像机世界旋转
|
||||
/// </summary>
|
||||
private Quaternion _lastCameraRotation;
|
||||
|
||||
#endregion
|
||||
|
||||
#region 公开属性
|
||||
|
||||
/// <summary>
|
||||
/// 当前使用的渲染摄像机
|
||||
/// </summary>
|
||||
public Camera RenderCamera => _renderCamera;
|
||||
|
||||
/// <summary>
|
||||
/// Canvas组件引用
|
||||
/// </summary>
|
||||
public Canvas Canvas => _canvas;
|
||||
|
||||
/// <summary>
|
||||
/// RectTransform引用
|
||||
/// </summary>
|
||||
public RectTransform RectTransform
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_rectTransform == null)
|
||||
_rectTransform = GetComponent<RectTransform>();
|
||||
return _rectTransform;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 坐标变化阈值
|
||||
/// </summary>
|
||||
public float PositionThreshold => m_PositionThreshold;
|
||||
|
||||
#endregion
|
||||
|
||||
#region 生命周期
|
||||
|
||||
protected virtual void Awake()
|
||||
{
|
||||
// 在Awake阶段注册单例
|
||||
s_Main = this;
|
||||
}
|
||||
|
||||
protected virtual void OnEnable()
|
||||
{
|
||||
_canvas = GetComponentInParent<Canvas>();
|
||||
if (_canvas == null)
|
||||
_canvas = GetComponent<Canvas>();
|
||||
|
||||
_rectTransform = GetComponent<RectTransform>();
|
||||
|
||||
// 预分配对象池
|
||||
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 锚点管理
|
||||
|
||||
/// <summary>
|
||||
/// 添加世界锚点到管理列表
|
||||
/// </summary>
|
||||
/// <param name="anchor">要添加的锚点</param>
|
||||
public void AddAnchor(WorldAnchor anchor)
|
||||
{
|
||||
if (anchor == null || _anchorSet.Contains(anchor))
|
||||
return;
|
||||
|
||||
_anchorSet.Add(anchor);
|
||||
_pendingAdds.Add(anchor);
|
||||
_anchorsDirty = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从管理列表移除世界锚点
|
||||
/// </summary>
|
||||
/// <param name="anchor">要移除的锚点</param>
|
||||
public void RemoveAnchor(WorldAnchor anchor)
|
||||
{
|
||||
if (anchor == null || !_anchorSet.Contains(anchor))
|
||||
return;
|
||||
|
||||
_anchorSet.Remove(anchor);
|
||||
_pendingRemoves.Add(anchor);
|
||||
_anchorsDirty = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定锚点对应的中间窗口状态类
|
||||
/// </summary>
|
||||
/// <param name="anchor">锚点引用</param>
|
||||
/// <returns>对应的WindowState,不存在则返回null</returns>
|
||||
public WindowState GetWindowState(WorldAnchor anchor)
|
||||
{
|
||||
if (anchor == null) return null;
|
||||
_activeStates.TryGetValue(anchor, out var state);
|
||||
return state;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理静态待注册锚点列表 — 这些锚点在OnEnable时未找到坞,现在坞已就绪,正式注册它们
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理锚点脏更新:创建/销毁WindowState
|
||||
/// </summary>
|
||||
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 坐标更新
|
||||
|
||||
/// <summary>
|
||||
/// 刷新摄像机引用:检查Canvas的renderMode,选择合适的摄像机
|
||||
/// </summary>
|
||||
public void RefreshCameraReference()
|
||||
{
|
||||
if (m_OverrideCamera != null)
|
||||
{
|
||||
_renderCamera = m_OverrideCamera;
|
||||
return;
|
||||
}
|
||||
|
||||
if (_canvas == null)
|
||||
{
|
||||
_canvas = GetComponent<Canvas>();
|
||||
if (_canvas == null)
|
||||
_canvas = GetComponentInParent<Canvas>();
|
||||
}
|
||||
|
||||
if (_canvas != null && _canvas.renderMode == RenderMode.ScreenSpaceCamera)
|
||||
{
|
||||
_renderCamera = _canvas.worldCamera;
|
||||
}
|
||||
|
||||
// ScreenSpaceOverlay 或找不到摄像机时使用MainCamera
|
||||
if (_renderCamera == null)
|
||||
_renderCamera = Camera.main;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查摄像机变换是否发生变化
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新指定中间类的屏幕坐标
|
||||
/// </summary>
|
||||
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 子对象哈希集
|
||||
|
||||
/// <summary>
|
||||
/// 标记子对象哈希集需要重建
|
||||
/// </summary>
|
||||
public void MarkChildrenSetDirty()
|
||||
{
|
||||
_childrenSetDirty = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重建子对象哈希集,用于O(1)快速查找
|
||||
/// </summary>
|
||||
public void RebuildChildrenSet()
|
||||
{
|
||||
_childrenHashSet.Clear();
|
||||
|
||||
var t = transform;
|
||||
for (int i = 0; i < t.childCount; i++)
|
||||
{
|
||||
_childrenHashSet.Add(t.GetChild(i).gameObject);
|
||||
}
|
||||
|
||||
_childrenSetDirty = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查指定对象是否在当前容器的子项中(O(1)哈希查找)
|
||||
/// </summary>
|
||||
/// <param name="obj">要检查的对象</param>
|
||||
/// <returns>是否存在于子项中</returns>
|
||||
public bool IsChildObject(GameObject obj)
|
||||
{
|
||||
if (obj == null) return false;
|
||||
|
||||
if (_childrenSetDirty)
|
||||
RebuildChildrenSet();
|
||||
|
||||
return _childrenHashSet.Contains(obj);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 对象池管理
|
||||
|
||||
/// <summary>
|
||||
/// 从对象池获取一个WindowState实例
|
||||
/// </summary>
|
||||
private WindowState GetPooledState()
|
||||
{
|
||||
if (_statePool.Count > 0)
|
||||
return _statePool.Dequeue();
|
||||
|
||||
return new WindowState();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 静态工具方法
|
||||
|
||||
/// <summary>
|
||||
/// 将世界空间坐标转换到Canvas容器内的本地坐标
|
||||
/// </summary>
|
||||
/// <param name="worldPos">世界空间坐标</param>
|
||||
/// <param name="camera">使用的摄像机</param>
|
||||
/// <param name="container">目标Canvas容器的RectTransform</param>
|
||||
/// <returns>容器内的本地坐标</returns>
|
||||
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<Canvas>(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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取Canvas使用的渲染摄像机
|
||||
/// </summary>
|
||||
/// <param name="canvas">目标Canvas</param>
|
||||
/// <returns>渲染摄像机(如果Canvas是Overlay模式则返回MainCamera)</returns>
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 21778cbeb1d1b2f478dc597145635887
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,147 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace XericUI.VisualForm
|
||||
{
|
||||
/// <summary>
|
||||
/// 锚点窗口 - WorldAnchor的子类,持有UI窗口对象的引用。
|
||||
/// 窗口对象可以是实际的场景对象,也可以是预制体。
|
||||
/// 如果窗口锚点坞在子项中找不到对应对象,则视为预制体,需要通过实例化方法创建。
|
||||
/// </summary>
|
||||
[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;
|
||||
|
||||
/// <summary>
|
||||
/// 窗口对象引用(可以是实例或预制体)
|
||||
/// </summary>
|
||||
public GameObject WindowObject
|
||||
{
|
||||
get => m_WindowObject;
|
||||
set => m_WindowObject = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// UI启用开关,设为false时窗口实例将被隐藏
|
||||
/// </summary>
|
||||
public bool EnableUI
|
||||
{
|
||||
get => m_EnableUI;
|
||||
set
|
||||
{
|
||||
if (m_EnableUI == value) return;
|
||||
m_EnableUI = value;
|
||||
ApplyUIState();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断窗口对象是否为预制体(不在锚点坞的子项中则为预制体)
|
||||
/// 此标记由WindowState在初始化时设置
|
||||
/// </summary>
|
||||
[System.NonSerialized]
|
||||
public bool IsPrefab;
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前有效的窗口Transform(实例或自身)
|
||||
/// </summary>
|
||||
public Transform GetWindowTransform()
|
||||
{
|
||||
if (m_WindowObject != null)
|
||||
return m_WindowObject.transform;
|
||||
return transform;
|
||||
}
|
||||
|
||||
#region 工具方法
|
||||
|
||||
/// <summary>
|
||||
/// 从所属锚点坞获取自己的中间窗口状态类
|
||||
/// </summary>
|
||||
/// <returns>WindowState实例,不存在则返回null</returns>
|
||||
public WindowState GetWindowState()
|
||||
{
|
||||
if (TargetDock == null)
|
||||
return null;
|
||||
return TargetDock.GetWindowState(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取窗口实例对象(预制体实例化后的对象,或子项中已有的对象)
|
||||
/// </summary>
|
||||
/// <returns>窗口GameObject实例</returns>
|
||||
public GameObject GetWindowInstance()
|
||||
{
|
||||
var state = GetWindowState();
|
||||
return state?.WindowInstance;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取窗口当前的屏幕坐标(容器内本地坐标)
|
||||
/// </summary>
|
||||
/// <returns>屏幕本地坐标</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取锚点当前的世界坐标
|
||||
/// </summary>
|
||||
/// <returns>世界坐标</returns>
|
||||
public Vector3 GetWorldPosition()
|
||||
{
|
||||
var state = GetWindowState();
|
||||
if (state != null)
|
||||
return state.LastWorldPosition;
|
||||
return CachedTransform.position;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查窗口实例是否处于活动状态
|
||||
/// </summary>
|
||||
public bool IsWindowActive()
|
||||
{
|
||||
var state = GetWindowState();
|
||||
if (state == null) return false;
|
||||
return state.IsEnabled && m_EnableUI;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 内部方法
|
||||
|
||||
/// <summary>
|
||||
/// 将UI开关状态应用到窗口实例
|
||||
/// </summary>
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 65b8e58a984465e48814956733bb6156
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,115 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace XericUI.VisualForm
|
||||
{
|
||||
/// <summary>
|
||||
/// 窗口实例对象池 - 管理预制体实例的复用,减少频繁的创建和销毁开销
|
||||
/// </summary>
|
||||
public static class WindowPool
|
||||
{
|
||||
/// <summary>
|
||||
/// 待命字典:键为预制体原型引用,值为隐藏的待复用实例队列
|
||||
/// </summary>
|
||||
private static readonly Dictionary<GameObject, Queue<GameObject>> s_StandbyDict = new Dictionary<GameObject, Queue<GameObject>>();
|
||||
|
||||
/// <summary>
|
||||
/// 从对象池获取一个实例。如果池中有则复用,否则实例化新的
|
||||
/// </summary>
|
||||
/// <param name="prefab">预制体原型</param>
|
||||
/// <param name="parent">父级Transform</param>
|
||||
/// <returns>获取到的实例</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将实例回收到对象池中
|
||||
/// </summary>
|
||||
/// <param name="prefab">预制体原型</param>
|
||||
/// <param name="instance">要回收的实例</param>
|
||||
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<GameObject>();
|
||||
s_StandbyDict[prefab] = queue;
|
||||
}
|
||||
|
||||
queue.Enqueue(instance);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清空指定预制体的对象池
|
||||
/// </summary>
|
||||
/// <param name="prefab">预制体原型</param>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清空所有对象池
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查指定预制体在池中是否有可用实例
|
||||
/// </summary>
|
||||
/// <param name="prefab">预制体原型</param>
|
||||
/// <returns>是否有可用实例</returns>
|
||||
public static bool HasAvailable(GameObject prefab)
|
||||
{
|
||||
return prefab != null && s_StandbyDict.TryGetValue(prefab, out var queue) && queue.Count > 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ecf3ad30a3eb01f40a4508ff21ba31dd
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,224 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace XericUI.VisualForm
|
||||
{
|
||||
/// <summary>
|
||||
/// 中间窗口状态类 - 管理锚点窗口和锚点坞之间的中间产物。
|
||||
/// 负责窗口实例的生命周期管理、坐标脏标记和屏幕定位。
|
||||
/// 使用对象池管理,避免频繁创建销毁。
|
||||
///
|
||||
/// 生命周期:
|
||||
/// OnStart - 从对象池取出时调用(初始化/复用)
|
||||
/// OnEnable - 锚点激活时调用(脏更新触发)
|
||||
/// OnDisable- 锚点隐藏时调用(脏更新触发)
|
||||
/// OnEnd - 回收到对象池时调用(清理/回收)
|
||||
/// </summary>
|
||||
public class WindowState
|
||||
{
|
||||
/// <summary>
|
||||
/// 关联的世界锚点
|
||||
/// </summary>
|
||||
public WorldAnchor Anchor { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 关联的锚点坞
|
||||
/// </summary>
|
||||
public AnchorDock Dock { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 原始窗口对象(来自AnchorWindow的引用)
|
||||
/// </summary>
|
||||
private GameObject _windowObject;
|
||||
|
||||
/// <summary>
|
||||
/// 实例化的窗口对象(如果是预制体则为此实例,否则为_windowObject本身)
|
||||
/// </summary>
|
||||
private GameObject _instance;
|
||||
|
||||
/// <summary>
|
||||
/// 是否为预制体
|
||||
/// </summary>
|
||||
private bool _isPrefab;
|
||||
|
||||
/// <summary>
|
||||
/// 是否为预制体(公开属性)
|
||||
/// </summary>
|
||||
public bool IsPrefab => _isPrefab;
|
||||
|
||||
/// <summary>
|
||||
/// 窗口实例对象(预制体实例化后的对象,或子项中已有的对象)
|
||||
/// </summary>
|
||||
public GameObject WindowInstance => _instance;
|
||||
|
||||
/// <summary>
|
||||
/// 上帧锚点世界坐标
|
||||
/// </summary>
|
||||
public Vector3 LastWorldPosition => _lastPosition;
|
||||
|
||||
/// <summary>
|
||||
/// 当前是否处于激活状态
|
||||
/// </summary>
|
||||
public bool IsEnabled { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 坐标脏标记 - 当锚点世界坐标发生变化时标记
|
||||
/// </summary>
|
||||
public bool CoordinateDirty { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 上帧世界坐标(用于比较变化量)
|
||||
/// </summary>
|
||||
private Vector3 _lastPosition;
|
||||
|
||||
#region 生命周期
|
||||
|
||||
/// <summary>
|
||||
/// 从对象池取出时的初始化(Start事件)
|
||||
/// </summary>
|
||||
/// <param name="anchor">关联的锚点</param>
|
||||
/// <param name="dock">关联的锚点坞</param>
|
||||
public void OnStart(WorldAnchor anchor, AnchorDock dock)
|
||||
{
|
||||
ResetState();
|
||||
|
||||
Anchor = anchor;
|
||||
Dock = dock;
|
||||
|
||||
// 解析窗口实例(原型对象或预制体实例化)
|
||||
ResolveWindowInstance();
|
||||
|
||||
// 初始化位置记录
|
||||
if (anchor != null)
|
||||
_lastPosition = anchor.CachedTransform.position;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析窗口实例:自动判断窗口对象是原型(已在子项中)还是预制体,并完成实例获取或创建。
|
||||
/// 如果锚点是AnchorWindow且携带窗口对象,则根据是否在坞子项中决定处理方向:
|
||||
/// - 在子项中 → 直接使用该对象作为实例(原型模式)
|
||||
/// - 不在子项中 → 从WindowPool获取或实例化预制体
|
||||
/// 如果锚点不是AnchorWindow或无窗口对象,则不创建实例。
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 锚点激活时的处理(Enable事件,由脏更新触发)
|
||||
/// </summary>
|
||||
public void OnEnable()
|
||||
{
|
||||
IsEnabled = true;
|
||||
|
||||
// 如果实例尚不存在(如纯WorldAnchor没有窗口对象),尝试解析
|
||||
if (_instance == null)
|
||||
ResolveWindowInstance();
|
||||
|
||||
if (_instance != null)
|
||||
_instance.SetActive(true);
|
||||
|
||||
// 激活时强制标记坐标脏,确保下一帧更新位置
|
||||
CoordinateDirty = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 锚点隐藏时的处理(Disable事件,由脏更新触发)
|
||||
/// </summary>
|
||||
public void OnDisable()
|
||||
{
|
||||
IsEnabled = false;
|
||||
|
||||
if (_instance != null)
|
||||
_instance.SetActive(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 回收到对象池时的清理(End事件)
|
||||
/// </summary>
|
||||
public void OnEnd()
|
||||
{
|
||||
// 回收预制体实例到对象池
|
||||
if (_isPrefab && _instance != null)
|
||||
{
|
||||
WindowPool.Release(_windowObject, _instance);
|
||||
}
|
||||
|
||||
ResetState();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 状态管理
|
||||
|
||||
/// <summary>
|
||||
/// 复位所有状态(用于对象池复用)
|
||||
/// </summary>
|
||||
private void ResetState()
|
||||
{
|
||||
Anchor = null;
|
||||
Dock = null;
|
||||
_windowObject = null;
|
||||
_instance = null;
|
||||
_isPrefab = false;
|
||||
IsEnabled = false;
|
||||
CoordinateDirty = false;
|
||||
_lastPosition = Vector3.zero;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查锚点坐标是否发生变化,超过阈值则标记脏
|
||||
/// 由锚点坞在每帧调用
|
||||
/// </summary>
|
||||
/// <param name="threshold">变化阈值</param>
|
||||
public void CheckPositionDirty(float threshold)
|
||||
{
|
||||
if (Anchor == null) return;
|
||||
|
||||
var currentPos = Anchor.CachedTransform.position;
|
||||
if (Vector3.Distance(_lastPosition, currentPos) > threshold)
|
||||
{
|
||||
CoordinateDirty = true;
|
||||
_lastPosition = currentPos;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前需要定位的Transform目标(实例化的窗口对象或锚点自身)
|
||||
/// </summary>
|
||||
/// <returns>目标Transform</returns>
|
||||
public Transform GetTargetTransform()
|
||||
{
|
||||
if (_instance != null)
|
||||
return _instance.transform;
|
||||
if (Anchor != null)
|
||||
return Anchor.CachedTransform;
|
||||
return null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ddaed09299c4aa44ba0d760add4f3597
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,71 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace XericUI.VisualForm
|
||||
{
|
||||
/// <summary>
|
||||
/// 世界对象锚点 - 挂载在场景对象上,将世界空间位置传递给锚点坞进行屏幕空间定位
|
||||
/// </summary>
|
||||
[AddComponentMenu("Xeric UI Vessel/AnchorWindow/WorldAnchor")]
|
||||
public class WorldAnchor : XVFormBase
|
||||
{
|
||||
[SerializeField, Tooltip("目标锚点坞,不填则自动在父级中查找")]
|
||||
private AnchorDock m_TargetDock;
|
||||
|
||||
/// <summary>
|
||||
/// 目标锚点坞引用
|
||||
/// </summary>
|
||||
public AnchorDock TargetDock
|
||||
{
|
||||
get => m_TargetDock;
|
||||
set => m_TargetDock = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 缓存Transform组件
|
||||
/// </summary>
|
||||
[System.NonSerialized]
|
||||
private Transform _cachedTransform;
|
||||
|
||||
/// <summary>
|
||||
/// 缓存Transform,避免重复GetComponent
|
||||
/// </summary>
|
||||
public Transform CachedTransform
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_cachedTransform == null)
|
||||
_cachedTransform = transform;
|
||||
return _cachedTransform;
|
||||
}
|
||||
}
|
||||
|
||||
protected void OnEnable()
|
||||
{
|
||||
// 尝试自动查找锚点坞
|
||||
if (m_TargetDock == null)
|
||||
m_TargetDock = GetComponentInParent<AnchorDock>();
|
||||
|
||||
// 回退到静态单例
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fdf2bb8183505fd4dbcf6095985e80af
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,51 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace XericUI.VisualForm
|
||||
{
|
||||
/// <summary>
|
||||
/// 虚拟窗体基类,提供基础的窗体标记功能
|
||||
/// </summary>
|
||||
[AddComponentMenu("Xeric UI Vessel/AnchorWindow/XVFormBase")]
|
||||
public class XVFormBase : MonoBehaviour
|
||||
{
|
||||
[SerializeField]
|
||||
private string m_FormID;
|
||||
|
||||
/// <summary>
|
||||
/// 窗体唯一标识
|
||||
/// </summary>
|
||||
public string FormID
|
||||
{
|
||||
get => m_FormID;
|
||||
set => m_FormID = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 窗体是否处于活动状态
|
||||
/// </summary>
|
||||
public bool IsFormActive => gameObject.activeInHierarchy && enabled;
|
||||
|
||||
protected virtual void Awake()
|
||||
{
|
||||
TryGenerateFormID();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试自动生成FormID(如果当前为空)
|
||||
/// </summary>
|
||||
private void TryGenerateFormID()
|
||||
{
|
||||
if (string.IsNullOrEmpty(m_FormID))
|
||||
{
|
||||
m_FormID = ((uint)GetInstanceID()).ToString();
|
||||
}
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
protected virtual void OnValidate()
|
||||
{
|
||||
TryGenerateFormID();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ea57a5ab7132aa34f862d183e6910015
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user