138 lines
6.1 KiB
C#
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;
|
|
}
|
|
} |