using UnityEngine; /// /// 让一个"普通 Transform"(非 RectTransform,如 gameplay 敌人容器 empty/enemyObject)在分辨率变化时 /// 横向跟随其所属 Canvas 的某一屏幕边缘,保持到该边缘的间距恒定。 /// /// 用于敌人这类"用普通 Transform 本地坐标定位、挂在 ScreenSpaceCamera Canvas 下"的对象—— /// 它们无锚点,UIEdgeFollow(要求 RectTransform)用不了。本组件自测量:运行时读取对象在所属 /// Canvas 局部空间的 X,算出到选定边缘的间距并保持恒定,与该 Canvas 的精确坐标系无关(健壮)。 /// /// 仅改 localPosition.x(表现层),不改 Y/Z/旋转/缩放/引用,不与只改 alpha/scaleY 的入场动画冲突。 /// 满足三要求:1) 不改业务逻辑/引用;2) 仅横向,首帧于设计分辨率(16:9)测量则位置逐像素不变; /// 3) 跟随边缘。 /// [DisallowMultipleComponent] public sealed class UITransformEdgeFollow : MonoBehaviour { public enum Edge { Left, Right } [SerializeField] private Edge edge = Edge.Left; // 设计基准宽度:gap 以 16:9(1920)设计画布为基准计算,与首次附加时的实际分辨率无关, // 保证 16:9 逐像素一致、其它分辨率下敌人始终贴同一边缘、露出同样部分。 private const float DesignWidth = 1920f; private Transform tf; private RectTransform rootRt; // 所属 Canvas 的 RectTransform private float gap; // 对象中心到选定边缘的间距(设计参考单位,恒定) private bool measured; private float lastRootW = -1f; private void OnEnable() { tf = transform; ResolveRoot(); Measure(); } private void ResolveRoot() { Canvas c = GetComponentInParent(); if (c != null) { Canvas root = c.rootCanvas != null ? c.rootCanvas : c; rootRt = root.GetComponent(); } } private void Measure() { if (measured || tf == null || rootRt == null) return; float rootW = rootRt.rect.width; if (rootW <= 0f) return; // 对象在 Canvas 局部空间的 X。该值由固定 localPosition 决定,与分辨率无关(缩放均匀), // 即为设计位置。Canvas 关于中心对称(rect 以 0 为中心),故设计左/右缘 = ∓DesignWidth/2。 float localX = rootRt.InverseTransformPoint(tf.position).x; float designLeft = -DesignWidth * 0.5f; // 16:9 设计左缘 = -960 float designRight = DesignWidth * 0.5f; // 16:9 设计右缘 = +960 // gap = 对象中心到选定边缘的距离(设计参考单位,恒定)。 // 敌人 gap 会是负值(设计上就在左缘之外,只露右半)——正是要保持的视觉。 gap = edge == Edge.Left ? (localX - designLeft) : (designRight - localX); lastRootW = rootW; measured = true; // 应用一次,使当前分辨率立即到位(不必等分辨率变化)。 ApplyToEdge(); } private void LateUpdate() { if (rootRt == null) { ResolveRoot(); return; } if (!measured) { Measure(); return; } float rootW = rootRt.rect.width; if (Mathf.Abs(rootW - lastRootW) <= 0.5f) return; lastRootW = rootW; ApplyToEdge(); } // 让对象中心距当前画布选定边缘保持恒定的 gap(设计参考单位)。 // 画布关于中心对称,故当前左缘 = -rootW/2、右缘 = +rootW/2。 private void ApplyToEdge() { if (tf == null || rootRt == null || !measured) return; float rootW = rootRt.rect.width; if (rootW <= 0f) return; float curLeft = -rootW * 0.5f; float curRight = rootW * 0.5f; float targetLocalX = edge == Edge.Left ? (curLeft + gap) : (curRight - gap); float curLocalX = rootRt.InverseTransformPoint(tf.position).x; float deltaLocalX = targetLocalX - curLocalX; if (Mathf.Abs(deltaLocalX) <= 0.01f) return; // 把 Canvas 局部 X 位移换算到对象自身父空间的 localPosition.x。 Vector3 worldCur = tf.position; Vector3 localCurInCanvas = rootRt.InverseTransformPoint(worldCur); Vector3 worldTarget = rootRt.TransformPoint(new Vector3(targetLocalX, localCurInCanvas.y, localCurInCanvas.z)); Vector3 parentTarget = tf.parent != null ? tf.parent.InverseTransformPoint(worldTarget) : worldTarget; tf.localPosition = new Vector3(parentTarget.x, tf.localPosition.y, tf.localPosition.z); } }