UI系统
This commit is contained in:
@@ -12,26 +12,64 @@ namespace BaseGames.UI
|
||||
/// </summary>
|
||||
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()
|
||||
{
|
||||
@@ -43,6 +81,9 @@ namespace BaseGames.UI
|
||||
public void Show()
|
||||
{
|
||||
_shownAt = Time.unscaledTime;
|
||||
_targetProgress = 0f;
|
||||
_displayedProgress = 0f;
|
||||
_isShowing = true;
|
||||
if (_loadingRoot != null) _loadingRoot.SetActive(true);
|
||||
if (_progressFill != null) _progressFill.fillAmount = 0f;
|
||||
|
||||
@@ -53,41 +94,76 @@ namespace BaseGames.UI
|
||||
_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)
|
||||
if (_tipText != null && _tipMessages != null && _tipMessages.Length > 0)
|
||||
_tipText.text = LocalizationManager.Get(_tipMessages[Random.Range(0, _tipMessages.Length)], LocalizationTable.UI);
|
||||
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());
|
||||
|
||||
public void SetProgress(float value)
|
||||
{
|
||||
if (_progressFill != null)
|
||||
_progressFill.fillAmount = Mathf.Clamp01(value);
|
||||
}
|
||||
/// <summary>接收真实加载进度。注意:进度条显示采用基于时间的"感知进度"(见 <see cref="Update"/>),
|
||||
/// 这里的真实值仅用于判定"是否已完成"(达到 1 时冲满)——因为 Addressables 单场景的真实进度
|
||||
/// 高度前置、不适合直接驱动进度条。</summary>
|
||||
public void SetProgress(float value) => _targetProgress = Mathf.Clamp01(value);
|
||||
|
||||
// ── 内部 ─────────────────────────────────────────────────────────────
|
||||
|
||||
// 缓存等待对象以避免典型路径(剩余时间 == _minDisplayTime)下的重复分配。
|
||||
private WaitForSecondsRealtime _cachedFullWait;
|
||||
// 进度条采用"感知进度":未完成时按时间预算(_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()
|
||||
{
|
||||
float elapsed = Time.unscaledTime - _shownAt;
|
||||
float remaining = _minDisplayTime - elapsed;
|
||||
if (remaining > 0f)
|
||||
// 收尾:把目标推到满,让缓动平滑填满进度条后再隐藏。
|
||||
_targetProgress = 1f;
|
||||
|
||||
// 同时满足两个条件才隐藏:① 至少显示 MinDisplay ② 进度条视觉已填满。
|
||||
// 安全上限避免极端卡顿(缓动追不上 / 进度条缺失)时永不隐藏。
|
||||
float minDisplay = MinDisplay;
|
||||
float safetyDeadline = Time.unscaledTime + minDisplay + 1.5f;
|
||||
while (Time.unscaledTime < safetyDeadline)
|
||||
{
|
||||
// 完整剩余 ≈ 显示时间时复用缓存对象;否则按需新建(罕见路径)。
|
||||
if (Mathf.Approximately(remaining, _minDisplayTime))
|
||||
{
|
||||
_cachedFullWait ??= new WaitForSecondsRealtime(_minDisplayTime);
|
||||
yield return _cachedFullWait;
|
||||
}
|
||||
else
|
||||
{
|
||||
yield return new WaitForSecondsRealtime(remaining);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user