Files
2026-07-22 05:35:53 +08:00

115 lines
5.1 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using UnityEngine;
/// <summary>
/// 让一个“中心锚定 + 固定横向偏移”的顶层 HUD 容器,在任意宽高比下横向跟随其设计所属的屏幕边缘。
/// 只处理横向(X),纵向(Y)完全不动;保持 1920 设计宽度下的视觉位置逐像素不变。
///
/// 为什么用组件而非直接改业务脚本坐标:许多 HUD 的位置由业务脚本在运行时写死设置
/// (relative 到这些容器)。本组件只移动“容器”本身到边缘相对位置,容器内部由脚本布置的
/// 子元素保持相对关系不变——既实现了“跟随边缘”,又不触碰任何业务坐标逻辑。
///
/// 满足三要求:1) 不改业务逻辑/引用;2) 仅横向、16:9 逐像素一致、不动 Y 与动画;3) 跟随边缘。
/// </summary>
[RequireComponent(typeof(RectTransform))]
[DisallowMultipleComponent]
public sealed class UIEdgeFollow : MonoBehaviour
{
public enum Edge { Left, Right, Auto }
public Edge edge = Edge.Auto;
// 设计基准宽度:所有 gap 以此为参考,保证与首次附加时的分辨率无关。
private const float DesignWidth = 1920f;
private RectTransform rt;
private RectTransform rootRt;
private float gapX; // 元素中心距目标边缘的间距(设计参考像素,恒定)
private Edge resolved;
private bool measured;
private float lastRootW = -1f;
private void OnEnable()
{
rt = GetComponent<RectTransform>();
ResolveRoot();
Measure();
ApplyIfNeeded(true);
}
private void ResolveRoot()
{
Canvas c = GetComponentInParent<Canvas>();
if (c != null)
{
Canvas root = c.rootCanvas != null ? c.rootCanvas : c;
rootRt = root.GetComponent<RectTransform>();
}
}
// 元素在画布局部空间的设计 X(以画布中心为原点)。用世界坐标换算,正确处理嵌套父级
// (如 bbt 挂在 logo 下,anchoredPosition 相对 logo 而非画布)。
private float designCanvasX;
private void Measure()
{
if (measured || rt == null || rootRt == null) return;
// 仅处理横向未拉伸 + 中心锚定的元素。
if (Mathf.Abs(rt.anchorMax.x - rt.anchorMin.x) > 0.01f) return;
if (Mathf.Abs(rt.anchorMin.x - 0.5f) > 0.01f) return;
// 用元素世界位置换算到"画布局部空间"的 X(画布中心为原点)。
// 关键:InverseTransformPoint 已返回画布参考单位(reference pixels),且与分辨率无关
//anchoredPosition 固定在参考单位、画布缩放被 Inverse 抵消),直接用即可。
// 绝不能再除以 rootW 归一——那会在非 16:9(rootW≠1920)时把坐标算错,元素停在中间。
// 这对直接子级(uid/score)和嵌套子级(bbt 挂在 logo 下)都正确。
float rootW = rootRt.rect.width;
if (rootW <= 0f) return;
designCanvasX = rootRt.InverseTransformPoint(rt.position).x; // 画布中心为原点,参考单位
// gap = 元素中心到"设计左/右缘"的距离(参考单位,恒定)。设计画布宽=DesignWidth。
float distFromDesignLeft = DesignWidth * 0.5f + designCanvasX; // 到左缘距离
resolved = edge;
if (resolved == Edge.Auto)
resolved = designCanvasX < 0f ? Edge.Left : Edge.Right;
gapX = resolved == Edge.Left ? distFromDesignLeft : (DesignWidth - distFromDesignLeft);
measured = true;
}
private void LateUpdate()
{
if (rootRt == null) { ResolveRoot(); return; }
if (!measured) { Measure(); if (!measured) return; }
float rootW = rootRt.rect.width;
if (Mathf.Abs(rootW - lastRootW) > 0.5f) ApplyIfNeeded(false);
}
private void ApplyIfNeeded(bool force)
{
if (rt == null || rootRt == null) return;
if (!measured) { Measure(); if (!measured) return; }
float rootW = rootRt.rect.width;
if (rootW <= 0f) return;
lastRootW = rootW;
// 目标画布局部 X(画布中心为原点,参考单位):当前画布半宽=rootW/2。
// 左贴边:目标 = -rootW/2 + gapX;右贴边:目标 = rootW/2 - gapX。
float halfW = rootW * 0.5f;
float targetCanvasLocalX = resolved == Edge.Left ? (-halfW + gapX) : (halfW - gapX);
// 当前画布局部 X;若已到位则跳过。
Vector3 worldCur = rt.position;
Vector3 canvasCur = rootRt.InverseTransformPoint(worldCur);
if (Mathf.Abs(targetCanvasLocalX - canvasCur.x) <= 0.01f) return;
// 画布局部目标 → 世界目标点 → 自身父空间 anchoredPosition.x(仅横向)。
Vector3 worldTarget = rootRt.TransformPoint(new Vector3(targetCanvasLocalX, canvasCur.y, canvasCur.z));
float worldDeltaX = worldTarget.x - worldCur.x;
Vector3 parentScale = rt.parent != null ? rt.parent.lossyScale : Vector3.one;
float parentScaleX = Mathf.Abs(parentScale.x) < 1e-6f ? 1f : parentScale.x;
Vector2 anchoredNow = rt.anchoredPosition;
rt.anchoredPosition = new Vector2(anchoredNow.x + worldDeltaX / parentScaleX, anchoredNow.y);
}
}