using System.Collections.Generic; using UnityEngine; /// /// 纵向循环滚动背景(所有直接子物体按顺序当作背景图,第一个在最上面)。 /// /// 适配不同机型的做法:背景大小仍由子物体上的 BgFit 按屏幕铺满,这里不自己算缩放, /// 而是在“第一帧”(BgFit 缩放完成后)读出背景的真实世界高度,用它来定间距: /// 间距 = 真实图高 - 2像素 /// 间距和图高永远同一个来源,所以不管什么分辨率/宽高比,接缝都不会张开露黑。 /// 另外位置按整像素对齐、底部留余量并跟随相机,运行中不会出现黑线。 /// public class BGManager : MonoBehaviour { [Header("滚动")] [Tooltip("背景向下滚动的速度(世界单位/秒)")] public float speed = 3f; [Header("拼接(消除黑线)")] [Tooltip("相邻两张背景的重叠像素:留一点重叠,接缝就不会因取整/采样露黑")] public float overlapPixels = 2f; [Tooltip("背景底部相对屏幕下边缘多留的比例,避免镜头抖动时露黑")] public float bottomMargin = 0.05f; [Tooltip("位置按整像素对齐:接缝只会落在像素边界上,画面更稳(参考工程的背景也是这样)")] public bool snapToPixel = true; // —— 运行时数据 —— public float height; // 单张背景缩放后的真实世界高度 private float spacing; // 相邻背景间距(整像素对齐)= 图高 - 重叠 private float unitsPerPixel; // 1 个屏幕像素等于多少世界单位 private float marginWorld; // 底部余量(世界单位,整像素对齐) private float offset; // 累计滚动距离 private bool layoutReady; private readonly List tiles = new List(); private readonly List pivotToBottom = new List(); // pivot 到底边的世界距离 private readonly List pivotToLeft = new List(); // pivot 到左边的世界距离 void Start() { tiles.Clear(); foreach (Transform child in transform) tiles.Add(child); } /// 第一帧执行:此时 BgFit 已把每张背景缩放到铺满屏幕,读出来的高度才是最终值。 /// 记录每张背景 pivot 到底边/左边的距离,摆放时按底边、左边对齐, /// 这样以后换图(pivot 不是左下角)也不会错位。 void CacheLayout() { Camera cam = Camera.main; if (cam == null) return; float visibleH = cam.orthographicSize * 2f; // 相机可见高度(随屏幕自适应) unitsPerPixel = visibleH / Mathf.Max(Screen.height, 1); float minTileH = float.MaxValue; pivotToBottom.Clear(); pivotToLeft.Clear(); foreach (Transform t in tiles) { float minY = 0f; float minX = 0f; SpriteRenderer sr = t.GetComponent(); if (sr != null && sr.sprite != null) { Vector3 scale = t.lossyScale; minTileH = Mathf.Min(minTileH, sr.bounds.size.y); // 真实图高:间距以它为准 minY = sr.sprite.bounds.min.y * scale.y; minX = sr.sprite.bounds.min.x * scale.x; } pivotToBottom.Add(minY); pivotToLeft.Add(minX); } height = minTileH == float.MaxValue ? visibleH : minTileH; // 间距 = 图高 - 重叠,并按整像素取整(保证每张背景的边都在像素线上) float overlapWorld = overlapPixels * unitsPerPixel; float spacingPixels = Mathf.Round((height - overlapWorld) / unitsPerPixel); spacing = Mathf.Max(spacingPixels, 1f) * unitsPerPixel; marginWorld = Mathf.Round(bottomMargin * Screen.height) * unitsPerPixel; layoutReady = true; } void Update() { if (tiles.Count == 0) return; if (!layoutReady) CacheLayout(); if (!layoutReady || spacing <= 0f) return; Camera cam = Camera.main; if (cam == null) return; offset += speed * Time.deltaTime; // 当前格子内的位移(0 ~ spacing)与已经滚过的格子数。 // 位移取整像素后所有背景共用同一个值,间距不会被各自取整破坏。 float phase = Snap(offset - Mathf.Floor(offset / spacing) * spacing); int step = Mathf.FloorToInt(offset / spacing); // 基准点:屏幕左下角,再往下留 bottomMargin,跟随相机(镜头震动也不会露边) float leftX = Snap(cam.transform.position.x - cam.orthographicSize * cam.aspect); float bottomY = Snap(cam.transform.position.y - cam.orthographicSize) - marginWorld; int n = tiles.Count; for (int i = 0; i < n; i++) { // 第 1 个子物体在最上面,初始占第 (n-1-i) 格;滚出屏幕后回到最上面 int slot = (n - 1 - i) - step; slot = ((slot % n) + n) % n; Vector3 p = tiles[i].position; p.x = leftX + pivotToLeft[i]; p.y = bottomY + pivotToBottom[i] + slot * spacing - phase; tiles[i].position = p; } } /// 世界坐标吸附到屏幕像素 float Snap(float world) { if (!snapToPixel || unitsPerPixel <= 0f) return world; return Mathf.Round(world / unitsPerPixel) * unitsPerPixel; } }