好多新内容
This commit is contained in:
@@ -1758,9 +1758,9 @@ RectTransform:
|
||||
- {fileID: 1447481020518725738}
|
||||
m_Father: {fileID: 4008714089197239054}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: -628.13, y: -267.01}
|
||||
m_AnchorMin: {x: 0, y: 0.5}
|
||||
m_AnchorMax: {x: 0, y: 0.5}
|
||||
m_AnchoredPosition: {x: 331.87, y: -267.01}
|
||||
m_SizeDelta: {x: 100, y: 100}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &8373938424588989755
|
||||
@@ -2124,9 +2124,9 @@ RectTransform:
|
||||
- {fileID: 1318901128213550095}
|
||||
m_Father: {fileID: 5634981615641515618}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: 323.3, y: 5}
|
||||
m_AnchorMin: {x: 1, y: 0.5}
|
||||
m_AnchorMax: {x: 1, y: 0.5}
|
||||
m_AnchoredPosition: {x: -636.7, y: 5}
|
||||
m_SizeDelta: {x: 100, y: 100}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!1 &1512308382957859215
|
||||
@@ -2161,9 +2161,9 @@ RectTransform:
|
||||
m_Children: []
|
||||
m_Father: {fileID: 6434860682258445909}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: -886.6095, y: -440.45862}
|
||||
m_AnchorMin: {x: 0, y: 0.5}
|
||||
m_AnchorMax: {x: 0, y: 0.5}
|
||||
m_AnchoredPosition: {x: 73.3905, y: -440.45862}
|
||||
m_SizeDelta: {x: 146.7813, y: 61.0829}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &7958189264278634160
|
||||
@@ -3023,9 +3023,9 @@ RectTransform:
|
||||
- {fileID: 5977557715898357349}
|
||||
m_Father: {fileID: 5634981615641515618}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: -280.4, y: 5.78}
|
||||
m_AnchorMin: {x: 0, y: 0.5}
|
||||
m_AnchorMax: {x: 0, y: 0.5}
|
||||
m_AnchoredPosition: {x: 679.6, y: 5.78}
|
||||
m_SizeDelta: {x: 2410.4807, y: 100}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &4595481187193100221
|
||||
@@ -3592,9 +3592,9 @@ RectTransform:
|
||||
- {fileID: 7954660427444040773}
|
||||
m_Father: {fileID: 5634981615641515618}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: -520.8, y: -0.00005340576}
|
||||
m_AnchorMin: {x: 0, y: 0.5}
|
||||
m_AnchorMax: {x: 0, y: 0.5}
|
||||
m_AnchoredPosition: {x: 439.2, y: -0.00005340576}
|
||||
m_SizeDelta: {x: 128, y: 30}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!1 &2463687307556080059
|
||||
@@ -6388,9 +6388,9 @@ RectTransform:
|
||||
- {fileID: 6358183742841940414}
|
||||
m_Father: {fileID: 5634981615641515618}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: 518.05, y: 5}
|
||||
m_AnchorMin: {x: 1, y: 0.5}
|
||||
m_AnchorMax: {x: 1, y: 0.5}
|
||||
m_AnchoredPosition: {x: -441.95, y: 5}
|
||||
m_SizeDelta: {x: 100, y: 100}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!1 &5054239668136224504
|
||||
@@ -6478,7 +6478,7 @@ MonoBehaviour:
|
||||
m_ScaleFactor: 1
|
||||
m_ReferenceResolution: {x: 1920, y: 1080}
|
||||
m_ScreenMatchMode: 0
|
||||
m_MatchWidthOrHeight: 0.5
|
||||
m_MatchWidthOrHeight: 1
|
||||
m_PhysicalUnit: 3
|
||||
m_FallbackScreenDPI: 96
|
||||
m_DefaultSpriteDPI: 96
|
||||
@@ -10513,9 +10513,9 @@ RectTransform:
|
||||
m_Children: []
|
||||
m_Father: {fileID: 5634981615641515618}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: -825.01624, y: 0}
|
||||
m_AnchorMin: {x: 0, y: 0.5}
|
||||
m_AnchorMax: {x: 0, y: 0.5}
|
||||
m_AnchoredPosition: {x: 134.984, y: 0}
|
||||
m_SizeDelta: {x: 307.5676, y: 50}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &8992617676759434598
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// 全屏背景/遮罩自适应(Cover):保持原始宽高比,等比放大到覆盖整个根 Canvas,超出部分裁切。
|
||||
/// 过覆盖 => 任意宽高比都无缝铺满、且 sizeDelta 永不为 0;对纯色遮罩与艺术背景都正确、不失真。
|
||||
/// 仅调整 sizeDelta(表现层),不改锚点/位置/pivot,不影响居中布局与位移动画。
|
||||
/// 16:9 屏下结果等于设计尺寸,与原始逐像素一致。由 UIResolutionAutoFitter 自动附加。
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(RectTransform))]
|
||||
[DisallowMultipleComponent]
|
||||
public class UIBackgroundCover : MonoBehaviour
|
||||
{
|
||||
public float sourceAspect = 16f / 9f;
|
||||
|
||||
private RectTransform rt;
|
||||
private RectTransform coverTarget;
|
||||
private float lastW = -1f, lastH = -1f;
|
||||
private bool aspectResolved;
|
||||
// 重入锁:设置 sizeDelta 会同步触发 OnRectTransformDimensionsChange,
|
||||
// 若不拦截会无限递归导致 StackOverflow 崩溃。
|
||||
private bool _isFitting;
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
rt = GetComponent<RectTransform>();
|
||||
ResolveAspectFromInitialSize();
|
||||
ResolveTarget();
|
||||
Fit(true);
|
||||
}
|
||||
|
||||
private void LateUpdate()
|
||||
{
|
||||
// 每帧检测 coverTarget 实际尺寸变化(而非只看 Screen):Unity 布局延迟更新,
|
||||
// Screen 变化后 CanvasScaler 可能过几帧才把新尺寸写入 rootCanvas.rect。
|
||||
// Fit(false) 内部按 lastW/lastH 判重,尺寸未变时零开销,无需额外节流。
|
||||
if (coverTarget == null) ResolveTarget();
|
||||
if (coverTarget != null)
|
||||
{
|
||||
float pw = coverTarget.rect.width;
|
||||
float ph = coverTarget.rect.height;
|
||||
if (!Mathf.Approximately(pw, lastW) || !Mathf.Approximately(ph, lastH))
|
||||
{
|
||||
Fit(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>用元素初始 sizeDelta 推断原始宽高比,避免写死。</summary>
|
||||
private void ResolveAspectFromInitialSize()
|
||||
{
|
||||
if (aspectResolved) return;
|
||||
if (rt == null) rt = GetComponent<RectTransform>();
|
||||
Vector2 s = rt.sizeDelta;
|
||||
if (s.x > 1f && s.y > 1f) sourceAspect = s.x / s.y;
|
||||
aspectResolved = true;
|
||||
}
|
||||
|
||||
private void OnRectTransformDimensionsChange()
|
||||
{
|
||||
if (_isFitting) return; // 拦截自身写入触发的递归回调
|
||||
Fit(false);
|
||||
}
|
||||
|
||||
private void ResolveTarget()
|
||||
{
|
||||
Canvas canvas = GetComponentInParent<Canvas>();
|
||||
if (canvas != null)
|
||||
{
|
||||
Canvas root = canvas.rootCanvas != null ? canvas.rootCanvas : canvas;
|
||||
coverTarget = root.GetComponent<RectTransform>();
|
||||
}
|
||||
}
|
||||
|
||||
private void Fit(bool force)
|
||||
{
|
||||
if (_isFitting) return;
|
||||
if (rt == null) rt = GetComponent<RectTransform>();
|
||||
if (coverTarget == null) ResolveTarget();
|
||||
if (coverTarget == null || sourceAspect <= 0f) return;
|
||||
|
||||
float pw = coverTarget.rect.width;
|
||||
float ph = coverTarget.rect.height;
|
||||
if (pw <= 0f || ph <= 0f) return;
|
||||
if (!force && Mathf.Approximately(pw, lastW) && Mathf.Approximately(ph, lastH)) return;
|
||||
lastW = pw; lastH = ph;
|
||||
|
||||
float targetAspect = pw / ph;
|
||||
float w, h;
|
||||
if (targetAspect > sourceAspect) { w = pw; h = pw / sourceAspect; }
|
||||
else { h = ph; w = ph * sourceAspect; }
|
||||
|
||||
Vector2 newSize = new Vector2(w, h);
|
||||
if (rt.sizeDelta != newSize)
|
||||
{
|
||||
_isFitting = true;
|
||||
rt.sizeDelta = newSize;
|
||||
_isFitting = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2f19fe0a8ae05114bba0f36c5945c104
|
||||
@@ -0,0 +1,50 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
/// <summary>
|
||||
/// 让 CanvasScaler 的 matchWidthOrHeight 随屏幕宽高比动态切换,实现“设计区域永不裁切”(contain):
|
||||
/// - 屏幕比设计更宽(>16:9):锁高度(match=1),左右由背景 cover 填满,UI 不被竖向裁。
|
||||
/// - 屏幕比设计更窄(<16:9,如 16:10 / 4:3):锁宽度(match=0),上下由背景 cover 填满,UI 不被横向裁。
|
||||
/// 这样 1920x1080 设计范围内的所有 UI 在任意宽高比下都完整可见,仅“更宽松”的一侧留出由背景填充的余量。
|
||||
///
|
||||
/// 满足三要求:
|
||||
/// 1) 不改业务逻辑、不改对象引用;只调 CanvasScaler 的一个表现参数。
|
||||
/// 2) 16:9 屏下 match=1 与原状态一致,逐像素不变;只改缩放基准,不动任何锚点/位置/动画。
|
||||
/// 3) 窄屏不再裁切、宽屏铺满,配合背景 cover 始终填满屏幕。
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(CanvasScaler))]
|
||||
[DisallowMultipleComponent]
|
||||
public sealed class UICanvasMatchFitter : MonoBehaviour
|
||||
{
|
||||
private const float DesignAspect = 1920f / 1080f;
|
||||
|
||||
private CanvasScaler scaler;
|
||||
private float lastAspect = -1f;
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
scaler = GetComponent<CanvasScaler>();
|
||||
Apply();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
// 分辨率/窗口变化时才重算。
|
||||
float aspect = (float)Screen.width / Mathf.Max(1, Screen.height);
|
||||
if (Mathf.Abs(aspect - lastAspect) > 0.0001f) Apply();
|
||||
}
|
||||
|
||||
private void Apply()
|
||||
{
|
||||
if (scaler == null) scaler = GetComponent<CanvasScaler>();
|
||||
if (scaler == null) return;
|
||||
if (scaler.uiScaleMode != CanvasScaler.ScaleMode.ScaleWithScreenSize) return;
|
||||
|
||||
float aspect = (float)Screen.width / Mathf.Max(1, Screen.height);
|
||||
lastAspect = aspect;
|
||||
|
||||
scaler.screenMatchMode = CanvasScaler.ScreenMatchMode.MatchWidthOrHeight;
|
||||
// 更宽→锁高度(1);更窄→锁宽度(0)。等于始终匹配“更受限的一侧”,设计区域完整可见。
|
||||
scaler.matchWidthOrHeight = aspect >= DesignAspect ? 1f : 0f;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1a18755a72ae63a43b21a20c8f4489ca
|
||||
@@ -0,0 +1,114 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8745b948635db4f4187b2001878439b2
|
||||
@@ -0,0 +1,419 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
using UnityEngine.UI;
|
||||
|
||||
/// <summary>
|
||||
/// 全局分辨率自适应器:场景加载时自动扫描全屏背景类 Image(居中锚定 + 尺寸接近设计分辨率),
|
||||
/// 为其附加 UIBackgroundCover,使背景在任意宽高比(含超宽 32:9)下等比放大铺满、不留黑边、不失真。
|
||||
///
|
||||
/// 设计原则(对应三要求):
|
||||
/// 1) 零业务逻辑改动:不改脚本行为、不改对象引用,只在表现层调整背景 sizeDelta。
|
||||
/// 2) 不改锚点/pivot/位置:位移动画与居中布局完全不受影响;16:9 屏计算结果等于原设计尺寸。
|
||||
/// 3) 背景总是铺满屏幕。
|
||||
///
|
||||
/// 仅针对“全屏背景/遮罩”生效,普通按钮/面板/菜单等居中 UI 保持原样(它们本就该居中,不该拉伸)。
|
||||
/// </summary>
|
||||
public sealed class UIResolutionAutoFitter : MonoBehaviour
|
||||
{
|
||||
private const string BootstrapObjectName = "[UIResolutionAutoFitter]";
|
||||
private static UIResolutionAutoFitter _instance;
|
||||
|
||||
// 设计基准分辨率(与全工程 CanvasScaler referenceResolution 一致)。
|
||||
private const float DesignWidth = 1920f;
|
||||
private const float DesignHeight = 1080f;
|
||||
|
||||
// 判定为“全屏背景”的尺寸容差:宽高需分别接近设计分辨率或其 2 倍(部分背景做了 3840x2160)。
|
||||
private const float SizeTolerance = 96f;
|
||||
|
||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
|
||||
private static void Bootstrap()
|
||||
{
|
||||
if (_instance != null) return;
|
||||
GameObject go = new GameObject(BootstrapObjectName);
|
||||
_instance = go.AddComponent<UIResolutionAutoFitter>();
|
||||
DontDestroyOnLoad(go);
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
SceneManager.sceneLoaded += OnSceneLoaded;
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
SceneManager.sceneLoaded -= OnSceneLoaded;
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
// 处理启动时已经加载好的首个场景。
|
||||
StartCoroutine(FitAfterFrame());
|
||||
// 低频常驻补扫:覆盖“任意时刻才实例化/打开”的 prefab 全屏底图
|
||||
//(结算、歌曲详情、角色/设置面板等可能在会话中随时弹出)。
|
||||
// 每次只给尚未挂 UIBackgroundCover 的背景加组件,已处理的直接跳过,单次开销很低。
|
||||
StartCoroutine(PeriodicRescan());
|
||||
}
|
||||
|
||||
private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
|
||||
{
|
||||
// 新场景的 Canvas 是新实例,旧记录已失效,清空以便重新 setup。
|
||||
_setupDoneCanvases.Clear();
|
||||
_enemyFollowAttached = false;
|
||||
_trackStabilizerAttached = false;
|
||||
_cameraTunerAttached = false;
|
||||
StartCoroutine(FitAfterFrame());
|
||||
}
|
||||
|
||||
private IEnumerator FitAfterFrame()
|
||||
{
|
||||
// 等一帧让 Canvas/布局初始化完成,再扫描(避免 sizeDelta 尚未就绪)。
|
||||
yield return null;
|
||||
ScanAndFit();
|
||||
}
|
||||
|
||||
// 当前是否为 gameplay 场景(名字含 gameplay/gamePlay)。仅该场景启用 HUD 自动贴边。
|
||||
private bool _gameplayActive;
|
||||
|
||||
// 已完成一次性 setup(match 适配 + 边缘跟随)的 Canvas 集合,避免每次扫描重复全树遍历。
|
||||
// 切场景时清空(Canvas 会重建为新实例)。背景 cover 扫描不受此影响,仍每次执行。
|
||||
private readonly HashSet<Canvas> _setupDoneCanvases = new HashSet<Canvas>();
|
||||
|
||||
// 靠边判定阈值:元素中心距设计画布中心超过该值(参考像素)才自动贴边。
|
||||
private const float EdgeOffsetThreshold = 300f;
|
||||
|
||||
// 自动贴边时跳过的名字关键字:全屏背景/遮罩(由 cover 负责)与天然应居中的元素。
|
||||
private static readonly string[] AutoEdgeSkip =
|
||||
{
|
||||
"bg", "background", "mask", "black", "white", "遮罩", "背景",
|
||||
"pause", "已暂停", "start", "count", "321", "go"
|
||||
};
|
||||
|
||||
private void UpdateGameplayFlag()
|
||||
{
|
||||
string sn = SceneManager.GetActiveScene().name;
|
||||
_gameplayActive = !string.IsNullOrEmpty(sn)
|
||||
&& sn.ToLowerInvariant().Contains("gameplay");
|
||||
}
|
||||
|
||||
// 按横向偏移自动贴边(仅 gameplay HUD 顶层容器用)。
|
||||
private static void TryAutoEdgeFollowByOffset(RectTransform rt, float canvasWidth)
|
||||
{
|
||||
if (rt == null) return;
|
||||
if (rt.GetComponent<UIEdgeFollow>() != null) return;
|
||||
// 跳过:父级已有 UIEdgeFollow(子元素应相对父级布局,不独立贴边)。
|
||||
Transform parent = rt.parent;
|
||||
while (parent != null)
|
||||
{
|
||||
if (parent.GetComponent<UIEdgeFollow>() != null) return;
|
||||
if (parent.GetComponent<Canvas>() != null) break; // 到达 Canvas 根停止
|
||||
parent = parent.parent;
|
||||
}
|
||||
// 仅横向未拉伸 + 锚点接近中心。
|
||||
if (Mathf.Abs(rt.anchorMax.x - rt.anchorMin.x) > 0.01f) return;
|
||||
if (Mathf.Abs(rt.anchorMin.x - 0.5f) > 0.01f) return;
|
||||
string lower = rt.gameObject.name.ToLowerInvariant();
|
||||
for (int i = 0; i < AutoEdgeSkip.Length; i++)
|
||||
if (lower.Contains(AutoEdgeSkip[i])) return;
|
||||
// 全屏尺寸的排除(背景类)。
|
||||
if (rt.sizeDelta.x >= canvasWidth - 1f) return;
|
||||
// 计算元素中心的绝对X坐标(相对Canvas中心),用实际Canvas宽度而非硬编码1920。
|
||||
float absX = 0.5f * canvasWidth + rt.anchoredPosition.x;
|
||||
float canvasCenter = 0.5f * canvasWidth;
|
||||
if (Mathf.Abs(absX - canvasCenter) < EdgeOffsetThreshold) return;
|
||||
rt.gameObject.AddComponent<UIEdgeFollow>();
|
||||
}
|
||||
|
||||
// gameplay 场景:递归扫描 Canvas 下所有 RectTransform,对符合条件的元素附加 UIEdgeFollow。
|
||||
// 深度优先遍历,覆盖嵌套的角色面板/技能指示器等。每个元素独立判定,避免打散成组布局。
|
||||
private static void RecursiveTryAutoEdgeFollow(RectTransform canvasRt)
|
||||
{
|
||||
if (canvasRt == null) return;
|
||||
float canvasWidth = canvasRt.rect.width;
|
||||
if (canvasWidth <= 0f) canvasWidth = DesignWidth; // fallback
|
||||
RecursiveTryAutoEdgeFollowInternal(canvasRt, canvasWidth);
|
||||
}
|
||||
|
||||
private static void RecursiveTryAutoEdgeFollowInternal(RectTransform rt, float canvasWidth)
|
||||
{
|
||||
if (rt == null) return;
|
||||
// 先尝试给当前元素附加(若符合条件)。
|
||||
TryAutoEdgeFollowByOffset(rt, canvasWidth);
|
||||
// 再递归处理所有子级。
|
||||
for (int i = 0; i < rt.childCount; i++)
|
||||
{
|
||||
RectTransform child = rt.GetChild(i) as RectTransform;
|
||||
if (child != null) RecursiveTryAutoEdgeFollowInternal(child, canvasWidth);
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator PeriodicRescan()
|
||||
{
|
||||
// 0.5s 兼顾"面板打开后尽快铺满背景"与常驻开销。
|
||||
// 注意:单次扫描含 FindObjectsByType + 逐 Canvas 的 GetComponentsInChildren(会分配数组),
|
||||
// 属较重操作,间隔不宜过短(0.25s 会带来可感的 GC/CPU 尖峰)。ScanAndFit 内部对已处理的
|
||||
// Canvas(match/edgefollow)与已挂 cover 的 Image 都会跳过,避免重复重活。
|
||||
var wait = new WaitForSecondsRealtime(0.5f);
|
||||
while (true)
|
||||
{
|
||||
yield return wait;
|
||||
ScanAndFit();
|
||||
}
|
||||
}
|
||||
|
||||
private void ScanAndFit()
|
||||
{
|
||||
UpdateGameplayFlag();
|
||||
Canvas[] canvases = FindObjectsByType<Canvas>(FindObjectsInactive.Include, FindObjectsSortMode.None);
|
||||
for (int i = 0; i < canvases.Length; i++)
|
||||
{
|
||||
Canvas canvas = canvases[i];
|
||||
if (canvas == null) continue;
|
||||
|
||||
// 注意:不能按 Canvas 缓存跳过——像商店/邮件等面板是运行时挂到“已存在的场景 Canvas”下的,
|
||||
// 若跳过已见 Canvas,这些后加入的全屏背景就永远不会被 cover(会露出背后界面)。
|
||||
// 改为在下面对每个 Image 用 GetComponent<UIBackgroundCover> 判断是否已处理,成本很低。
|
||||
|
||||
// 跳过自管理布局/动画的特殊 UI(如全局通知 gNotice):它在 Awake 缓存自身位置并做
|
||||
// 弹出/上浮/缩放动画,若我们再改其 scaler/背景会造成实例化瞬间的跳动。名字或组件识别。
|
||||
if (ShouldSkipCanvas(canvas)) continue;
|
||||
|
||||
// 性能:match 适配器与边缘跟随都是"每个 Canvas 一次性附加",无需每次扫描都重做
|
||||
// (它们含 GetComponentsInChildren 全树遍历 + 递归,较重)。已处理过的 Canvas 直接跳过这部分,
|
||||
// 只让下面便宜的背景 cover 扫描继续每次执行(它需捕获运行时新挂入的面板底图)。
|
||||
bool canvasSetupDone = _setupDoneCanvases.Contains(canvas);
|
||||
if (!canvasSetupDone)
|
||||
{
|
||||
// 所有带 CanvasScaler 的 Canvas 统一用 UICanvasMatchFitter:宽屏锁高度、窄屏锁宽度,
|
||||
// 保证缩放系数正确、设计区域完整可见。绝不能宽屏锁宽度(会把 UI 放大数倍)。
|
||||
CanvasScaler scaler = canvas.GetComponent<CanvasScaler>();
|
||||
if (scaler != null && scaler.GetComponent<UICanvasMatchFitter>() == null)
|
||||
{
|
||||
scaler.gameObject.AddComponent<UICanvasMatchFitter>();
|
||||
}
|
||||
|
||||
// 结算是"整块按 1920 写死"的居中卡片,不做 HUD 边缘跟随(否则打散版式)。
|
||||
bool useEdgeFollowLayout = _gameplayActive && !IsSettlementCanvas(canvas);
|
||||
|
||||
// 按名对已知独立小部件/HUD 容器附加 UIEdgeFollow(白名单,避免打散成组布局)。
|
||||
RectTransform[] allRects = canvas.GetComponentsInChildren<RectTransform>(true);
|
||||
for (int r = 0; r < allRects.Length; r++)
|
||||
{
|
||||
RectTransform rt = allRects[r];
|
||||
if (rt == null) continue;
|
||||
if (EdgeFollowWhitelist.Contains(rt.gameObject.name))
|
||||
{
|
||||
TryAttachEdgeFollow(rt);
|
||||
}
|
||||
// 父级名+子级名精确定位:某些边角元素的偏移在通用名(如"Image")的子级上,
|
||||
// 直接把通用名加白名单会误伤同名对象,故用"父级名/子级名"精确命中。
|
||||
// 例:steamIcon 状态图标的偏移在其子物体 "Image"(左下角,-1751.5,-916)上。
|
||||
else if (rt.parent != null && ParentChildEdgeFollow.TryGetValue(rt.parent.name, out string childName)
|
||||
&& rt.gameObject.name == childName)
|
||||
{
|
||||
TryAttachEdgeFollow(rt);
|
||||
}
|
||||
}
|
||||
|
||||
// gameplay 非结算 HUD:递归对中心锚定、明显靠边的元素附加 UIEdgeFollow。
|
||||
if (useEdgeFollowLayout && scaler != null)
|
||||
{
|
||||
RectTransform canvasRt = canvas.transform as RectTransform;
|
||||
if (canvasRt != null) RecursiveTryAutoEdgeFollow(canvasRt);
|
||||
}
|
||||
|
||||
_setupDoneCanvases.Add(canvas);
|
||||
}
|
||||
|
||||
// 只处理根 Canvas 下的层级;子 Canvas 会被其根 Canvas 的遍历覆盖到。
|
||||
Image[] images = canvas.GetComponentsInChildren<Image>(true);
|
||||
for (int j = 0; j < images.Length; j++)
|
||||
{
|
||||
Image img = images[j];
|
||||
if (img == null) continue;
|
||||
RectTransform rt = img.rectTransform;
|
||||
if (rt == null) continue;
|
||||
if (!IsFullscreenBackground(rt)) continue;
|
||||
if (img.GetComponent<UIBackgroundCover>() == null)
|
||||
{
|
||||
// 统一用 Cover:等比放大到覆盖根 Canvas,过覆盖=无缝且永不为 0。
|
||||
// 之前的 Stretch(填满父级)对嵌套在小尺寸容器下的全屏图(如 quhuiImage 的父级仅100x100)
|
||||
// 会把 sizeDelta 拉成 0 导致图片消失,故弃用。
|
||||
img.gameObject.AddComponent<UIBackgroundCover>();
|
||||
}
|
||||
}
|
||||
|
||||
// RawImage(如 main_main 的视频背景 videoDisplay,用 RenderTexture 显示视频):
|
||||
// RawImage 与 Image 同继承 Graphic,UIBackgroundCover 只改 sizeDelta 对二者一致;
|
||||
// 其 uvRect=(0,0,1,1) 铺满纹理、RT 为 16:9,等比 cover 不会拉伸变形。
|
||||
// GetComponentsInChildren<Image> 抓不到 RawImage,故单独扫一遍。
|
||||
RawImage[] rawImages = canvas.GetComponentsInChildren<RawImage>(true);
|
||||
for (int j = 0; j < rawImages.Length; j++)
|
||||
{
|
||||
RawImage raw = rawImages[j];
|
||||
if (raw == null) continue;
|
||||
RectTransform rt = raw.rectTransform;
|
||||
if (rt == null) continue;
|
||||
if (!IsFullscreenBackground(rt)) continue;
|
||||
if (raw.GetComponent<UIBackgroundCover>() == null)
|
||||
{
|
||||
raw.gameObject.AddComponent<UIBackgroundCover>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// gameplay 世界空间背景(SpriteRenderer,如 bg_replace):由相机渲染、非 UI,
|
||||
// 超宽屏下相机视野扩大会露出背景边缘外的黑边。扫描低 sortingOrder 的全屏 Sprite 背景,
|
||||
// 附加 UISpriteBackgroundCover 按宽高比放大 localScale 覆盖。仅 gameplay 场景处理。
|
||||
if (_gameplayActive)
|
||||
{
|
||||
ScanSpriteBackgrounds();
|
||||
ScanEnemyFollow();
|
||||
ScanTrackRatioStabilizer();
|
||||
}
|
||||
}
|
||||
|
||||
// gameplay 主相机:附加 TrackScreenRatioStabilizer,按宽高比补偿垂直FOV,
|
||||
// 使轨道横向屏占比在不同分辨率下大致恒定(避免超宽屏轨道太窄误触)。仅附加一次。
|
||||
private void ScanTrackRatioStabilizer()
|
||||
{
|
||||
if (!_trackStabilizerAttached)
|
||||
{
|
||||
Camera cam = Camera.main;
|
||||
if (cam != null)
|
||||
{
|
||||
if (cam.GetComponent<TrackScreenRatioStabilizer>() == null)
|
||||
cam.gameObject.AddComponent<TrackScreenRatioStabilizer>();
|
||||
_trackStabilizerAttached = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 相机分辨率微调器:挂到 effectEventController 单例对象上(它负责相机 pivot,微调经其安全施加)。
|
||||
// 独立于上面的标志重试——effectEventController 可能稍晚才初始化。
|
||||
if (!_cameraTunerAttached)
|
||||
{
|
||||
var eff = effectEventController.Instance;
|
||||
if (eff != null)
|
||||
{
|
||||
if (eff.GetComponent<CameraResolutionTuner>() == null)
|
||||
eff.gameObject.AddComponent<CameraResolutionTuner>();
|
||||
_cameraTunerAttached = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
private bool _trackStabilizerAttached;
|
||||
private bool _cameraTunerAttached;
|
||||
|
||||
// gameplay 敌人容器(普通 Transform,非 RectTransform):让其跟随屏幕左缘。
|
||||
// 敌人显示对象是 enemyObject_toWiggle,其定位父级是名为 empty 的普通 Transform,
|
||||
// 挂在 enemyHealthUI 的 ScreenSpaceCamera Canvas 下。给该定位父级附加 UITransformEdgeFollow(左)。
|
||||
private void ScanEnemyFollow()
|
||||
{
|
||||
if (_enemyFollowAttached) return; // 已附加则不再每次 GameObject.Find(字符串查找较贵)。
|
||||
GameObject enemyObj = GameObject.Find("enemyObject_toWiggle");
|
||||
if (enemyObj == null) return; // Find 只找 active 对象;敌人未激活时下次补扫再处理。
|
||||
// 取其定位父级 empty(承载横向偏移的那层);找不到就用自身。
|
||||
Transform target = enemyObj.transform.parent != null ? enemyObj.transform.parent : enemyObj.transform;
|
||||
if (target.GetComponent<UITransformEdgeFollow>() == null)
|
||||
target.gameObject.AddComponent<UITransformEdgeFollow>();
|
||||
_enemyFollowAttached = true;
|
||||
}
|
||||
private bool _enemyFollowAttached;
|
||||
|
||||
// 扫描并处理世界空间 Sprite 背景(覆盖 gameplay 相机渲染的底图)。
|
||||
private static void ScanSpriteBackgrounds()
|
||||
{
|
||||
SpriteRenderer[] sprites = FindObjectsByType<SpriteRenderer>(FindObjectsInactive.Include, FindObjectsSortMode.None);
|
||||
for (int i = 0; i < sprites.Length; i++)
|
||||
{
|
||||
SpriteRenderer sr = sprites[i];
|
||||
if (sr == null) continue;
|
||||
// 仅处理"背景类":sortingOrder 很低(<= -100,如 bg_replace 的 -999)。
|
||||
// 这类是铺底背景,需覆盖;普通游戏 Sprite(音符/角色/特效)不动。
|
||||
if (sr.sortingOrder > -100) continue;
|
||||
if (sr.GetComponent<UISpriteBackgroundCover>() != null) continue;
|
||||
sr.gameObject.AddComponent<UISpriteBackgroundCover>();
|
||||
}
|
||||
}
|
||||
|
||||
// 已知“独立、不属于任何布局组”的边角小部件/HUD 容器精确名单。
|
||||
// 仅这些会被贴边跟随;功能界面的成组内容一律不动,避免打散版式。
|
||||
private static readonly HashSet<string> EdgeFollowWhitelist = new HashSet<string>
|
||||
{
|
||||
// 主界面:世界聊天入口按钮(独立浮标,中心锚定+右偏移)。
|
||||
"chat",
|
||||
// Main_main 主菜单边角独立元素:跟随屏幕边缘。
|
||||
// 不含 logo/game_Logo/selectServer/anyButton(选服/进入按钮与logo,保持设计位置)。
|
||||
"exit", "help", "supporting", "steamIcon", "uid", "fdLogo", "logo_fdLogo",
|
||||
// 版本号文本(左上角,挂在 logo 下)。logo 保持居中不动,故 bbt 需独立贴左缘。
|
||||
// 名字在 Main_main 唯一,直接加名不会误伤。
|
||||
"bbt",
|
||||
};
|
||||
|
||||
// 父级名 → 子级名:偏移在通用名子级上的边角元素(直接把通用名加白名单会误伤同名对象)。
|
||||
// steamIcon(父)本身偏移为0,真正贴左下角的是它名为 "Image" 的子级(-1751.5,-916)。
|
||||
private static readonly Dictionary<string, string> ParentChildEdgeFollow = new Dictionary<string, string>
|
||||
{
|
||||
{ "steamIcon", "Image" },
|
||||
};
|
||||
|
||||
private static void TryAttachEdgeFollow(RectTransform rt)
|
||||
{
|
||||
if (rt == null) return;
|
||||
if (rt.GetComponent<UIEdgeFollow>() != 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;
|
||||
rt.gameObject.AddComponent<UIEdgeFollow>();
|
||||
}
|
||||
|
||||
// 判定 Canvas 是否属于结算(settlement)界面:祖先链里有名为 settlement 的对象。
|
||||
// 结算是整块按 1920 写死的居中布局(靠 match=1 锁高度天然居中),不做 HUD 边缘跟随以免打散版式。
|
||||
private static bool IsSettlementCanvas(Canvas canvas)
|
||||
{
|
||||
if (canvas == null) return false;
|
||||
Transform t = canvas.transform;
|
||||
while (t != null)
|
||||
{
|
||||
if (t.gameObject.name == "settlement") return true;
|
||||
t = t.parent;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 跳过自管理的特殊 UI 根:按名匹配(gNotice 全局通知实例)。
|
||||
private static bool ShouldSkipCanvas(Canvas canvas)
|
||||
{
|
||||
if (canvas == null) return false;
|
||||
string n = canvas.gameObject.name;
|
||||
// gNotice 实例根名 "GlobalNoticeSystem_SceneInstance" 及其 prefab 名。
|
||||
if (n.Contains("GlobalNoticeSystem")) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判定是否为全屏背景:居中锚定(非拉伸)且尺寸接近设计分辨率(或其 2 倍)。
|
||||
/// 排除已经是全拉伸(0-1)的(那类天然铺满,无需处理)与尺寸偏小的普通 UI。
|
||||
/// </summary>
|
||||
private static bool IsFullscreenBackground(RectTransform rt)
|
||||
{
|
||||
Vector2 aMin = rt.anchorMin;
|
||||
Vector2 aMax = rt.anchorMax;
|
||||
|
||||
// 已全拉伸:无需处理。
|
||||
bool stretched = Mathf.Abs(aMax.x - aMin.x) > 0.99f && Mathf.Abs(aMax.y - aMin.y) > 0.99f;
|
||||
if (stretched) return false;
|
||||
|
||||
// 仅处理居中/固定锚定(锚点宽高跨度接近 0)。
|
||||
bool fixedAnchor = Mathf.Abs(aMax.x - aMin.x) < 0.01f && Mathf.Abs(aMax.y - aMin.y) < 0.01f;
|
||||
if (!fixedAnchor) return false;
|
||||
|
||||
Vector2 size = rt.sizeDelta;
|
||||
bool matchDesign = Mathf.Abs(size.x - DesignWidth) < SizeTolerance
|
||||
&& Mathf.Abs(size.y - DesignHeight) < SizeTolerance;
|
||||
bool matchDouble = Mathf.Abs(size.x - DesignWidth * 2f) < SizeTolerance * 2f
|
||||
&& Mathf.Abs(size.y - DesignHeight * 2f) < SizeTolerance * 2f;
|
||||
return matchDesign || matchDouble;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5ab6a774083052748a8da59390e563dc
|
||||
@@ -0,0 +1,62 @@
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// gameplay 世界空间背景(SpriteRenderer,如 bg_replace)自适应覆盖。
|
||||
/// 该背景由透视相机渲染、带旋转/缩放,尺寸按 16:9 设计。超宽屏(21:9/32:9)下相机
|
||||
/// 水平视野扩大会看到背景边缘之外的黑边,故按屏幕宽高比等比放大其 localScale 使其覆盖。
|
||||
///
|
||||
/// 仅调整 localScale(表现层),不改旋转/位置/材质/引用,不影响业务逻辑与动画。
|
||||
/// 16:9 屏下缩放系数=1,与原设计逐像素一致。由 UIResolutionAutoFitter 自动附加。
|
||||
///
|
||||
/// 满足三要求:1) 不改业务逻辑/引用;2) 16:9 缩放=1、不动旋转位置;3) 超宽屏铺满。
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(SpriteRenderer))]
|
||||
[DisallowMultipleComponent]
|
||||
public sealed class UISpriteBackgroundCover : MonoBehaviour
|
||||
{
|
||||
private const float DesignAspect = 16f / 9f;
|
||||
|
||||
private Transform tf;
|
||||
private Vector3 baseScale; // 初始设计缩放(16:9 基准),永不变
|
||||
private bool baseCaptured;
|
||||
private int lastW = -1, lastH = -1;
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
tf = transform;
|
||||
CaptureBaseScale();
|
||||
Apply();
|
||||
}
|
||||
|
||||
private void CaptureBaseScale()
|
||||
{
|
||||
if (baseCaptured) return;
|
||||
baseScale = tf.localScale;
|
||||
baseCaptured = true;
|
||||
}
|
||||
|
||||
private void LateUpdate()
|
||||
{
|
||||
if (Screen.width != lastW || Screen.height != lastH)
|
||||
{
|
||||
Apply();
|
||||
}
|
||||
}
|
||||
|
||||
private void Apply()
|
||||
{
|
||||
if (!baseCaptured) CaptureBaseScale();
|
||||
lastW = Screen.width;
|
||||
lastH = Screen.height;
|
||||
|
||||
float aspect = (float)Screen.width / Mathf.Max(1, Screen.height);
|
||||
// 更宽的屏:水平视野变宽,需按 aspect/DesignAspect 放大以覆盖两侧。
|
||||
// 更窄的屏:垂直视野相对更高,需按 DesignAspect/aspect 放大以覆盖上下。
|
||||
// 取二者较大值(cover),保证任一方向都不露黑边。16:9 时系数=1。
|
||||
float widthFactor = aspect > DesignAspect ? aspect / DesignAspect : 1f;
|
||||
float heightFactor = aspect < DesignAspect ? DesignAspect / aspect : 1f;
|
||||
float factor = Mathf.Max(widthFactor, heightFactor);
|
||||
|
||||
tf.localScale = baseScale * factor;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d9611e36b8928ad4183b10e194cfa053
|
||||
@@ -0,0 +1,104 @@
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// 让一个"普通 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) 跟随边缘。
|
||||
/// </summary>
|
||||
[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<Canvas>();
|
||||
if (c != null)
|
||||
{
|
||||
Canvas root = c.rootCanvas != null ? c.rootCanvas : c;
|
||||
rootRt = root.GetComponent<RectTransform>();
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2faaf8d3e94085a4784340c9bf1db125
|
||||
@@ -409,7 +409,9 @@ public class UnimplementedFeaturePrompt : MonoBehaviour
|
||||
{
|
||||
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
|
||||
scaler.referenceResolution = new Vector2(1920f, 1080f);
|
||||
scaler.matchWidthOrHeight = 0.5f;
|
||||
// 锁高度缩放:分辨率跟随屏幕后,非 16:9 屏也能保证各 UI 层一致缩放、不错位。
|
||||
scaler.screenMatchMode = CanvasScaler.ScreenMatchMode.MatchWidthOrHeight;
|
||||
scaler.matchWidthOrHeight = 1f;
|
||||
}
|
||||
|
||||
return canvas;
|
||||
|
||||
Reference in New Issue
Block a user