using UnityEngine; /// /// 让一个“中心锚定 + 固定横向偏移”的顶层 HUD 容器,在任意宽高比下横向跟随其设计所属的屏幕边缘。 /// 只处理横向(X),纵向(Y)完全不动;保持 1920 设计宽度下的视觉位置逐像素不变。 /// /// 为什么用组件而非直接改业务脚本坐标:许多 HUD 的位置由业务脚本在运行时写死设置 /// (relative 到这些容器)。本组件只移动“容器”本身到边缘相对位置,容器内部由脚本布置的 /// 子元素保持相对关系不变——既实现了“跟随边缘”,又不触碰任何业务坐标逻辑。 /// /// 满足三要求:1) 不改业务逻辑/引用;2) 仅横向、16:9 逐像素一致、不动 Y 与动画;3) 跟随边缘。 /// [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(); ResolveRoot(); Measure(); ApplyIfNeeded(true); } private void ResolveRoot() { Canvas c = GetComponentInParent(); if (c != null) { Canvas root = c.rootCanvas != null ? c.rootCanvas : c; rootRt = root.GetComponent(); } } // 元素在画布局部空间的设计 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); } }