using UnityEngine;
using Unity.Cinemachine;
namespace BaseGames.Camera
{
///
/// 相机像素对齐(Pixel-perfect Snapping)。
///
/// 每帧在 CinemachineBrain LateUpdate 完成后,将 Unity 主相机的世界坐标
/// 四舍五入到最近的像素网格,消除像素艺术精灵的亚像素抖动(sub-pixel jitter)。
///
///
/// 挂载位置:Persistent 场景中与 同一 GameObject([Camera] 节点)。
///
///
/// 执行顺序:+1000,确保在 CinemachineBrain(默认 -1000)的 LateUpdate 之后运行。
/// 直接修改 Unity Camera 的 transform.position,不影响 Cinemachine 内部状态。
///
///
/// 混合进行中时可选择暂停对齐( = true),
/// 避免两台 VCam 插值结果在整数像素间跳变,产生阶梯感。
///
///
[DefaultExecutionOrder(1000)]
[AddComponentMenu("BaseGames/Camera/Camera Pixel Snapper")]
[RequireComponent(typeof(UnityEngine.Camera))]
public class CameraPixelSnapper : MonoBehaviour
{
[Tooltip("每世界单位的像素数(PPU)。须与项目精灵的 Pixels Per Unit 设置一致。\n" +
"0 = 禁用像素对齐。常用值:16(粗像素)、32(标准)、100(高分辨率)。")]
[Min(0f)]
[SerializeField] private float _pixelsPerUnit = 16f;
[Tooltip("相机混合动画(Blend)进行中时暂停像素对齐,待混合结束后恢复。\n" +
"开启时混合动画更平滑(无阶梯感);关闭时混合期间也精确对齐但可能有轻微跳帧。\n" +
"推荐保持开启。")]
[SerializeField] private bool _disableDuringBlend = true;
[SerializeField] private CinemachineBrain _brain;
private UnityEngine.Camera _camera;
private CinemachineCamera _cachedVCam;
private CinemachineConfiner3D _cachedConfiner;
private void Reset()
{
_brain = GetComponent();
_camera = GetComponent();
}
private void Awake()
{
_camera = GetComponent();
}
private void LateUpdate()
{
if (_pixelsPerUnit <= 0f || _camera == null) return;
// 混合进行中时跳过,避免插值位置在像素网格间产生阶梯感
if (_disableDuringBlend && _brain != null && _brain.IsBlending) return;
float ppu = _pixelsPerUnit;
Vector3 pos = _camera.transform.position;
pos.x = Mathf.Round(pos.x * ppu) / ppu;
pos.y = Mathf.Round(pos.y * ppu) / ppu;
// Z 轴保持不变(透视深度不需要对齐)
// 像素取整可能将相机推出 Confiner 边界(最多 0.5/PPU 的微小超出)。
// 对取整后的位置施加限位矩形钳制,确保不超出当前激活区域的 Confiner 边界。
if (TryGetActiveConfinerBounds(out Bounds confinerBounds))
{
pos.x = Mathf.Clamp(pos.x, confinerBounds.min.x, confinerBounds.max.x);
pos.y = Mathf.Clamp(pos.y, confinerBounds.min.y, confinerBounds.max.y);
}
_camera.transform.position = pos;
}
///
/// 获取当前活跃 VCam 的 CinemachineConfiner3D 边界盒(世界空间 AABB)。
/// 缓存上次查询的 VCam 实例;仅在活跃 VCam 发生切换时重新调用 GetComponent,
/// 避免每帧 GetComponent 开销。
///
private bool TryGetActiveConfinerBounds(out Bounds bounds)
{
bounds = default;
if (_brain == null) return false;
var vcam = _brain.ActiveVirtualCamera as CinemachineCamera;
if (vcam == null) return false;
// 只在活跃 VCam 切换时刷新缓存
if (!ReferenceEquals(vcam, _cachedVCam))
{
_cachedVCam = vcam;
_cachedConfiner = vcam.GetComponent();
}
if (_cachedConfiner == null || !_cachedConfiner.IsValid) return false;
bounds = _cachedConfiner.BoundingVolume.bounds;
return true;
}
#if UNITY_EDITOR
/// 编辑器模式下实时预览对齐效果。
private void OnDrawGizmosSelected()
{
if (_pixelsPerUnit <= 0f || _camera == null) return;
float ppu = _pixelsPerUnit;
float cellW = 1f / ppu;
// 在 Scene 视图中围绕相机位置绘制 5×5 像素网格示意(仅辅助调试用)
Gizmos.color = new Color(0.6f, 1f, 0.6f, 0.4f);
Vector3 origin = _camera.transform.position;
origin.x = Mathf.Floor(origin.x / cellW) * cellW;
origin.y = Mathf.Floor(origin.y / cellW) * cellW;
for (int ix = -2; ix <= 3; ix++)
for (int iy = -2; iy <= 3; iy++)
Gizmos.DrawWireCube(
new Vector3(origin.x + ix * cellW, origin.y + iy * cellW, 0f),
new Vector3(cellW, cellW, 0.01f));
}
#endif
}
}