264
Assets/Scripts/PixelPerfectBorderBackground.cs
Normal file
264
Assets/Scripts/PixelPerfectBorderBackground.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user