using Ichni; using UnityEngine; using UnityEngine.UI; /// /// 固定宽高比 + 自动填充黑边脚本 /// 根据目标宽高比(targetWidth : targetHeight)自动设置 Camera 的 Viewport, /// 并用四张 RawImage 填充视口外部的黑边(Pillarbox / Letterbox)。 /// 支持在编辑器模式(Editor)下实时预览。 /// [ExecuteAlways] [RequireComponent(typeof(RectTransform))] public class PixelPerfectBorderBackground : MonoBehaviour { [Header("Source")] public Camera mainCamera; [Header("Fixed Aspect Ratio")] [Tooltip("启用固定宽高比,自动设置 Camera 的 Viewport")] public bool enableFixedAspect = true; [Tooltip("目标宽度(像素,仅用于计算比例)")] public int targetWidth = 1920; [Tooltip("目标高度(像素,仅用于计算比例)")] public int targetHeight = 1080; [Header("Border Images")] public RawImage left; public RawImage right; public RawImage top; public RawImage bottom; [Header("Behaviour")] public bool updateEveryFrame = true; public bool disableRaycastTargets = true; [Header("Debug")] [SerializeField] private Rect lastGamePixelRect; [SerializeField] private Vector2Int lastScreenSize; private int _lastScreenWidth = -1; private int _lastScreenHeight = -1; private Rect _lastPixelRect; private bool _forceRefresh = true; private void Awake() { ApplyRaycastState(); } private void OnEnable() { ApplyRaycastState(); _forceRefresh = true; // 在编辑器下,注册屏幕缩放或重绘的事件,确保能够实时刷新 #if UNITY_EDITOR UnityEditor.EditorApplication.update += EditorUpdate; #endif } private void OnDisable() { #if UNITY_EDITOR UnityEditor.EditorApplication.update -= EditorUpdate; #endif } #if UNITY_EDITOR private void EditorUpdate() { if (!Application.isPlaying && updateEveryFrame) { _forceRefresh = true; LateUpdate(); } } #endif private void OnValidate() { _forceRefresh = true; } private void LateUpdate() { if (updateEveryFrame || _forceRefresh) { ApplyRaycastState(); UpdateBars(_forceRefresh); _forceRefresh = false; } } /// /// 根据目标宽高比计算并应用 Camera 的 Viewport Rect。 /// 开启 enableFixedAspect 时自动计算 Letterbox / Pillarbox 布局, /// 关闭时恢复 Camera 全屏。 /// private void ApplyFixedAspect(Camera cam) { if (!enableFixedAspect || targetWidth <= 0 || targetHeight <= 0) { // 未启用固定比例 → 恢复全屏 Viewport cam.rect = new Rect(0f, 0f, 1f, 1f); return; } float screenW = Mathf.Max(1f, Screen.width); float screenH = Mathf.Max(1f, Screen.height); float screenAspect = screenW / screenH; float targetAspect = (float)targetWidth / targetHeight; Rect viewport; if (screenAspect > targetAspect) { // 屏幕更宽 → 左右黑边(Pillarbox) // 让 Viewport 高度占满,宽度按比例缩小居中 float viewWidth = targetAspect / screenAspect; float viewX = (1f - viewWidth) * 0.5f; viewport = new Rect(viewX, 0f, viewWidth, 1f); } else { // 屏幕更高 → 上下黑边(Letterbox) // 让 Viewport 宽度占满,高度按比例缩小居中 float viewHeight = screenAspect / targetAspect; float viewY = (1f - viewHeight) * 0.5f; viewport = new Rect(0f, viewY, 1f, viewHeight); } cam.rect = viewport; } public void UpdateBars(bool force) { Camera cam = ResolveCamera(); if (cam == null) { SetEnabled(left, false); SetEnabled(right, false); SetEnabled(top, false); SetEnabled(bottom, false); return; } // ---- 1. 应用固定宽高比(设置 Camera Viewport) ---- ApplyFixedAspect(cam); // ---- 2. 原有的黑边填充逻辑 ---- // 获取当前分辨率(在编辑器下代表 Game 窗口分辨率) int screenWidth = Mathf.Max(1, Screen.width); int screenHeight = Mathf.Max(1, Screen.height); // cam.pixelRect 已经根据相机的 Viewport Rect 自动换算成了实际像素区域 Rect pixelRect = ClampPixelRect(cam.pixelRect, screenWidth, screenHeight); if (!force && screenWidth == _lastScreenWidth && screenHeight == _lastScreenHeight && Approximately(pixelRect, _lastPixelRect)) { return; } _lastScreenWidth = screenWidth; _lastScreenHeight = screenHeight; _lastPixelRect = pixelRect; lastScreenSize = new Vector2Int(screenWidth, screenHeight); lastGamePixelRect = pixelRect; // 计算并设置四个方向的遮罩大小与 UV SetSlice(left, 0f, 0f, pixelRect.xMin, screenHeight, screenWidth, screenHeight); SetSlice(right, pixelRect.xMax, 0f, screenWidth, screenHeight, screenWidth, screenHeight); SetSlice(bottom, pixelRect.xMin, 0f, pixelRect.xMax, pixelRect.yMin, screenWidth, screenHeight); SetSlice(top, pixelRect.xMin, pixelRect.yMax, pixelRect.xMax, screenHeight, screenWidth, screenHeight); } private Camera ResolveCamera() { if (mainCamera != null) return mainCamera; if (EditorManager.instance != null) return EditorManager.instance.cameraManager.currentCamera; return Camera.main; } private void SetSlice(RawImage image, float xMin, float yMin, float xMax, float yMax, int screenWidth, int screenHeight) { if (image == null) return; float width = Mathf.Max(0f, xMax - xMin); float height = Mathf.Max(0f, yMax - yMin); // 允许一点点像素误差 bool visible = width > 0.1f && height > 0.1f; SetEnabled(image, visible); if (!visible) return; RectTransform rect = image.rectTransform; Vector2 parentSize = GetParentSize(rect, screenWidth, screenHeight); Vector2 min = new Vector2(xMin / screenWidth, yMin / screenHeight); Vector2 max = new Vector2(xMax / screenWidth, yMax / screenHeight); rect.anchorMin = Vector2.zero; rect.anchorMax = Vector2.zero; rect.offsetMin = new Vector2(min.x * parentSize.x, min.y * parentSize.y); rect.offsetMax = new Vector2(max.x * parentSize.x, max.y * parentSize.y); rect.pivot = new Vector2(0.5f, 0.5f); // 如果你用的是纯黑图片,UV 没影响;如果是背景图,这行代码能保证四张图拼在一起是无缝完整的 image.uvRect = new Rect(min.x, min.y, max.x - min.x, max.y - min.y); } private Vector2 GetParentSize(RectTransform rect, int screenWidth, int screenHeight) { if (rect != null && rect.parent is RectTransform parent) { Rect parentRect = parent.rect; if (parentRect.width > 0.001f && parentRect.height > 0.001f) return parentRect.size; } return new Vector2(screenWidth, screenHeight); } private void SetEnabled(RawImage image, bool enabled) { if (image != null && image.enabled != enabled) image.enabled = enabled; } private Rect ClampPixelRect(Rect rect, int screenWidth, int screenHeight) { float xMin = Mathf.Clamp(rect.xMin, 0f, screenWidth); float xMax = Mathf.Clamp(rect.xMax, xMin, screenWidth); float yMin = Mathf.Clamp(rect.yMin, 0f, screenHeight); float yMax = Mathf.Clamp(rect.yMax, yMin, screenHeight); return Rect.MinMaxRect(xMin, yMin, xMax, yMax); } private bool Approximately(Rect a, Rect b) { return Mathf.Approximately(a.x, b.x) && Mathf.Approximately(a.y, b.y) && Mathf.Approximately(a.width, b.width) && Mathf.Approximately(a.height, b.height); } private void ApplyRaycastState() { if (!disableRaycastTargets) return; SetRaycastTarget(left, false); SetRaycastTarget(right, false); SetRaycastTarget(top, false); SetRaycastTarget(bottom, false); } private void SetRaycastTarget(RawImage image, bool value) { if (image != null && image.raycastTarget != value) image.raycastTarget = value; } }