添加固定比例(1920x1080)

Signed-off-by: TRADER_FOER <lhf190@outlook.com>
This commit is contained in:
2026-07-14 19:49:56 +08:00
parent 9ce3173697
commit c16c2480ff
6 changed files with 20498 additions and 18602 deletions

View File

@@ -0,0 +1,264 @@
using Ichni;
using UnityEngine;
using UnityEngine.UI;
/// <summary>
/// 固定宽高比 + 自动填充黑边脚本
/// 根据目标宽高比targetWidth : targetHeight自动设置 Camera 的 Viewport
/// 并用四张 RawImage 填充视口外部的黑边Pillarbox / Letterbox
/// 支持在编辑器模式Editor下实时预览。
/// </summary>
[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;
}
}
/// <summary>
/// 根据目标宽高比计算并应用 Camera 的 Viewport Rect。
/// 开启 enableFixedAspect 时自动计算 Letterbox / Pillarbox 布局,
/// 关闭时恢复 Camera 全屏。
/// </summary>
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;
}
}