UI相关优化补充

This commit is contained in:
2026-05-25 13:21:41 +08:00
parent 3c812cfb41
commit a1f9122153
54 changed files with 2008 additions and 112 deletions

View File

@@ -0,0 +1,66 @@
using UnityEngine;
using BaseGames.Core;
namespace BaseGames.UI.Utility
{
/// <summary>
/// 色盲滤镜接入点(架构 10_UIModule §可访问性)。
/// 默认实现把 <see cref="ColorblindMode"/> 写入两个全局 Shader 参数:
/// _GlobalColorblindMode (int) 0=None, 1=Protanopia, 2=Deuteranopia, 3=Tritanopia
/// _GlobalColorblindStrength (float):固定 1.0,预留给后续过渡淡入
///
/// 项目接入方式:
/// - URP在 RendererFeature / PostProcess 中读取上述全局参数做 LUT 切换或 Daltonization 计算。
/// - 内置管线:自定义 Image Effect 读取这两个参数。
///
/// 不依赖任何后处理包,挂在持久 GameObject 即可DontDestroyOnLoad
/// 业务方可实现 <see cref="IColorblindApplier"/> 并通过 ServiceLocator 替换默认行为。
/// </summary>
[DefaultExecutionOrder(-700)]
public class ColorblindApplier : MonoBehaviour, IColorblindApplier
{
private static readonly int ModeId = Shader.PropertyToID("_GlobalColorblindMode");
private static readonly int StrengthId = Shader.PropertyToID("_GlobalColorblindStrength");
[Tooltip("打开后会写入 Shader 全局变量,但不会强制后处理生效;后处理流程需自行读取。")]
[SerializeField] private bool _writeShaderGlobals = true;
private void Awake()
{
ServiceLocator.RegisterIfAbsent<IColorblindApplier>(this);
}
private void OnEnable()
{
SettingsManager.SettingsChanged += OnSettingsChanged;
// 启动时若已加载,主动同步一次
var svc = ServiceLocator.GetOrDefault<ISettingsService>();
if (svc?.Current != null) ApplyMode(svc.Current.ColorblindMode);
}
private void OnDisable()
{
SettingsManager.SettingsChanged -= OnSettingsChanged;
}
private void OnDestroy()
{
ServiceLocator.Unregister<IColorblindApplier>(this);
}
private void OnSettingsChanged(GlobalSettingsData data) => ApplyMode(data.ColorblindMode);
public void ApplyMode(ColorblindMode mode)
{
if (!_writeShaderGlobals) return;
Shader.SetGlobalInt(ModeId, (int)mode);
Shader.SetGlobalFloat(StrengthId, mode == ColorblindMode.None ? 0f : 1f);
}
}
/// <summary>色盲滤镜接入接口;由 ColorblindApplier 默认实现。</summary>
public interface IColorblindApplier
{
void ApplyMode(ColorblindMode mode);
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7c8f1d9c11cc5cb43bdf3177b8028532
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,67 @@
using UnityEngine;
using UnityEngine.UI;
using BaseGames.Core;
namespace BaseGames.UI
{
/// <summary>
/// 将 <see cref="ISettingsService"/>.<c>Current.UIScale</c> 应用到挂载的
/// <see cref="CanvasScaler"/>。挂在每个根 Canvas 上即可。
///
/// 实现要点:
/// · 记录 Inspector 初始的 <c>scaleFactor</c> / <c>referenceResolution</c> 作为基准值,
/// 避免反复缩放导致的累积漂移;
/// · 订阅 <see cref="SettingsManager.SettingsChanged"/> 在玩家调整后即时刷新;
/// · 兼容 <see cref="CanvasScaler.ScaleMode.ScaleWithScreenSize"/> 与
/// <see cref="CanvasScaler.ScaleMode.ConstantPixelSize"/> 两种模式。
/// </summary>
[DisallowMultipleComponent]
[RequireComponent(typeof(CanvasScaler))]
public class UIScaleApplier : MonoBehaviour
{
private CanvasScaler _scaler;
private float _baseScaleFactor;
private Vector2 _baseReferenceResolution;
private void Awake()
{
_scaler = GetComponent<CanvasScaler>();
_baseScaleFactor = _scaler.scaleFactor;
_baseReferenceResolution = _scaler.referenceResolution;
}
private void OnEnable()
{
SettingsManager.SettingsChanged += OnSettingsChanged;
Apply();
}
private void OnDisable()
{
SettingsManager.SettingsChanged -= OnSettingsChanged;
}
private void OnSettingsChanged(GlobalSettingsData _) => Apply();
private void Apply()
{
var svc = ServiceLocator.GetOrDefault<ISettingsService>();
float ui = svc?.Current?.UIScale ?? 1f;
if (ui <= 0f) ui = 1f;
switch (_scaler.uiScaleMode)
{
case CanvasScaler.ScaleMode.ConstantPixelSize:
_scaler.scaleFactor = _baseScaleFactor * ui;
break;
case CanvasScaler.ScaleMode.ScaleWithScreenSize:
// 缩小参考分辨率 → 等价于放大 UI。
_scaler.referenceResolution = _baseReferenceResolution / ui;
break;
default:
_scaler.scaleFactor = _baseScaleFactor * ui;
break;
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9eae5e71f8a9b5b4582bfa1f0b41ac52
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,131 @@
using System.Collections;
using UnityEngine;
using UnityEngine.UI;
namespace BaseGames.UI
{
/// <summary>
/// UI 协程补间静态库。
///
/// 设计动机:消除 <see cref="BossHPBar"/>、<see cref="ToastNotification"/>、
/// <see cref="FloatingDamageText"/> 等组件中重复出现的 Lerp + WaitForEndOfFrame 样板,
/// 集中维护时间步与回调约定。所有协程默认使用 <see cref="Time.unscaledDeltaTime"/>
/// 以便 UI 动画在游戏暂停(<see cref="Time.timeScale"/> = 0时仍能播放。
///
/// 性能特征:
/// · 无堆分配(除协程对象本身);
/// · 提前返回保护无效目标;
/// · <paramref name="duration"/> &lt;= 0 时立即吸附到终态并退出(一帧 yield 用于保持
/// 与正常协程一致的栈语义)。
/// </summary>
public static class UITween
{
// ── 位置 ─────────────────────────────────────────────────────────────
/// <summary>将 RectTransform 的 anchoredPosition 平滑过渡到目标值。</summary>
public static IEnumerator MoveAnchored(RectTransform rect,
Vector2 target,
float duration,
bool unscaled = true)
{
if (rect == null) yield break;
if (duration <= 0f)
{
rect.anchoredPosition = target;
yield break;
}
Vector2 start = rect.anchoredPosition;
float t = 0f;
while (t < duration)
{
rect.anchoredPosition = Vector2.Lerp(start, target, t / duration);
t += unscaled ? Time.unscaledDeltaTime : Time.deltaTime;
yield return null;
}
rect.anchoredPosition = target;
}
// ── 透明度 ───────────────────────────────────────────────────────────
/// <summary>将 CanvasGroup.alpha 平滑过渡到目标值。</summary>
public static IEnumerator FadeCanvasGroup(CanvasGroup cg,
float target,
float duration,
bool unscaled = true)
{
if (cg == null) yield break;
if (duration <= 0f)
{
cg.alpha = target;
yield break;
}
float start = cg.alpha;
float t = 0f;
while (t < duration)
{
cg.alpha = Mathf.Lerp(start, target, t / duration);
t += unscaled ? Time.unscaledDeltaTime : Time.deltaTime;
yield return null;
}
cg.alpha = target;
}
/// <summary>将 GraphicImage / TMP_Text 等)的颜色 Alpha 通道平滑过渡到目标值。</summary>
public static IEnumerator FadeGraphic(Graphic graphic,
float targetAlpha,
float duration,
bool unscaled = true)
{
if (graphic == null) yield break;
if (duration <= 0f)
{
var c = graphic.color;
c.a = targetAlpha;
graphic.color = c;
yield break;
}
float startAlpha = graphic.color.a;
float t = 0f;
while (t < duration)
{
var c = graphic.color;
c.a = Mathf.Lerp(startAlpha, targetAlpha, t / duration);
graphic.color = c;
t += unscaled ? Time.unscaledDeltaTime : Time.deltaTime;
yield return null;
}
var fc = graphic.color;
fc.a = targetAlpha;
graphic.color = fc;
}
// ── 缩放 ─────────────────────────────────────────────────────────────
/// <summary>将 Transform 的 localScale 等比平滑过渡到目标值。</summary>
public static IEnumerator Scale(Transform tr,
Vector3 target,
float duration,
bool unscaled = true)
{
if (tr == null) yield break;
if (duration <= 0f)
{
tr.localScale = target;
yield break;
}
Vector3 start = tr.localScale;
float t = 0f;
while (t < duration)
{
tr.localScale = Vector3.Lerp(start, target, t / duration);
t += unscaled ? Time.unscaledDeltaTime : Time.deltaTime;
yield return null;
}
tr.localScale = target;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 96fd9d373d6d2784c8d04d8e56d0bb3e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: