using System.Collections;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
using BaseGames.Core.Events;
using BaseGames.Localization;
namespace BaseGames.UI
{
///
/// 全屏加载界面:进度条 + 提示文字 + 随机背景图(架构 10_UIModule §7.7)。
///
public class LoadingScreenManager : MonoBehaviour
{
[Tooltip("数据配置(策划编辑):随机提示、标题、时长/手感。\n" +
"非空时优先于下方序列化默认值;为空则回退默认值(向后兼容)。\n" +
"视觉样式(图片/配色/布局)在 UI_LoadingScreen 预制件里由美术编辑,不在此表。")]
[SerializeField] private LoadingScreenConfigSO _config;
[Header("视图引用(由 UI_LoadingScreen 预制件绑定)")]
[SerializeField] private GameObject _loadingRoot;
[SerializeField] private Image _progressFill;
[SerializeField] private TMP_Text _titleText;
[SerializeField] private TMP_Text _tipText;
[SerializeField] private Image[] _backgroundArts;
[Header("默认值(无 _config 时回退使用)")]
[SerializeField] private string[] _tipMessages; // 本地化 key(对应 "UI" 表中的条目,如 "tip_explore")
[SerializeField] private float _minDisplayTime = 0.5f;
[Tooltip("进度条填充缓动速度(每秒可填充的比例),用于把显示值平滑推向目标、避免瞬跳;越大越快、越小越平滑。")]
[SerializeField] private float _fillLerpSpeed = 1.6f;
[Tooltip("预计加载时长(秒),用于‘感知进度’:进度条按时间平滑爬升至约 90%,真正加载完成时再冲满。\n" +
"原因:Addressables 对单个场景的真实进度高度前置(毫秒内跳到 ~90% 再长时间停滞),\n" +
"直接驱动会让进度条‘瞬间近满→卡死’;按时间爬升能保证进度条始终可见地推进。\n" +
"建议设为该游戏场景的典型加载耗时;偏长则爬得慢、偏短则更快到 90%。")]
[SerializeField] private float _expectedLoadTime = 2.5f;
[Header("Event Channels")]
[SerializeField] private VoidEventChannelSO _onLoadingStarted;
[SerializeField] private VoidEventChannelSO _onLoadingComplete;
[SerializeField] private FloatEventChannelSO _onLoadingProgressUpdated;
// 未完成时进度条按时间爬升的上限(约 90%),余下 10% 留给"加载完成"那一冲。
private const float kCreepCap = 0.9f;
private float _shownAt;
private float _targetProgress; // 真实进度 [0,1],仅用于判定"是否已完成"(达到 1 冲满)
private float _displayedProgress; // 进度条当前显示值(感知进度,缓动推进)
private bool _isShowing;
private readonly CompositeDisposable _subs = new();
// 配置取值:_config 优先,否则用组件上的序列化默认值(向后兼容)。
private float MinDisplay => _config != null ? _config.MinDisplayTime : _minDisplayTime;
private float ExpectedLoad => _config != null ? _config.ExpectedLoadTime : _expectedLoadTime;
private float FillSpeed => _config != null ? _config.FillLerpSpeed : _fillLerpSpeed;
private string[] TipKeys => (_config != null && _config.TipKeys != null && _config.TipKeys.Length > 0)
? _config.TipKeys : _tipMessages;
private string TitleKey => _config != null ? _config.TitleKey : null;
private void OnEnable()
{
_onLoadingStarted?.Subscribe(Show).AddTo(_subs);
_onLoadingComplete?.Subscribe(Hide).AddTo(_subs);
_onLoadingProgressUpdated?.Subscribe(SetProgress).AddTo(_subs);
// 关键:本组件的宿主 Canvas 必须保持 active,OnEnable 才会执行并订阅上面的事件
// (管理器不能挂在被自己关闭的 GameObject 上,否则永不订阅、加载画面永不显示)。
// 因此这里只把内层遮罩根节点初始隐藏,显隐由 Show()/Hide() 切换 _loadingRoot。
_isShowing = false;
if (_loadingRoot != null) _loadingRoot.SetActive(false);
}
private void OnDisable()
{
_subs.Clear();
}
// ── 公开 API(SceneLoader 可直接调用)────────────────────────────────
public void Show()
{
_shownAt = Time.unscaledTime;
_targetProgress = 0f;
_displayedProgress = 0f;
_isShowing = true;
if (_loadingRoot != null) _loadingRoot.SetActive(true);
if (_progressFill != null) _progressFill.fillAmount = 0f;
// 随机背景
if (_backgroundArts != null && _backgroundArts.Length > 0)
{
foreach (var bg in _backgroundArts) if (bg != null) bg.enabled = false;
_backgroundArts[Random.Range(0, _backgroundArts.Length)].enabled = true;
}
// 标题(config 提供 key;留空则隐藏 Title 节点)
if (_titleText != null)
{
string titleKey = TitleKey;
bool hasTitle = !string.IsNullOrEmpty(titleKey);
_titleText.gameObject.SetActive(hasTitle);
if (hasTitle)
_titleText.text = LocalizationManager.Get(titleKey, LocalizationTable.UI);
}
// 随机提示(通过 LocalizationManager 解析 key)
var tipKeys = TipKeys;
if (_tipText != null && tipKeys != null && tipKeys.Length > 0)
_tipText.text = LocalizationManager.Get(tipKeys[Random.Range(0, tipKeys.Length)], LocalizationTable.UI);
}
public void Hide() => StartCoroutine(HideAfterMinTime());
/// 接收真实加载进度。注意:进度条显示采用基于时间的"感知进度"(见 ),
/// 这里的真实值仅用于判定"是否已完成"(达到 1 时冲满)——因为 Addressables 单场景的真实进度
/// 高度前置、不适合直接驱动进度条。
public void SetProgress(float value) => _targetProgress = Mathf.Clamp01(value);
// ── 内部 ─────────────────────────────────────────────────────────────
// 进度条采用"感知进度":未完成时按时间预算(_expectedLoadTime)平滑爬升至 kCreepCap(~90%),
// 加载完成(_targetProgress 达到 1,由 SceneLoader 末帧或 Hide() 置位)时冲满到 1。
// 不直接跟随 Addressables 的真实 PercentComplete —— 它对单场景高度前置(毫秒内近满后长时间停滞),
// 直接映射会让进度条"瞬间近满→卡死"。使用 unscaledDeltaTime,不受 timeScale 影响。
private void Update()
{
if (!_isShowing || _progressFill == null) return;
float effectiveTarget;
if (_targetProgress >= 1f)
{
effectiveTarget = 1f; // 加载完成 → 冲满
}
else
{
float elapsed = Time.unscaledTime - _shownAt;
float expected = ExpectedLoad;
effectiveTarget = expected > 0f
? Mathf.Clamp01(elapsed / expected) * kCreepCap // 按时间爬升至 ~90%
: kCreepCap;
}
_displayedProgress = Mathf.MoveTowards(
_displayedProgress, effectiveTarget, FillSpeed * Time.unscaledDeltaTime);
_progressFill.fillAmount = _displayedProgress;
}
private IEnumerator HideAfterMinTime()
{
// 收尾:把目标推到满,让缓动平滑填满进度条后再隐藏。
_targetProgress = 1f;
// 同时满足两个条件才隐藏:① 至少显示 MinDisplay ② 进度条视觉已填满。
// 安全上限避免极端卡顿(缓动追不上 / 进度条缺失)时永不隐藏。
float minDisplay = MinDisplay;
float safetyDeadline = Time.unscaledTime + minDisplay + 1.5f;
while (Time.unscaledTime < safetyDeadline)
{
bool minTimeElapsed = Time.unscaledTime - _shownAt >= minDisplay;
bool barFilled = _progressFill == null || _displayedProgress >= 0.999f;
if (minTimeElapsed && barFilled) break;
yield return null;
}
_isShowing = false;
if (_loadingRoot != null) _loadingRoot.SetActive(false);
}
}
}