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 } }