Files
5.24BuYu/Assets/MyGame/Scripts/Sprites/BGManager.cs
T
2026-10-17 03:01:15 +08:00

138 lines
6.1 KiB
C#

using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 纵向循环滚动背景(所有直接子物体按顺序当作背景图,第一个在最上面)。
///
/// 适配不同机型的做法:背景大小仍由子物体上的 BgFit 按屏幕铺满,这里不自己算缩放,
/// 而是在“第一帧”(BgFit 缩放完成后)读出背景的真实世界高度,用它来定间距:
/// 间距 = 真实图高 - 2像素
/// 间距和图高永远同一个来源,所以不管什么分辨率/宽高比,接缝都不会张开露黑。
/// 另外位置按整像素对齐、底部留余量并跟随相机,运行中不会出现黑线。
/// </summary>
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 bool camWarned;
private readonly List<Transform> tiles = new List<Transform>();
private readonly List<float> pivotToBottom = new List<float>(); // pivot 到底边的世界距离
private readonly List<float> pivotToLeft = new List<float>(); // 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<SpriteRenderer>();
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 还没累加),表现就是“背景在、但不动”。
// 本场景自己没有相机,完全依赖别的场景里被标成 MainCamera 的相机;相机被换掉/禁用就会走到这里。
// 打一次日志,避免这种问题被误判成“脚本坏了”。
if (!camWarned)
{
camWarned = true;
Debug.LogError("[BGManager] 拿不到 Camera.main(当前没有已启用且 Tag=MainCamera 的相机),背景不会滚动!");
}
return;
}
camWarned = false;
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;
}
}