修改锚点窗口对entry类的依赖,现在可以自行通过泛型扩展。

This commit is contained in:
2026-06-15 09:26:24 +08:00
parent 856c395819
commit 922eb0e450
11 changed files with 394 additions and 26 deletions
+134 -6
View File
@@ -27,6 +27,13 @@ namespace XericUI.VisualForm
[SerializeField, Tooltip("预分配的中间类对象池容量")]
private int m_StatePoolPreallocate = 16;
[Header("安全区")]
[SerializeField, Tooltip("安全区矩形(Canvas本地空间),窗口超出此区域视为超出安全区")]
private Rect m_SafeZone = new Rect(-800f, -450f, 1600f, 900f);
[SerializeField, Tooltip("安全区检测模式")]
private SafeZoneMode m_SafeZoneMode = SafeZoneMode.Rect;
#region 静态单例
private static AnchorDock s_Main;
@@ -243,11 +250,12 @@ namespace XericUI.VisualForm
state.CheckPositionDirty(m_PositionThreshold);
}
// 4. 遍历所有中间类,根据脏标记更新坐标
// 4. 遍历所有中间类,根据脏标记更新坐标(跳过无启用条目的状态)
foreach (var kvp in _activeStates)
{
var state = kvp.Value;
if (state == null || !state.IsEnabled) continue;
if (!state.HasAnyEnabled) continue;
bool needUpdate = _cameraDirty || state.CoordinateDirty;
if (needUpdate)
@@ -436,7 +444,8 @@ namespace XericUI.VisualForm
}
/// <summary>
/// 更新指定中间类中所有窗口条目的屏幕坐标(基础位置 + 条目偏移量)
/// 更新指定中间类中所有窗口条目的屏幕坐标。
/// 流程:计算基础坐标 → 检测溢出 → 钳制需要安全区约束的条目 → 逐个设置位置。
/// </summary>
private void UpdateStatePosition(WindowState state)
{
@@ -448,14 +457,44 @@ namespace XericUI.VisualForm
if (!TryWorldToScreenPoint(worldPos, _worldCamera, _uiCamera, _canvas, RectTransform, out Vector2 basePoint))
return;
// 为每个窗口条目设置位置:基础坐标 + 条目自身偏移量
foreach (var entry in state.Entries)
Rect canvasLocalRect = ((RectTransform)_canvas.transform).rect;
bool hasClamp = state.HasAnyClamp;
var anchorWindow = state.Anchor as IAnchorWindowEntry;
// 计算基础坐标的溢出状态
OverflowState baseOverflow = CheckOverflow(basePoint, m_SafeZone, m_SafeZoneMode, canvasLocalRect);
state.Overflow = baseOverflow;
// 为基础坐标计算钳制位置(仅在需要钳制的条目上使用)
Vector2 clampedBase = hasClamp ? ClampToSafeZone(basePoint, m_SafeZone, m_SafeZoneMode) : basePoint;
for (int i = 0; i < state.Entries.Count; i++)
{
var entry = state.Entries[i];
if (entry.Instance == null) continue;
if (!entry.Enabled) continue;
Vector2 rawPoint = basePoint + entry.ScreenOffset;
Vector2 finalPoint;
if (entry.ClampToSafeZone && baseOverflow != OverflowState.InRange)
{
finalPoint = clampedBase + entry.ScreenOffset;
}
else
{
finalPoint = rawPoint;
}
// 检测逐条目溢出状态变化,触发生命周期事件
OverflowState entryOverflow = CheckOverflow(finalPoint, m_SafeZone, m_SafeZoneMode, canvasLocalRect);
if (entryOverflow != entry.CurrentOverflow)
{
entry.CurrentOverflow = entryOverflow;
anchorWindow?.OnWindowLevelChange(i, entryOverflow);
}
Vector2 finalPoint = basePoint + entry.ScreenOffset;
var target = entry.Instance.transform;
if (target is RectTransform rectTarget)
rectTarget.anchoredPosition = finalPoint;
else
@@ -607,6 +646,95 @@ namespace XericUI.VisualForm
return Camera.main;
}
/// <summary>
/// 检查本地坐标是否超出安全区。
/// 矩形模式:检查点是否在Rect范围内。
/// 椭圆模式:检查点到中心的归一化距离是否超过各轴半径。
/// </summary>
/// <param name="localPoint">Canvas本地坐标</param>
/// <param name="safeZone">安全区矩形</param>
/// <param name="mode">检测模式</param>
/// <param name="canvasLocalRect">Canvas本地空间矩形(用于判断是否超出屏幕)</param>
/// <returns>溢出状态(Flags可叠加)</returns>
public static OverflowState CheckOverflow(Vector2 localPoint, Rect safeZone, SafeZoneMode mode, Rect canvasLocalRect)
{
OverflowState result = OverflowState.InRange;
// 检查是否超出安全区
bool outOfSafeZone;
if (mode == SafeZoneMode.Ellipse)
{
// 椭圆:(px/rx)² + (py/ry)² > 1
Vector2 center = safeZone.center;
float rx = safeZone.width * 0.5f;
float ry = safeZone.height * 0.5f;
if (rx <= 0f || ry <= 0f)
{
outOfSafeZone = false;
}
else
{
float nx = (localPoint.x - center.x) / rx;
float ny = (localPoint.y - center.y) / ry;
outOfSafeZone = (nx * nx + ny * ny) > 1f;
}
}
else
{
outOfSafeZone = !safeZone.Contains(localPoint);
}
if (outOfSafeZone)
result |= OverflowState.OutOfSafeZone;
// 检查是否超出屏幕
if (!canvasLocalRect.Contains(localPoint))
result |= OverflowState.OutOfScreen;
return result;
}
/// <summary>
/// 将本地坐标钳制到安全区边界内。
/// 矩形模式:clamp x到[minX, maxX],y到[minY, maxY]。
/// 椭圆模式:沿中心方向投影到椭圆边界上。
/// </summary>
/// <param name="localPoint">Canvas本地坐标</param>
/// <param name="safeZone">安全区矩形</param>
/// <param name="mode">检测模式</param>
/// <returns>钳制后的坐标</returns>
public static Vector2 ClampToSafeZone(Vector2 localPoint, Rect safeZone, SafeZoneMode mode)
{
if (mode == SafeZoneMode.Ellipse)
{
Vector2 center = safeZone.center;
float rx = safeZone.width * 0.5f;
float ry = safeZone.height * 0.5f;
if (rx <= 0f || ry <= 0f)
return center;
Vector2 delta = localPoint - center;
float nx = delta.x / rx;
float ny = delta.y / ry;
float dist = Mathf.Sqrt(nx * nx + ny * ny);
if (dist <= 1f)
return localPoint; // 已在椭圆内
// 投影到椭圆边界
return center + delta / dist;
}
else
{
// 矩形钳制
return new Vector2(
Mathf.Clamp(localPoint.x, safeZone.xMin, safeZone.xMax),
Mathf.Clamp(localPoint.y, safeZone.yMin, safeZone.yMax)
);
}
}
#endregion
}
}
+74 -8
View File
@@ -1,24 +1,33 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
namespace XericUI.VisualForm
{
/// <summary>
/// 锚点窗口
/// </summary>
[AddComponentMenu("Xeric UI Vessel/AnchorWindow/AnchorWindow")]
public class AnchorWindowByDefaultEntry : AnchorWindow<WindowEntry> { }
/// <summary>
/// 锚点窗口 - WorldAnchor的子类,持有一组UI窗口条目的引用。
/// 每个条目包含窗口对象、屏幕偏移量和激活状态,支持一个锚点同时生成多个定位窗口。
/// 窗口对象可以是实际的场景对象,也可以是预制体。
/// </summary>
[AddComponentMenu("Xeric UI Vessel/AnchorWindow/AnchorWindow")]
public class AnchorWindow : WorldAnchor
public abstract class AnchorWindow<T> : WorldAnchor, IAnchorWindowEntry<T>
where T : WindowEntry
{
[Header("窗口条目")]
[SerializeField, Tooltip("窗口对象集合,每个条目可独立配置偏移和启用状态")]
private List<WindowEntry> m_WindowEntries = new List<WindowEntry>();
private List<T> m_WindowEntries = new List<T>();
/// <summary>
/// 窗口条目列表
/// </summary>
public List<WindowEntry> WindowEntries => m_WindowEntries;
public IList<T> WindowEntries => m_WindowEntries;
public IEnumerable<WindowEntry> DefaultTypeWindowEntries => m_WindowEntries;
public int DefaultTypeWindowEntriesCount => m_WindowEntries.Count;
/// <summary>
/// 当前生效的窗口条目数(过滤掉WindowObject为null的条目)
@@ -36,8 +45,9 @@ namespace XericUI.VisualForm
}
/// <summary>
/// 获取第一个有效窗口对象的Transform(兼容旧API)
/// 获取第一个有效窗口对象的Transform
/// </summary>
[Obsolete("旧api")]
public Transform GetWindowTransform()
{
foreach (var entry in m_WindowEntries)
@@ -50,6 +60,50 @@ namespace XericUI.VisualForm
#region 工具方法
/// <summary>
/// 设置指定索引条目的启用状态。写入后自动检查所有条目,
/// 若全部禁用则同步标记到 WindowState(坞将跳过该窗口的位置更新)。
/// </summary>
/// <param name="index">条目索引</param>
/// <param name="enabled">是否启用</param>
public void SetEntryEnabled(int index, bool enabled)
{
if (index < 0 || index >= m_WindowEntries.Count) return;
var entry = m_WindowEntries[index];
if (entry == null) return;
entry.SetEnabledInternal(enabled);
// 同步到WindowState
GetWindowState()?.RefreshFlags();
}
public void SetAllEntryEnabled(bool enabled)
{
for (int i = 0; i < m_WindowEntries.Count; i++)
m_WindowEntries[i].SetEnabledInternal(enabled);
GetWindowState()?.RefreshFlags();
}
/// <summary>
/// 设置指定索引条目的钳制到安全区标记。写入后自动同步 WindowState 的 HasAnyClamp。
/// </summary>
/// <param name="index">条目索引</param>
/// <param name="clamp">是否钳制</param>
public void SetEntryClampToSafeZone(int index, bool clamp)
{
if (index < 0 || index >= m_WindowEntries.Count) return;
var entry = m_WindowEntries[index];
if (entry == null) return;
entry.SetClampToSafeZoneInternal(clamp);
// 同步到WindowState
GetWindowState()?.RefreshFlags();
}
/// <summary>
/// 从所属锚点坞获取自己的中间窗口状态类
/// </summary>
@@ -118,5 +172,17 @@ namespace XericUI.VisualForm
}
#endregion
#region 生命周期事件
/// <summary>
/// 窗口层级变更事件 — 当某个条目的溢出状态发生变化时触发。
/// 继承类可重写此方法,根据溢出状态切换不同窗口显示(如溢出后切换为指向示意器)。
/// </summary>
/// <param name="entryIndex">发生变化的条目索引</param>
/// <param name="state">新的溢出状态</param>
public virtual void OnWindowLevelChange(int entryIndex, OverflowState state) { }
#endregion
}
}
+33
View File
@@ -0,0 +1,33 @@
using System.Collections.Generic;
namespace XericUI.VisualForm
{
public interface IAnchorWindowEntry
{
/// <summary>
/// 默认窗口条目列表
/// </summary>
public IEnumerable<WindowEntry> DefaultTypeWindowEntries { get; }
/// <summary>
/// 默认窗口条目数量
/// </summary>
public int DefaultTypeWindowEntriesCount { get; }
/// <summary>
/// 窗口层级变更事件 — 当某个条目的溢出状态发生变化时触发。
/// </summary>
/// <param name="entryIndex">发生变化的条目索引</param>
/// <param name="state">新的溢出状态</param>
public void OnWindowLevelChange(int entryIndex, OverflowState state);
}
public interface IAnchorWindowEntry<T> : IAnchorWindowEntry
where T : WindowEntry
{
/// <summary>
/// 窗口条目列表
/// </summary>
public IList<T> WindowEntries { get; }
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 36a31785515e4fa98a926f175813ebbc
timeCreated: 1781485721
+29
View File
@@ -0,0 +1,29 @@
using System;
namespace XericUI.VisualForm
{
/// <summary>
/// 安全区检测模式
/// </summary>
public enum SafeZoneMode
{
/// <summary>矩形安全区</summary>
Rect,
/// <summary>椭圆形安全区</summary>
Ellipse
}
/// <summary>
/// 窗口溢出状态(Flags枚举,支持位运算叠加)
/// </summary>
[Flags]
public enum OverflowState
{
/// <summary>在安全区范围内</summary>
InRange = 0,
/// <summary>超出安全区</summary>
OutOfSafeZone = 1 << 0,
/// <summary>超出屏幕</summary>
OutOfScreen = 1 << 1
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3402a5cfbf1cca740b9374783abb4c6b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+22 -6
View File
@@ -6,6 +6,7 @@ namespace XericUI.VisualForm
/// 窗口条目数据 — 锚点窗口上每个窗口对象的配置集合。
/// 包含窗口对象引用、屏幕二维偏移量和激活状态。
/// 支持一个锚点同时生成多个定位窗口。
/// 运行时修改通过 AnchorWindow 的 SetEntryXxx 方法,自动同步到 WindowState。
/// </summary>
[System.Serializable]
public class WindowEntry
@@ -19,6 +20,9 @@ namespace XericUI.VisualForm
[SerializeField, Tooltip("是否启用此窗口")]
private bool m_Enabled = true;
[SerializeField, Tooltip("超出安全区后是否钳制在安全区内")]
private bool m_ClampToSafeZone;
/// <summary>
/// 窗口对象引用(实例或预制体)
/// </summary>
@@ -38,13 +42,14 @@ namespace XericUI.VisualForm
}
/// <summary>
/// 是否启用此窗口
/// 是否启用此窗口。代码修改请通过 AnchorWindow.SetEntryEnabled()
/// </summary>
public bool Enabled
{
get => m_Enabled;
set => m_Enabled = value;
}
public bool Enabled => m_Enabled;
/// <summary>
/// 超出安全区后是否钳制在安全区内。代码修改请通过 AnchorWindow.SetEntryClampToSafeZone()
/// </summary>
public bool ClampToSafeZone => m_ClampToSafeZone;
// --- 运行时状态(非序列化) ---
@@ -59,5 +64,16 @@ namespace XericUI.VisualForm
/// </summary>
[System.NonSerialized]
public bool IsPrefab;
/// <summary>
/// 当前帧的溢出状态(逐条目跟踪,用于检测变化触发事件)
/// </summary>
[System.NonSerialized]
public OverflowState CurrentOverflow;
// --- 内部方法(供 AnchorWindow 调用) ---
internal void SetEnabledInternal(bool value) => m_Enabled = value;
internal void SetClampToSafeZoneInternal(bool value) => m_ClampToSafeZone = value;
}
}
+85 -3
View File
@@ -37,6 +37,21 @@ namespace XericUI.VisualForm
/// </summary>
public bool IsEnabled { get; private set; }
/// <summary>
/// 当前窗口溢出状态(由坞每帧更新)
/// </summary>
public OverflowState Overflow { get; set; }
/// <summary>
/// 是否存在至少一个需要钳制在安全区内的条目
/// </summary>
public bool HasAnyClamp { get; private set; }
/// <summary>
/// 是否存在至少一个启用的条目
/// </summary>
public bool HasAnyEnabled { get; private set; }
/// <summary>
/// 坐标脏标记 - 当锚点世界坐标发生变化时标记
/// </summary>
@@ -80,13 +95,12 @@ namespace XericUI.VisualForm
{
if (Dock == null || Anchor == null) return;
var anchorWindow = Anchor as AnchorWindow;
if (anchorWindow == null || anchorWindow.WindowEntries.Count == 0)
if (Anchor is not IAnchorWindowEntry { DefaultTypeWindowEntriesCount: > 0 } anchorWindow)
return;
Entries.Clear();
foreach (var srcEntry in anchorWindow.WindowEntries)
foreach (var srcEntry in anchorWindow.DefaultTypeWindowEntries)
{
if (srcEntry == null || srcEntry.WindowObject == null)
continue;
@@ -109,6 +123,8 @@ namespace XericUI.VisualForm
Entries.Add(entry);
}
RefreshFlags();
}
/// <summary>
@@ -178,10 +194,32 @@ namespace XericUI.VisualForm
Anchor = null;
Dock = null;
IsEnabled = false;
Overflow = OverflowState.InRange;
HasAnyClamp = false;
HasAnyEnabled = false;
CoordinateDirty = false;
_lastPosition = Vector3.zero;
}
/// <summary>
/// 刷新聚合标记:遍历所有条目,更新 HasAnyClamp 和 HasAnyEnabled。
/// 由 AnchorWindow 的 SetEntryXxx 方法或 ResolveWindowEntries 调用。
/// </summary>
public void RefreshFlags()
{
bool anyClamp = false;
bool anyEnabled = false;
foreach (var entry in Entries)
{
if (entry.ClampToSafeZone) anyClamp = true;
if (entry.Enabled) anyEnabled = true;
}
HasAnyClamp = anyClamp;
HasAnyEnabled = anyEnabled;
}
/// <summary>
/// 检查锚点坐标是否发生变化,超过阈值则标记脏
/// 由锚点坞在每帧调用
@@ -224,5 +262,49 @@ namespace XericUI.VisualForm
}
#endregion
#region 静态工具方法 — 指向示意旋转计算
/// <summary>
/// 计算从钳制位置指向屏幕中心的方向矢量。
/// </summary>
/// <param name="clampedPosition">钳制后的屏幕坐标</param>
/// <param name="screenCenter">屏幕中心坐标</param>
/// <returns>归一化方向矢量(从clampedPosition指向screenCenter)</returns>
public static Vector2 GetLookAtDirection(Vector2 clampedPosition, Vector2 screenCenter)
{
Vector2 dir = screenCenter - clampedPosition;
return dir.sqrMagnitude > 0.0001f ? dir.normalized : Vector2.up;
}
/// <summary>
/// 计算Z轴旋转角度,使初始指向矢量旋转后对准从钳制位置到屏幕中心的方向。
/// 典型用法:传入 Vector2.up 作为初始指向,得到 transform.eulerAngles.z 的值。
/// </summary>
/// <param name="clampedPosition">钳制后的屏幕坐标</param>
/// <param name="screenCenter">屏幕中心坐标</param>
/// <param name="initialDirection">初始指向矢量(如 Vector2.up 表示默认向上)</param>
/// <returns>Z轴旋转角度(度),可直接赋给 transform.eulerAngles.z</returns>
public static float GetLookAtAngle(Vector2 clampedPosition, Vector2 screenCenter, Vector2 initialDirection)
{
Vector2 dir = GetLookAtDirection(clampedPosition, screenCenter);
return Vector2.SignedAngle(initialDirection, dir);
}
/// <summary>
/// 计算旋转四元数,使初始指向矢量旋转后对准从钳制位置到屏幕中心的方向。
/// 典型用法:传入 Vector2.up 作为初始指向,得到 transform.rotation 的值。
/// </summary>
/// <param name="clampedPosition">钳制后的屏幕坐标</param>
/// <param name="screenCenter">屏幕中心坐标</param>
/// <param name="initialDirection">初始指向矢量(如 Vector2.up 表示默认向上)</param>
/// <returns>Z轴旋转四元数,可直接赋给 transform.rotation</returns>
public static Quaternion GetLookAtRotation(Vector2 clampedPosition, Vector2 screenCenter, Vector2 initialDirection)
{
float angle = GetLookAtAngle(clampedPosition, screenCenter, initialDirection);
return Quaternion.Euler(0f, 0f, angle);
}
#endregion
}
}
+1 -1
View File
@@ -6,7 +6,7 @@ namespace XericUI.VisualForm
/// 世界对象锚点 - 挂载在场景对象上,将世界空间位置传递给锚点坞进行屏幕空间定位
/// </summary>
[AddComponentMenu("Xeric UI Vessel/AnchorWindow/WorldAnchor")]
public class WorldAnchor : XVFormBase
public class WorldAnchor : XvFormBase
{
[SerializeField, Tooltip("目标锚点坞,不填则自动在父级中查找")]
private AnchorDock m_TargetDock;
@@ -5,8 +5,8 @@ namespace XericUI.VisualForm
/// <summary>
/// 虚拟窗体基类,提供基础的窗体标记功能
/// </summary>
[AddComponentMenu("Xeric UI Vessel/AnchorWindow/XVFormBase")]
public class XVFormBase : MonoBehaviour
[AddComponentMenu("Xeric UI Vessel/AnchorWindow/XvFormBase")]
public class XvFormBase : MonoBehaviour
{
[SerializeField]
private string m_FormID;