UI相关优化补充
This commit is contained in:
8
Assets/Tests/PlayMode.meta
Normal file
8
Assets/Tests/PlayMode.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 71c6d91864f353542af76dc4bdd78175
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
24
Assets/Tests/PlayMode/BaseGames.Tests.PlayMode.asmdef
Normal file
24
Assets/Tests/PlayMode/BaseGames.Tests.PlayMode.asmdef
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "BaseGames.Tests.PlayMode",
|
||||
"rootNamespace": "BaseGames.Tests.PlayMode",
|
||||
"references": [
|
||||
"BaseGames.Core",
|
||||
"BaseGames.Core.Events",
|
||||
"BaseGames.UI",
|
||||
"UnityEngine.TestRunner",
|
||||
"Unity.TextMeshPro"
|
||||
],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": true,
|
||||
"precompiledReferences": [
|
||||
"nunit.framework.dll"
|
||||
],
|
||||
"autoReferenced": false,
|
||||
"defineConstraints": [
|
||||
"UNITY_INCLUDE_TESTS"
|
||||
],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ba2164f377b449148960bb029fea13e7
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
37
Assets/Tests/PlayMode/ColorblindApplierTests.cs
Normal file
37
Assets/Tests/PlayMode/ColorblindApplierTests.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using System.Collections;
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
using UnityEngine.TestTools;
|
||||
using BaseGames.Core;
|
||||
using BaseGames.UI.Utility;
|
||||
|
||||
namespace BaseGames.Tests.PlayMode
|
||||
{
|
||||
/// <summary>
|
||||
/// ColorblindApplier 测试:写入 Shader 全局变量。
|
||||
/// </summary>
|
||||
public class ColorblindApplierTests
|
||||
{
|
||||
private GameObject _host;
|
||||
|
||||
[SetUp] public void SetUp() { _host = new GameObject("ColorblindHost"); }
|
||||
[TearDown] public void TearDown() { Object.DestroyImmediate(_host); }
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator ApplyMode_WritesShaderGlobals()
|
||||
{
|
||||
var applier = _host.AddComponent<ColorblindApplier>();
|
||||
applier.ApplyMode(ColorblindMode.Deuteranopia);
|
||||
yield return null;
|
||||
Assert.AreEqual((int)ColorblindMode.Deuteranopia,
|
||||
Shader.GetGlobalInt(Shader.PropertyToID("_GlobalColorblindMode")));
|
||||
Assert.AreEqual(1f,
|
||||
Shader.GetGlobalFloat(Shader.PropertyToID("_GlobalColorblindStrength")), 0.001f);
|
||||
|
||||
applier.ApplyMode(ColorblindMode.None);
|
||||
yield return null;
|
||||
Assert.AreEqual(0,
|
||||
Shader.GetGlobalInt(Shader.PropertyToID("_GlobalColorblindMode")));
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/Tests/PlayMode/ColorblindApplierTests.cs.meta
Normal file
11
Assets/Tests/PlayMode/ColorblindApplierTests.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 07ca55485ed3db14e822d55d1116e5d3
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
49
Assets/Tests/PlayMode/RequiredFieldValidatorTests.cs
Normal file
49
Assets/Tests/PlayMode/RequiredFieldValidatorTests.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
using BaseGames.Core;
|
||||
|
||||
namespace BaseGames.Tests.PlayMode
|
||||
{
|
||||
/// <summary>
|
||||
/// RequiredFieldValidator 反射扫描测试。
|
||||
/// </summary>
|
||||
public class RequiredFieldValidatorTests
|
||||
{
|
||||
private class Sample : MonoBehaviour
|
||||
{
|
||||
[RequiredField] public GameObject Required;
|
||||
[RequiredField("提示文本")] public string RequiredString;
|
||||
public GameObject Optional;
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void MissingField_LogsWarning()
|
||||
{
|
||||
var go = new GameObject("ReqHost");
|
||||
var s = go.AddComponent<Sample>();
|
||||
s.Required = null;
|
||||
s.RequiredString = "";
|
||||
|
||||
// 期待两条警告(Required + RequiredString)
|
||||
UnityEngine.TestTools.LogAssert.Expect(LogType.Warning,
|
||||
new System.Text.RegularExpressions.Regex(@"\[RequiredField\] Sample\.Required.*"));
|
||||
UnityEngine.TestTools.LogAssert.Expect(LogType.Warning,
|
||||
new System.Text.RegularExpressions.Regex(@"\[RequiredField\] Sample\.RequiredString.*"));
|
||||
RequiredFieldValidator.ValidateAll(s);
|
||||
|
||||
Object.DestroyImmediate(go);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FilledField_NoWarning()
|
||||
{
|
||||
var go = new GameObject("ReqHost2");
|
||||
var s = go.AddComponent<Sample>();
|
||||
s.Required = go;
|
||||
s.RequiredString = "ok";
|
||||
|
||||
RequiredFieldValidator.ValidateAll(s); // 不应有警告
|
||||
Object.DestroyImmediate(go);
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/Tests/PlayMode/RequiredFieldValidatorTests.cs.meta
Normal file
11
Assets/Tests/PlayMode/RequiredFieldValidatorTests.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5afa9356c16c2954f899e0d092be37b3
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
79
Assets/Tests/PlayMode/ToastManagerTests.cs
Normal file
79
Assets/Tests/PlayMode/ToastManagerTests.cs
Normal file
@@ -0,0 +1,79 @@
|
||||
using System.Collections;
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityEngine.TestTools;
|
||||
using TMPro;
|
||||
using BaseGames.UI;
|
||||
|
||||
namespace BaseGames.Tests.PlayMode
|
||||
{
|
||||
/// <summary>
|
||||
/// ToastManager 队列行为测试:
|
||||
/// · Enqueue 一条后 Toast 激活并最终隐藏
|
||||
/// · 连续 Enqueue 3 条按序串行播放
|
||||
///
|
||||
/// 不依赖事件频道(直接调用 Enqueue),不依赖本地化(标题/正文为常量字符串)。
|
||||
/// </summary>
|
||||
public class ToastManagerTests
|
||||
{
|
||||
private GameObject _host;
|
||||
private ToastManager _mgr;
|
||||
private ToastNotification _toast;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
_host = new GameObject("ToastHost");
|
||||
|
||||
// Toast 预制:CanvasGroup + 子文本
|
||||
var toastGO = new GameObject("Toast", typeof(CanvasGroup));
|
||||
toastGO.transform.SetParent(_host.transform);
|
||||
toastGO.SetActive(false);
|
||||
_toast = toastGO.AddComponent<ToastNotification>();
|
||||
// 反射注入 _displayDuration / _fadeDuration 减为短值,缩短测试时长
|
||||
SetPrivate(_toast, "_displayDuration", 0.05f);
|
||||
SetPrivate(_toast, "_fadeDuration", 0.02f);
|
||||
|
||||
_mgr = _host.AddComponent<ToastManager>();
|
||||
SetPrivate(_mgr, "_toast", _toast);
|
||||
_host.SetActive(false);
|
||||
_host.SetActive(true); // 触发 Awake/OnEnable
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void TearDown() { Object.DestroyImmediate(_host); }
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator Enqueue_ShowsThenHides()
|
||||
{
|
||||
_mgr.Enqueue("T", "B", null);
|
||||
yield return null;
|
||||
Assert.IsTrue(_toast.gameObject.activeSelf, "入队后 Toast 应当激活");
|
||||
|
||||
// 总时长 ~= 0.02+0.05+0.02 = 0.09s,再 + 队列等待 0.1s
|
||||
yield return new WaitForSecondsRealtime(0.5f);
|
||||
Assert.IsFalse(_toast.gameObject.activeSelf, "Toast 自动隐藏未生效");
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator MultipleEnqueue_PlaysSerially()
|
||||
{
|
||||
_mgr.Enqueue("A", "1", null);
|
||||
_mgr.Enqueue("B", "2", null);
|
||||
yield return null;
|
||||
Assert.IsTrue(_toast.gameObject.activeSelf, "第一条应当立即播放");
|
||||
// 不严格验证内容(涉及私有字段),只验证活动状态推进。
|
||||
yield return new WaitForSecondsRealtime(1.0f);
|
||||
Assert.IsFalse(_toast.gameObject.activeSelf, "两条串行播放后应当全部结束");
|
||||
}
|
||||
|
||||
// ── 反射工具 ──────────────────────────────────────────────────────
|
||||
private static void SetPrivate(object target, string fieldName, object value)
|
||||
{
|
||||
var f = target.GetType().GetField(fieldName,
|
||||
System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
|
||||
f?.SetValue(target, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/Tests/PlayMode/ToastManagerTests.cs.meta
Normal file
11
Assets/Tests/PlayMode/ToastManagerTests.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ee01b829ae0f99445b90f0561e2ccae1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
94
Assets/Tests/PlayMode/UIManagerSmokeTest.cs
Normal file
94
Assets/Tests/PlayMode/UIManagerSmokeTest.cs
Normal file
@@ -0,0 +1,94 @@
|
||||
using System.Collections;
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
using UnityEngine.TestTools;
|
||||
using BaseGames.UI;
|
||||
|
||||
namespace BaseGames.Tests.PlayMode
|
||||
{
|
||||
/// <summary>
|
||||
/// UIManager 烟雾测试:验证面板栈基本不变式。
|
||||
///
|
||||
/// 覆盖:
|
||||
/// · OpenPanel → CloseTopPanel 后栈为空;
|
||||
/// · 嵌套打开(A → B → CloseTop)后 A 仍处于激活态;
|
||||
/// · 重复 OpenPanel 同一面板不会双压栈(HashSet 去重)。
|
||||
///
|
||||
/// 注:测试只覆盖框架行为,不验证业务面板(CharmPanel 等)的内部逻辑。
|
||||
/// </summary>
|
||||
public class UIManagerSmokeTest
|
||||
{
|
||||
private GameObject _hostGO;
|
||||
private UIManager _manager;
|
||||
private GameObject _panelA;
|
||||
private GameObject _panelB;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
_hostGO = new GameObject("UIManager_Host");
|
||||
_manager = _hostGO.AddComponent<UIManager>();
|
||||
_panelA = new GameObject("PanelA");
|
||||
_panelB = new GameObject("PanelB");
|
||||
_panelA.SetActive(false);
|
||||
_panelB.SetActive(false);
|
||||
|
||||
// 强制 OnEnable 触发(AddComponent 同帧已触发,这里二次启用保证 ServiceLocator 状态)
|
||||
_hostGO.SetActive(false);
|
||||
_hostGO.SetActive(true);
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
Object.DestroyImmediate(_panelA);
|
||||
Object.DestroyImmediate(_panelB);
|
||||
Object.DestroyImmediate(_hostGO);
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator OpenThenCloseTop_PanelDeactivated()
|
||||
{
|
||||
_manager.OpenPanel(_panelA);
|
||||
yield return null;
|
||||
|
||||
Assert.IsTrue(_panelA.activeSelf, "OpenPanel 应当激活面板");
|
||||
|
||||
_manager.CloseTopPanel();
|
||||
yield return null;
|
||||
|
||||
Assert.IsFalse(_panelA.activeSelf, "CloseTopPanel 应当反激活栈顶面板");
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator NestedOpen_PreviousPanelHiddenAndRestored()
|
||||
{
|
||||
_manager.OpenPanel(_panelA);
|
||||
_manager.OpenPanel(_panelB);
|
||||
yield return null;
|
||||
|
||||
Assert.IsFalse(_panelA.activeSelf, "嵌套打开时上一层应被隐藏");
|
||||
Assert.IsTrue(_panelB.activeSelf, "新打开的面板应当激活");
|
||||
|
||||
_manager.CloseTopPanel();
|
||||
yield return null;
|
||||
|
||||
Assert.IsTrue(_panelA.activeSelf, "关闭栈顶后上一层应当恢复");
|
||||
Assert.IsFalse(_panelB.activeSelf, "关闭后栈顶面板应当反激活");
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator DoubleOpenSamePanel_NoStackDuplication()
|
||||
{
|
||||
_manager.OpenPanel(_panelA);
|
||||
_manager.OpenPanel(_panelA);
|
||||
yield return null;
|
||||
|
||||
// 关闭一次后栈应为空
|
||||
_manager.CloseTopPanel();
|
||||
yield return null;
|
||||
|
||||
Assert.IsFalse(_panelA.activeSelf, "重复打开应被去重;关闭一次后即应隐藏");
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/Tests/PlayMode/UIManagerSmokeTest.cs.meta
Normal file
11
Assets/Tests/PlayMode/UIManagerSmokeTest.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 953f5cb03f10b1d4db3d8da7121ba910
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
60
Assets/Tests/PlayMode/UITweenTests.cs
Normal file
60
Assets/Tests/PlayMode/UITweenTests.cs
Normal file
@@ -0,0 +1,60 @@
|
||||
using System.Collections;
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityEngine.TestTools;
|
||||
using BaseGames.UI;
|
||||
|
||||
namespace BaseGames.Tests.PlayMode
|
||||
{
|
||||
/// <summary>
|
||||
/// UITween 静态库测试:验证补间在终态吸附、零时长立即返回、null 安全。
|
||||
/// </summary>
|
||||
public class UITweenTests
|
||||
{
|
||||
private GameObject _go;
|
||||
|
||||
[SetUp] public void SetUp() { _go = new GameObject("TweenHost", typeof(RectTransform), typeof(CanvasGroup)); }
|
||||
[TearDown] public void TearDown() { Object.DestroyImmediate(_go); }
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator MoveAnchored_ReachesTarget()
|
||||
{
|
||||
var rect = _go.GetComponent<RectTransform>();
|
||||
rect.anchoredPosition = Vector2.zero;
|
||||
yield return _go.AddComponent<TestRunner>().Run(UITween.MoveAnchored(rect, new Vector2(100, 50), 0.05f));
|
||||
Assert.AreEqual(new Vector2(100, 50), rect.anchoredPosition);
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator FadeCanvasGroup_ReachesTarget()
|
||||
{
|
||||
var cg = _go.GetComponent<CanvasGroup>();
|
||||
cg.alpha = 0f;
|
||||
yield return _go.AddComponent<TestRunner>().Run(UITween.FadeCanvasGroup(cg, 1f, 0.05f));
|
||||
Assert.AreEqual(1f, cg.alpha, 0.001f);
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator ZeroDuration_SnapsImmediately()
|
||||
{
|
||||
var rect = _go.GetComponent<RectTransform>();
|
||||
yield return _go.AddComponent<TestRunner>().Run(UITween.MoveAnchored(rect, new Vector2(7, 7), 0f));
|
||||
Assert.AreEqual(new Vector2(7, 7), rect.anchoredPosition);
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator NullTarget_NoException()
|
||||
{
|
||||
yield return _go.AddComponent<TestRunner>().Run(UITween.MoveAnchored(null, Vector2.one, 0.05f));
|
||||
yield return _go.AddComponent<TestRunner>().Run(UITween.FadeCanvasGroup(null, 1f, 0.05f));
|
||||
Assert.Pass();
|
||||
}
|
||||
|
||||
/// <summary>挂宿主跑协程的辅助 MonoBehaviour(测试场景无 EventSystem)。</summary>
|
||||
private class TestRunner : MonoBehaviour
|
||||
{
|
||||
public IEnumerator Run(IEnumerator inner) { yield return StartCoroutine(inner); }
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/Tests/PlayMode/UITweenTests.cs.meta
Normal file
11
Assets/Tests/PlayMode/UITweenTests.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 65492216fce745f41b6a7352763938e2
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -3,6 +3,17 @@ using BaseGames.Core.Events;
|
||||
|
||||
namespace BaseGames.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// 色盲滤镜模式。运行时由后期处理(如 URP Volume)读取并切换对应的 LUT/Shader。
|
||||
/// </summary>
|
||||
public enum ColorblindMode
|
||||
{
|
||||
None = 0,
|
||||
Protanopia = 1,
|
||||
Deuteranopia = 2,
|
||||
Tritanopia = 3,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 游戏全局设置数据(运行时值)。
|
||||
/// </summary>
|
||||
@@ -21,6 +32,16 @@ namespace BaseGames.Core
|
||||
public string Language = "zh-CN";
|
||||
|
||||
public bool ShowSpeedrunTimer = false;
|
||||
|
||||
// ── 可访问性 ────────────────────────────────────────────────────────
|
||||
[Tooltip("UI 整体缩放系数(0.8 ~ 1.5),通过 CanvasScaler 应用。")]
|
||||
public float UIScale = 1f;
|
||||
|
||||
[Tooltip("色盲滤镜模式。")]
|
||||
public ColorblindMode ColorblindMode = ColorblindMode.None;
|
||||
|
||||
[Tooltip("镜头/UI 震动开关;关闭后受击晃动、命中冲击等屏幕震动被屏蔽。")]
|
||||
public bool ScreenShakeEnabled = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -46,6 +67,11 @@ namespace BaseGames.Core
|
||||
[Header("Speedrun")]
|
||||
public bool ShowSpeedrunTimer = false;
|
||||
|
||||
[Header("Accessibility")]
|
||||
[Range(0.8f, 1.5f)] public float DefaultUIScale = 1f;
|
||||
public ColorblindMode DefaultColorblindMode = ColorblindMode.None;
|
||||
public bool DefaultScreenShakeEnabled = true;
|
||||
|
||||
/// <summary>将 SO 默认值填入 GlobalSettingsData。</summary>
|
||||
public GlobalSettingsData CreateDefault() => new GlobalSettingsData
|
||||
{
|
||||
@@ -58,6 +84,9 @@ namespace BaseGames.Core
|
||||
FullScreen = DefaultFullScreen,
|
||||
Language = DefaultLanguage,
|
||||
ShowSpeedrunTimer = ShowSpeedrunTimer,
|
||||
UIScale = DefaultUIScale,
|
||||
ColorblindMode = DefaultColorblindMode,
|
||||
ScreenShakeEnabled = DefaultScreenShakeEnabled,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,12 @@ namespace BaseGames.Core
|
||||
void SetVSync(bool enabled);
|
||||
void SetTargetFrameRate(int fps);
|
||||
void SetLanguage(string localeCode);
|
||||
|
||||
// ── 可访问性 ────────────────────────────────────────────────────────
|
||||
void SetUIScale(float scale);
|
||||
void SetColorblindMode(ColorblindMode mode);
|
||||
void SetScreenShakeEnabled(bool enabled);
|
||||
|
||||
void Save();
|
||||
}
|
||||
}
|
||||
|
||||
55
Assets/_Game/Scripts/Core/RequiredFieldAttribute.cs
Normal file
55
Assets/_Game/Scripts/Core/RequiredFieldAttribute.cs
Normal file
@@ -0,0 +1,55 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// 标记一个序列化字段必填。运行期 / Inspector 漏配时给出明确提示,
|
||||
/// 减少策划"为什么没显示"的排查成本。
|
||||
///
|
||||
/// 用法:[SerializeField, RequiredField] private GameObject _root;
|
||||
///
|
||||
/// 表现:
|
||||
/// - Inspector 中字段未赋值时显示红色 HelpBox 并加红框(见 Editor/RequiredFieldDrawer.cs)。
|
||||
/// - 调用方在 OnValidate / Awake 中可调用 RequiredFieldValidator.ValidateAll(this) 触发运行期警告。
|
||||
/// </summary>
|
||||
[System.AttributeUsage(System.AttributeTargets.Field, AllowMultiple = false, Inherited = true)]
|
||||
public class RequiredFieldAttribute : PropertyAttribute
|
||||
{
|
||||
public readonly string Hint;
|
||||
public RequiredFieldAttribute(string hint = null) { Hint = hint; }
|
||||
}
|
||||
|
||||
/// <summary>运行期辅助:在 OnValidate / Awake 中调用,扫描自身被 [RequiredField] 标注的字段。</summary>
|
||||
public static class RequiredFieldValidator
|
||||
{
|
||||
/// <summary>
|
||||
/// 反射扫描 target 上所有 [RequiredField] 字段,未赋值时 Debug.LogWarning。
|
||||
/// 建议仅在 OnValidate / Awake 中调用(运行时调用反射有性能开销)。
|
||||
/// </summary>
|
||||
public static void ValidateAll(Object target)
|
||||
{
|
||||
if (target == null) return;
|
||||
var type = target.GetType();
|
||||
var fields = type.GetFields(System.Reflection.BindingFlags.Instance
|
||||
| System.Reflection.BindingFlags.Public
|
||||
| System.Reflection.BindingFlags.NonPublic);
|
||||
foreach (var f in fields)
|
||||
{
|
||||
var attr = (RequiredFieldAttribute)System.Attribute.GetCustomAttribute(f, typeof(RequiredFieldAttribute));
|
||||
if (attr == null) continue;
|
||||
var value = f.GetValue(target);
|
||||
if (IsNullOrMissingRef(value))
|
||||
{
|
||||
var hint = string.IsNullOrEmpty(attr.Hint) ? "" : $"({attr.Hint})";
|
||||
Debug.LogWarning($"[RequiredField] {type.Name}.{f.Name} 未赋值{hint}!", target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsNullOrMissingRef(object value)
|
||||
{
|
||||
if (value is Object uo) return uo == null; // 含 Missing Reference 的"虚假 null"也算
|
||||
return value == null;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Core/RequiredFieldAttribute.cs.meta
Normal file
11
Assets/_Game/Scripts/Core/RequiredFieldAttribute.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ee796ace5d7a52643a001ca1968b6e28
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using UnityEngine;
|
||||
|
||||
@@ -5,6 +6,8 @@ namespace BaseGames.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// 全局设置管理器。从 GlobalSettingsSO 读取默认值,从文件加载用户覆盖。
|
||||
/// 任何 Setter 调用 Save() 后会触发 <see cref="SettingsChanged"/> 静态事件,
|
||||
/// 供 UIScaleApplier / ColorblindApplier / CameraShake 等订阅。
|
||||
/// </summary>
|
||||
[DefaultExecutionOrder(-800)]
|
||||
public class SettingsManager : MonoBehaviour, ISettingsService
|
||||
@@ -18,6 +21,9 @@ namespace BaseGames.Core
|
||||
|
||||
public GlobalSettingsData Current => _current;
|
||||
|
||||
/// <summary>设置变更后触发(用于 UIScaleApplier、色盲滤镜、Camera Shake 等订阅)。</summary>
|
||||
public static event Action<GlobalSettingsData> SettingsChanged;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
ServiceLocator.Register<ISettingsService>(this);
|
||||
@@ -28,6 +34,7 @@ namespace BaseGames.Core
|
||||
{
|
||||
_current = Load() ?? _defaultSettings?.CreateDefault() ?? new GlobalSettingsData();
|
||||
Apply(_current);
|
||||
SettingsChanged?.Invoke(_current);
|
||||
}
|
||||
|
||||
private GlobalSettingsData Load()
|
||||
@@ -55,6 +62,7 @@ namespace BaseGames.Core
|
||||
{
|
||||
Debug.LogWarning($"[SettingsManager] 设置保存失败: {e.Message}");
|
||||
}
|
||||
SettingsChanged?.Invoke(_current);
|
||||
}
|
||||
|
||||
private void Apply(GlobalSettingsData data)
|
||||
@@ -66,13 +74,13 @@ namespace BaseGames.Core
|
||||
Screen.fullScreenMode = FullScreenMode.FullScreenWindow;
|
||||
}
|
||||
|
||||
// ── 音量设置(调用 AudioManager)────────────────────
|
||||
// ── 音量设置(调用 AudioManager)─────────────────────────────────────
|
||||
public void SetMasterVolume(float v) { _current.MasterVolume = v; Save(); }
|
||||
public void SetBGMVolume(float v) { _current.BGMVolume = v; Save(); }
|
||||
public void SetSFXVolume(float v) { _current.SFXVolume = v; Save(); }
|
||||
public void SetAmbientVolume(float v) { _current.AmbientVolume = v; Save(); }
|
||||
|
||||
// ── 画面设置 ──────────────────────────────────────────────────────
|
||||
// ── 画面设置 ──────────────────────────────────────────────────────────
|
||||
public void SetResolution(int w, int h, FullScreenMode mode)
|
||||
{
|
||||
Screen.SetResolution(w, h, mode);
|
||||
@@ -98,6 +106,23 @@ namespace BaseGames.Core
|
||||
Save();
|
||||
}
|
||||
|
||||
// ── 可访问性 ──────────────────────────────────────────────────────────
|
||||
public void SetUIScale(float scale)
|
||||
{
|
||||
_current.UIScale = Mathf.Clamp(scale, 0.5f, 2f);
|
||||
Save();
|
||||
}
|
||||
public void SetColorblindMode(ColorblindMode mode)
|
||||
{
|
||||
_current.ColorblindMode = mode;
|
||||
Save();
|
||||
}
|
||||
public void SetScreenShakeEnabled(bool enabled)
|
||||
{
|
||||
_current.ScreenShakeEnabled = enabled;
|
||||
Save();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
ServiceLocator.Unregister<ISettingsService>(this);
|
||||
|
||||
8
Assets/_Game/Scripts/Editor/Inspector.meta
Normal file
8
Assets/_Game/Scripts/Editor/Inspector.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bb77d9e506b335e46860387632561634
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
58
Assets/_Game/Scripts/Editor/Inspector/RequiredFieldDrawer.cs
Normal file
58
Assets/_Game/Scripts/Editor/Inspector/RequiredFieldDrawer.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using BaseGames.Core;
|
||||
|
||||
namespace BaseGames.Editor.Inspector
|
||||
{
|
||||
/// <summary>
|
||||
/// [RequiredField] 的 Inspector 绘制:未赋值时显示红色 HelpBox。
|
||||
/// </summary>
|
||||
[CustomPropertyDrawer(typeof(RequiredFieldAttribute))]
|
||||
public class RequiredFieldDrawer : PropertyDrawer
|
||||
{
|
||||
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
|
||||
{
|
||||
bool missing = IsMissing(property);
|
||||
if (missing)
|
||||
{
|
||||
var msgHeight = EditorGUIUtility.singleLineHeight * 1.6f;
|
||||
var msgRect = new Rect(position.x, position.y, position.width, msgHeight);
|
||||
var attr = (RequiredFieldAttribute)attribute;
|
||||
var hint = string.IsNullOrEmpty(attr.Hint) ? "" : $" ({attr.Hint})";
|
||||
EditorGUI.HelpBox(msgRect, $"必填字段未赋值{hint}", MessageType.Error);
|
||||
|
||||
var fieldRect = new Rect(position.x, position.y + msgHeight + 2, position.width,
|
||||
EditorGUIUtility.singleLineHeight);
|
||||
var prev = GUI.color; GUI.color = new Color(1f, 0.6f, 0.6f);
|
||||
EditorGUI.PropertyField(fieldRect, property, label);
|
||||
GUI.color = prev;
|
||||
}
|
||||
else
|
||||
{
|
||||
EditorGUI.PropertyField(position, property, label, true);
|
||||
}
|
||||
}
|
||||
|
||||
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
|
||||
{
|
||||
bool missing = IsMissing(property);
|
||||
float baseH = EditorGUI.GetPropertyHeight(property, label, true);
|
||||
return missing ? baseH + EditorGUIUtility.singleLineHeight * 1.6f + 4f : baseH;
|
||||
}
|
||||
|
||||
private static bool IsMissing(SerializedProperty p)
|
||||
{
|
||||
switch (p.propertyType)
|
||||
{
|
||||
case SerializedPropertyType.ObjectReference:
|
||||
return p.objectReferenceValue == null;
|
||||
case SerializedPropertyType.String:
|
||||
return string.IsNullOrEmpty(p.stringValue);
|
||||
case SerializedPropertyType.ExposedReference:
|
||||
return p.exposedReferenceValue == null;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7fbd662bb1183a549b6b2c63b8fd86f6
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
189
Assets/_Game/Scripts/Editor/Modules/EventChannelModule.cs
Normal file
189
Assets/_Game/Scripts/Editor/Modules/EventChannelModule.cs
Normal file
@@ -0,0 +1,189 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace BaseGames.Editor.Modules
|
||||
{
|
||||
/// <summary>
|
||||
/// DataHub 事件频道反查模块 —— 列出项目中所有 EventChannelSO 子类资产;
|
||||
/// 选中后可扫描全项目中引用该频道的 Prefab / ScriptableObject,
|
||||
/// 解决"谁发布/谁订阅了某条事件"在大型项目中难以追踪的问题。
|
||||
///
|
||||
/// 反查实现:遍历所有 Prefab 与 SO 资产,用 AssetDatabase.GetDependencies
|
||||
/// 检查依赖链是否包含目标频道资产路径。首次扫描会耗时较长(取决于资产规模),
|
||||
/// 之后按选中频道增量计算。
|
||||
/// </summary>
|
||||
public class EventChannelModule : IDataModule, IDataModuleOrdered
|
||||
{
|
||||
public string ModuleId => "eventchannel";
|
||||
public string DisplayName => "事件频道";
|
||||
public string IconName => "d_AudioMixerController Icon";
|
||||
public int DisplayOrder => 200;
|
||||
|
||||
private ListView _channelList;
|
||||
private readonly List<EventChannelEntry> _allChannels = new();
|
||||
private ScriptableObject _selected;
|
||||
private VisualElement _refsContainer;
|
||||
|
||||
private struct EventChannelEntry
|
||||
{
|
||||
public string TypeName;
|
||||
public string AssetPath;
|
||||
public ScriptableObject Asset;
|
||||
}
|
||||
|
||||
public void Initialize() => Refresh();
|
||||
|
||||
public void OnActivated() => Refresh();
|
||||
|
||||
private void Refresh()
|
||||
{
|
||||
_allChannels.Clear();
|
||||
// 扫描所有 ScriptableObject 子类,筛选名称以 "EventChannelSO" 结尾的(项目约定)。
|
||||
var types = TypeCache.GetTypesDerivedFrom<ScriptableObject>();
|
||||
foreach (var t in types)
|
||||
{
|
||||
if (t.IsAbstract) continue;
|
||||
if (!t.Name.EndsWith("EventChannelSO", StringComparison.Ordinal)) continue;
|
||||
var guids = AssetDatabase.FindAssets($"t:{t.Name}");
|
||||
foreach (var g in guids)
|
||||
{
|
||||
var path = AssetDatabase.GUIDToAssetPath(g);
|
||||
var asset = AssetDatabase.LoadAssetAtPath<ScriptableObject>(path);
|
||||
if (asset == null) continue;
|
||||
_allChannels.Add(new EventChannelEntry
|
||||
{
|
||||
TypeName = t.Name,
|
||||
AssetPath = path,
|
||||
Asset = asset
|
||||
});
|
||||
}
|
||||
}
|
||||
_allChannels.Sort((a, b) =>
|
||||
{
|
||||
int c = string.Compare(a.TypeName, b.TypeName, StringComparison.Ordinal);
|
||||
return c != 0 ? c : string.Compare(a.AssetPath, b.AssetPath, StringComparison.Ordinal);
|
||||
});
|
||||
_channelList?.Rebuild();
|
||||
}
|
||||
|
||||
public void BuildListPane(VisualElement container, Action<UnityEngine.Object> onSelected)
|
||||
{
|
||||
var header = new Label($"频道总数:{_allChannels.Count}");
|
||||
header.style.unityFontStyleAndWeight = FontStyle.Bold;
|
||||
header.style.paddingLeft = 6;
|
||||
header.style.paddingTop = 4;
|
||||
container.Add(header);
|
||||
|
||||
_channelList = new ListView(_allChannels, 20,
|
||||
() => new Label { style = { paddingLeft = 6 } },
|
||||
(ve, i) =>
|
||||
{
|
||||
var entry = _allChannels[i];
|
||||
var label = (Label)ve;
|
||||
label.text = $"[{entry.TypeName}] {System.IO.Path.GetFileNameWithoutExtension(entry.AssetPath)}";
|
||||
label.tooltip = entry.AssetPath;
|
||||
});
|
||||
_channelList.style.flexGrow = 1;
|
||||
_channelList.selectionChanged += sel =>
|
||||
{
|
||||
var first = sel.FirstOrDefault();
|
||||
if (first is EventChannelEntry e)
|
||||
{
|
||||
_selected = e.Asset;
|
||||
onSelected?.Invoke(e.Asset);
|
||||
}
|
||||
};
|
||||
container.Add(_channelList);
|
||||
}
|
||||
|
||||
public void BuildDetailPane(VisualElement container, UnityEngine.Object selected)
|
||||
{
|
||||
_selected = selected as ScriptableObject;
|
||||
container.Clear();
|
||||
|
||||
if (_selected == null)
|
||||
{
|
||||
var hint = new Label("请在左侧选择一条频道资产。");
|
||||
hint.style.paddingLeft = 8; hint.style.paddingTop = 8;
|
||||
container.Add(hint);
|
||||
return;
|
||||
}
|
||||
|
||||
// Header
|
||||
var title = new Label($"{_selected.GetType().Name} · {_selected.name}");
|
||||
title.style.unityFontStyleAndWeight = FontStyle.Bold;
|
||||
title.style.fontSize = 14;
|
||||
title.style.paddingLeft = 8; title.style.paddingTop = 6;
|
||||
container.Add(title);
|
||||
|
||||
var pathLabel = new Label(AssetDatabase.GetAssetPath(_selected))
|
||||
{
|
||||
style = { paddingLeft = 8, paddingBottom = 6, color = new StyleColor(new Color(0.7f, 0.7f, 0.7f)) }
|
||||
};
|
||||
container.Add(pathLabel);
|
||||
|
||||
var scanBtn = new Button(() => ScanReferences(_selected))
|
||||
{ text = "扫描全项目引用(Prefab / ScriptableObject)" };
|
||||
scanBtn.style.marginLeft = 8; scanBtn.style.marginRight = 8;
|
||||
container.Add(scanBtn);
|
||||
|
||||
_refsContainer = new VisualElement();
|
||||
_refsContainer.style.paddingLeft = 8; _refsContainer.style.paddingTop = 6;
|
||||
container.Add(_refsContainer);
|
||||
}
|
||||
|
||||
private void ScanReferences(ScriptableObject channel)
|
||||
{
|
||||
if (channel == null || _refsContainer == null) return;
|
||||
_refsContainer.Clear();
|
||||
|
||||
var targetPath = AssetDatabase.GetAssetPath(channel);
|
||||
if (string.IsNullOrEmpty(targetPath)) return;
|
||||
|
||||
var found = new List<string>();
|
||||
try
|
||||
{
|
||||
EditorUtility.DisplayProgressBar("扫描引用", "正在扫描 Prefab 与 SO …", 0f);
|
||||
|
||||
var prefabGuids = AssetDatabase.FindAssets("t:Prefab");
|
||||
var soGuids = AssetDatabase.FindAssets("t:ScriptableObject");
|
||||
var all = prefabGuids.Concat(soGuids).ToArray();
|
||||
|
||||
for (int i = 0; i < all.Length; i++)
|
||||
{
|
||||
var p = AssetDatabase.GUIDToAssetPath(all[i]);
|
||||
if (p == targetPath) continue;
|
||||
if (i % 64 == 0)
|
||||
EditorUtility.DisplayProgressBar("扫描引用", p, (float)i / all.Length);
|
||||
|
||||
var deps = AssetDatabase.GetDependencies(p, recursive: false);
|
||||
if (System.Array.IndexOf(deps, targetPath) >= 0)
|
||||
found.Add(p);
|
||||
}
|
||||
}
|
||||
finally { EditorUtility.ClearProgressBar(); }
|
||||
|
||||
var summary = new Label($"找到 {found.Count} 个直接引用:");
|
||||
summary.style.unityFontStyleAndWeight = FontStyle.Bold;
|
||||
_refsContainer.Add(summary);
|
||||
|
||||
foreach (var path in found)
|
||||
{
|
||||
var btn = new Button(() =>
|
||||
{
|
||||
var obj = AssetDatabase.LoadMainAssetAtPath(path);
|
||||
EditorGUIUtility.PingObject(obj);
|
||||
Selection.activeObject = obj;
|
||||
})
|
||||
{ text = path };
|
||||
btn.style.unityTextAlign = TextAnchor.MiddleLeft;
|
||||
btn.style.marginTop = 2;
|
||||
_refsContainer.Add(btn);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9f9fe819b8566b04494f41329e5e37e9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,6 +1,10 @@
|
||||
using UnityEngine;
|
||||
using BaseGames.Core;
|
||||
using BaseGames.Core.Events;
|
||||
// 项目内存在两份同名枚举(Core.ColorblindMode 与 Core.Events.ColorblindMode)。
|
||||
// AccessibilitySettingsSO / ColorBlindFilter / ColorblindModeEventChannelSO 使用 Events 版本,
|
||||
// 因此在本文件中明确以 Events 版本消歧,避免 CS0104。
|
||||
using ColorblindMode = BaseGames.Core.Events.ColorblindMode;
|
||||
|
||||
namespace BaseGames.Support.Accessibility
|
||||
{
|
||||
|
||||
@@ -20,7 +20,9 @@
|
||||
"BaseGames.Combat.StatusEffects",
|
||||
"BaseGames.Spells",
|
||||
"BaseGames.Quest",
|
||||
"BaseGames.Skills"
|
||||
"BaseGames.Skills",
|
||||
"Unity.Addressables",
|
||||
"Unity.ResourceManager"
|
||||
],
|
||||
"autoReferenced": true,
|
||||
"overrideReferences": false,
|
||||
|
||||
95
Assets/_Game/Scripts/UI/CharmCardView.cs
Normal file
95
Assets/_Game/Scripts/UI/CharmCardView.cs
Normal file
@@ -0,0 +1,95 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using TMPro;
|
||||
using BaseGames.Equipment;
|
||||
using BaseGames.Localization;
|
||||
|
||||
namespace BaseGames.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 护符卡片视图组件。
|
||||
///
|
||||
/// 设计动机:取代 <see cref="CharmEquipPanel"/> 中基于
|
||||
/// <see cref="Component.GetComponentsInChildren{T}(bool)"/> + 数组索引的脆弱绑定方式,
|
||||
/// 通过 Inspector 显式序列化引用,避免每次重建列表时的反射开销与索引漂移风险。
|
||||
///
|
||||
/// 用法:在卡片 Prefab 上挂载此组件并连接子节点引用,<see cref="CharmEquipPanel"/>
|
||||
/// 仅需调用 <see cref="Bind"/> 即可完成数据 → UI 的同步。
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
public class CharmCardView : MonoBehaviour
|
||||
{
|
||||
[Header("Visual")]
|
||||
[SerializeField] private Image _icon;
|
||||
[SerializeField] private GameObject _iconRoot; // 可选:无 icon 时整体隐藏
|
||||
|
||||
[Header("Text")]
|
||||
[SerializeField] private TMP_Text _nameText;
|
||||
[SerializeField] private TMP_Text _notchCostText;
|
||||
[SerializeField] private TMP_Text _descriptionText;
|
||||
[SerializeField] private TMP_Text _equipBadgeText; // 已装备角标 ("✓")
|
||||
|
||||
[Header("Interaction")]
|
||||
[SerializeField] private Button _actionButton;
|
||||
|
||||
/// <summary>
|
||||
/// 将护符数据绑定到视图。
|
||||
/// </summary>
|
||||
/// <param name="charm">数据源。</param>
|
||||
/// <param name="isEquipped">是否已装备(决定按钮行为与角标显示)。</param>
|
||||
/// <param name="onEquip">点击装备时回调(仅 <paramref name="isEquipped"/> = false 时使用)。</param>
|
||||
/// <param name="onUnequip">点击卸下时回调(仅 <paramref name="isEquipped"/> = true 时使用)。</param>
|
||||
public void Bind(CharmSO charm,
|
||||
bool isEquipped,
|
||||
Action<CharmSO> onEquip,
|
||||
Action<CharmSO> onUnequip)
|
||||
{
|
||||
if (charm == null) return;
|
||||
|
||||
// ── 图标 ────────────────────────────────────────────────────────
|
||||
if (_icon != null)
|
||||
{
|
||||
_icon.sprite = charm.icon;
|
||||
_icon.enabled = charm.icon != null;
|
||||
}
|
||||
if (_iconRoot != null) _iconRoot.SetActive(charm.icon != null);
|
||||
|
||||
// ── 文本(本地化容错:未找到键时回落到 charmId / 空字符串)──────
|
||||
if (_nameText != null)
|
||||
{
|
||||
string loc = LocalizationManager.Get(charm.displayNameKey, LocalizationTable.Items);
|
||||
_nameText.text = !string.IsNullOrEmpty(loc) && loc != charm.displayNameKey
|
||||
? loc : charm.charmId;
|
||||
}
|
||||
if (_notchCostText != null)
|
||||
_notchCostText.text = charm.notchCost.ToString();
|
||||
|
||||
if (_descriptionText != null)
|
||||
{
|
||||
string loc = LocalizationManager.Get(charm.descriptionKey, LocalizationTable.Items);
|
||||
_descriptionText.text = !string.IsNullOrEmpty(loc) && loc != charm.descriptionKey
|
||||
? loc : string.Empty;
|
||||
}
|
||||
if (_equipBadgeText != null)
|
||||
_equipBadgeText.text = isEquipped ? "✓" : string.Empty;
|
||||
|
||||
// ── 按钮 ────────────────────────────────────────────────────────
|
||||
if (_actionButton != null)
|
||||
{
|
||||
_actionButton.onClick.RemoveAllListeners();
|
||||
var captured = charm;
|
||||
if (isEquipped)
|
||||
{
|
||||
if (onUnequip != null)
|
||||
_actionButton.onClick.AddListener(() => onUnequip(captured));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (onEquip != null)
|
||||
_actionButton.onClick.AddListener(() => onEquip(captured));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/UI/CharmCardView.cs.meta
Normal file
11
Assets/_Game/Scripts/UI/CharmCardView.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0aa9daa814d61c94aaed9b6db2ac1fdf
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -139,12 +139,38 @@ namespace BaseGames.UI
|
||||
go.transform.SetParent(parent, worldPositionStays: false);
|
||||
go.SetActive(true);
|
||||
|
||||
// Icon(第一个 Image)
|
||||
// 优先使用预绑定视图组件(推荐);若 Prefab 未挂载则回落到反射查找以保证向后兼容。
|
||||
var view = go.GetComponent<CharmCardView>();
|
||||
if (view != null)
|
||||
{
|
||||
view.Bind(charm, isEquipped, OnEquipClicked, OnUnequipClicked);
|
||||
}
|
||||
else
|
||||
{
|
||||
FallbackBindByReflection(go, charm, isEquipped);
|
||||
}
|
||||
|
||||
return go;
|
||||
}
|
||||
|
||||
private void OnEquipClicked(CharmSO charm)
|
||||
{
|
||||
string err = _manager?.TryEquipCharm(charm);
|
||||
if (err != null) Debug.LogWarning($"[CharmEquipPanel] 装备失败: {err}");
|
||||
}
|
||||
|
||||
private void OnUnequipClicked(CharmSO charm) => _manager?.UnequipCharm(charm);
|
||||
|
||||
/// <summary>
|
||||
/// 兼容旧 Prefab(未挂载 <see cref="CharmCardView"/>)的反射绑定。
|
||||
/// 仅作为过渡方案;首次开发完成后建议在 Editor 中强制要求新组件。
|
||||
/// </summary>
|
||||
private void FallbackBindByReflection(GameObject go, CharmSO charm, bool isEquipped)
|
||||
{
|
||||
var images = go.GetComponentsInChildren<Image>(includeInactive: true);
|
||||
if (images.Length > 0 && charm.icon != null)
|
||||
images[0].sprite = charm.icon;
|
||||
|
||||
// 文本(约定:TMP_Text[0] = 名称, TMP_Text[1] = 凹槽消耗, TMP_Text[2] = 描述)
|
||||
var texts = go.GetComponentsInChildren<TMP_Text>(includeInactive: true);
|
||||
if (texts.Length > 0)
|
||||
{
|
||||
@@ -152,9 +178,7 @@ namespace BaseGames.UI
|
||||
texts[0].text = string.IsNullOrEmpty(name) || name == charm.displayNameKey
|
||||
? charm.charmId : name;
|
||||
}
|
||||
if (texts.Length > 1)
|
||||
texts[1].text = charm.notchCost.ToString();
|
||||
|
||||
if (texts.Length > 1) texts[1].text = charm.notchCost.ToString();
|
||||
if (texts.Length > 2)
|
||||
{
|
||||
string desc = LocalizationManager.Get(charm.descriptionKey, LocalizationTable.Items);
|
||||
@@ -162,34 +186,18 @@ namespace BaseGames.UI
|
||||
? string.Empty : desc;
|
||||
}
|
||||
|
||||
// 按钮(约定:第一个 Button = 装备/卸下切换)
|
||||
var btn = go.GetComponentInChildren<Button>(includeInactive: true);
|
||||
if (btn != null)
|
||||
{
|
||||
btn.onClick.RemoveAllListeners();
|
||||
CharmSO captured = charm;
|
||||
bool equip = !isEquipped;
|
||||
|
||||
if (equip)
|
||||
{
|
||||
btn.onClick.AddListener(() =>
|
||||
{
|
||||
string err = _manager?.TryEquipCharm(captured);
|
||||
if (err != null)
|
||||
Debug.LogWarning($"[CharmEquipPanel] 装备失败: {err}");
|
||||
});
|
||||
}
|
||||
if (!isEquipped)
|
||||
btn.onClick.AddListener(() => OnEquipClicked(captured));
|
||||
else
|
||||
{
|
||||
btn.onClick.AddListener(() => _manager?.UnequipCharm(captured));
|
||||
}
|
||||
|
||||
// 已装备时使用不同视觉(可选:标记文本)
|
||||
btn.onClick.AddListener(() => OnUnequipClicked(captured));
|
||||
if (texts.Length > 3)
|
||||
texts[3].text = isEquipped ? "✓" : string.Empty;
|
||||
}
|
||||
|
||||
return go;
|
||||
}
|
||||
|
||||
// ── 回收 ─────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -9,8 +9,8 @@ namespace BaseGames.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 伤害飘字(架构 10_UIModule §10)。
|
||||
/// 从对象池取出后由 FloatingDamageSpawner 调用 Show();协程完成后归还。
|
||||
/// 挂在预制体根 GameObject 上(Canvas_HUD 子节点,World Space Canvas 或 Screen Space)。
|
||||
/// 从对象池取出后由 FloatingDamageSpawner 调用 Show();
|
||||
/// 动画推进由 <see cref="FloatingDamageTickSystem"/> 统一调度,避免大量独立协程。
|
||||
/// </summary>
|
||||
public class FloatingDamageText : MonoBehaviour
|
||||
{
|
||||
@@ -26,8 +26,7 @@ namespace BaseGames.UI
|
||||
[SerializeField] private Canvas _parentCanvas;
|
||||
|
||||
private RectTransform _rectTransform;
|
||||
private Coroutine _animCoroutine;
|
||||
// 每次 Show() 解析一次,协程期间(< 1s)复用,避免每帧走 FindObjectByTag
|
||||
// 每次 Show() 解析一次,动画期间(< 1s)复用,避免每帧走 FindObjectByTag
|
||||
private Camera _cachedCamera;
|
||||
|
||||
private void Awake()
|
||||
@@ -36,15 +35,15 @@ namespace BaseGames.UI
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在世界坐标位置显示伤害数字并开始飘动动画。
|
||||
/// 动画结束后由对象池回收(Deactivate)。
|
||||
/// 在世界坐标位置显示伤害数字并启动飘动动画。
|
||||
/// 动画结束后由 OnTickComplete 回调列表 SetActive(false)。
|
||||
/// </summary>
|
||||
public void Show(Vector2 worldPosition, int damage, DamageType type)
|
||||
{
|
||||
if (_animCoroutine != null) StopCoroutine(_animCoroutine);
|
||||
// 取消上一条动画(如果被预期复用)
|
||||
FloatingDamageTickSystem.Instance.Unregister(this);
|
||||
|
||||
// 每次 Show 解析一次摄像机:动画时长 < 1s,期间不会切换主摄像机;
|
||||
// 若 Boss 过场后再次 Show,会自动获取新的主摄像机。
|
||||
// 每次 Show 解析一次摄像机
|
||||
_cachedCamera = (_parentCanvas != null && _parentCanvas.renderMode == RenderMode.ScreenSpaceCamera)
|
||||
? _parentCanvas.worldCamera
|
||||
: UnityEngine.Camera.main;
|
||||
@@ -53,9 +52,22 @@ namespace BaseGames.UI
|
||||
_text.color = GetColorForType(type);
|
||||
|
||||
SetAnchoredPosition(worldPosition);
|
||||
_animCoroutine = StartCoroutine(FloatAndFade(worldPosition));
|
||||
FloatingDamageTickSystem.Instance.Register(this, worldPosition,
|
||||
_duration, _floatDistance, _text.color.a);
|
||||
}
|
||||
|
||||
/// <summary>由 <see cref="FloatingDamageTickSystem"/> 每帧调用,t 为归一化进度 [0,1]。</summary>
|
||||
internal void TickAnimation(Vector2 startWorld, float t, float startAlpha, float floatDistance)
|
||||
{
|
||||
SetAnchoredPosition(startWorld + new Vector2(0, floatDistance * t));
|
||||
var color = _text.color;
|
||||
color.a = Mathf.Lerp(startAlpha, 0f, Mathf.Clamp01((t - 0.5f) / 0.5f));
|
||||
_text.color = color;
|
||||
}
|
||||
|
||||
/// <summary>动画结束回调:反激活 GameObject 归还池。</summary>
|
||||
internal void OnTickComplete() => gameObject.SetActive(false);
|
||||
|
||||
private void SetAnchoredPosition(Vector2 worldPosition)
|
||||
{
|
||||
var cam = _cachedCamera;
|
||||
@@ -82,6 +94,7 @@ namespace BaseGames.UI
|
||||
|
||||
private IEnumerator FloatAndFade(Vector2 startWorld)
|
||||
{
|
||||
// 保留作为 fallback:若业务方选择继续使用独立协程可直接调用本方法。
|
||||
float elapsed = 0f;
|
||||
var color = _text.color;
|
||||
var startAlpha = color.a;
|
||||
@@ -89,18 +102,11 @@ namespace BaseGames.UI
|
||||
while (elapsed < _duration)
|
||||
{
|
||||
float t = elapsed / _duration;
|
||||
|
||||
SetAnchoredPosition(startWorld + new Vector2(0, _floatDistance * t));
|
||||
|
||||
// alpha 淡出(后半段开始)—— 修改 struct 的 a 分量并回写,避免每帧 new Color 堆分配
|
||||
color.a = Mathf.Lerp(startAlpha, 0f, Mathf.Clamp01((t - 0.5f) / 0.5f));
|
||||
_text.color = color;
|
||||
|
||||
TickAnimation(startWorld, t, startAlpha, _floatDistance);
|
||||
elapsed += Time.deltaTime;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
gameObject.SetActive(false); // 归还对象池
|
||||
OnTickComplete();
|
||||
}
|
||||
|
||||
private static Color GetColorForType(DamageType type) => type switch
|
||||
|
||||
102
Assets/_Game/Scripts/UI/FloatingDamageTickSystem.cs
Normal file
102
Assets/_Game/Scripts/UI/FloatingDamageTickSystem.cs
Normal file
@@ -0,0 +1,102 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 全局伤害飘字 Tick 系统(架构 10_UIModule §10 优化)。
|
||||
///
|
||||
/// 设计动机:原实现中每条飘字独立 StartCoroutine(FloatAndFade),
|
||||
/// Boss 战或大范围 AOE 时同时存在数十条会产生大量协程对象与调度开销。
|
||||
/// 本系统用单一 Update + 紧凑 List<Entry> 集中推进,
|
||||
/// 所有飘字共享同一份每帧调度(仅一次 Update 调用),并消除协程对象。
|
||||
///
|
||||
/// 使用:FloatingDamageText.Show 不再 StartCoroutine,而是调用
|
||||
/// FloatingDamageTickSystem.Register(this, ...);系统自动在动画结束时
|
||||
/// 调用 OnComplete 回调(飘字 SetActive(false) 回池)。
|
||||
/// </summary>
|
||||
[DefaultExecutionOrder(+100)]
|
||||
public class FloatingDamageTickSystem : MonoBehaviour
|
||||
{
|
||||
private struct Entry
|
||||
{
|
||||
public FloatingDamageText Target;
|
||||
public Vector2 StartWorld;
|
||||
public float Elapsed;
|
||||
public float Duration;
|
||||
public float FloatDistance;
|
||||
public float StartAlpha;
|
||||
}
|
||||
|
||||
private static FloatingDamageTickSystem s_instance;
|
||||
private readonly List<Entry> _entries = new(64);
|
||||
|
||||
public static FloatingDamageTickSystem Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
if (s_instance != null) return s_instance;
|
||||
var go = new GameObject(nameof(FloatingDamageTickSystem));
|
||||
DontDestroyOnLoad(go);
|
||||
s_instance = go.AddComponent<FloatingDamageTickSystem>();
|
||||
return s_instance;
|
||||
}
|
||||
}
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (s_instance != null && s_instance != this) { Destroy(this); return; }
|
||||
s_instance = this;
|
||||
}
|
||||
|
||||
public void Register(FloatingDamageText target, Vector2 startWorld,
|
||||
float duration, float floatDistance, float startAlpha)
|
||||
{
|
||||
if (target == null || duration <= 0f) return;
|
||||
_entries.Add(new Entry
|
||||
{
|
||||
Target = target,
|
||||
StartWorld = startWorld,
|
||||
Elapsed = 0f,
|
||||
Duration = duration,
|
||||
FloatDistance = floatDistance,
|
||||
StartAlpha = startAlpha
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>取消指定目标的飘字动画(例如对象池回收)。</summary>
|
||||
public void Unregister(FloatingDamageText target)
|
||||
{
|
||||
for (int i = _entries.Count - 1; i >= 0; i--)
|
||||
if (_entries[i].Target == target) _entries.RemoveAt(i);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
float dt = Time.deltaTime;
|
||||
// 倒序遍历,便于安全 RemoveAt
|
||||
for (int i = _entries.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var e = _entries[i];
|
||||
if (e.Target == null) { _entries.RemoveAt(i); continue; }
|
||||
|
||||
e.Elapsed += dt;
|
||||
float t = e.Elapsed / e.Duration;
|
||||
bool done = t >= 1f;
|
||||
if (done) t = 1f;
|
||||
|
||||
e.Target.TickAnimation(e.StartWorld, t, e.StartAlpha, e.FloatDistance);
|
||||
|
||||
if (done)
|
||||
{
|
||||
e.Target.OnTickComplete();
|
||||
_entries.RemoveAt(i);
|
||||
}
|
||||
else
|
||||
{
|
||||
_entries[i] = e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/UI/FloatingDamageTickSystem.cs.meta
Normal file
11
Assets/_Game/Scripts/UI/FloatingDamageTickSystem.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f56b6630ca8a34944aa71535b4c68574
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -131,21 +131,11 @@ namespace BaseGames.UI
|
||||
// ── 动画协程 ──────────────────────────────────────────────────────────
|
||||
|
||||
private IEnumerator SlideTo(Vector2 target)
|
||||
{
|
||||
Vector2 start = _rect.anchoredPosition;
|
||||
float t = 0;
|
||||
while (t < _slideDuration)
|
||||
{
|
||||
_rect.anchoredPosition = Vector2.Lerp(start, target, t / _slideDuration);
|
||||
t += Time.unscaledDeltaTime;
|
||||
yield return null;
|
||||
}
|
||||
_rect.anchoredPosition = target;
|
||||
}
|
||||
=> UITween.MoveAnchored(_rect, target, _slideDuration);
|
||||
|
||||
private IEnumerator SlideOut()
|
||||
{
|
||||
yield return SlideTo(_hiddenPos);
|
||||
yield return UITween.MoveAnchored(_rect, _hiddenPos, _slideDuration);
|
||||
gameObject.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using Unity.Profiling;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using TMPro;
|
||||
@@ -47,6 +48,10 @@ namespace BaseGames.UI.HUD
|
||||
private readonly CompositeDisposable _subs = new();
|
||||
private int _lastLingZhu = int.MinValue;
|
||||
|
||||
// ── 性能基线 (Profiler 窗口可在 "Scripts" 类别下看到) ──
|
||||
private static readonly ProfilerMarker s_RebuildHPMarker = new("HUD.RebuildHPCells");
|
||||
private static readonly ProfilerMarker s_RebuildSpringMarker = new("HUD.RebuildSpringIcons");
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
_onHPChanged?.Subscribe(UpdateHP).AddTo(_subs);
|
||||
@@ -69,6 +74,7 @@ namespace BaseGames.UI.HUD
|
||||
|
||||
private void RebuildHPCells(int max)
|
||||
{
|
||||
using var _ = s_RebuildHPMarker.Auto();
|
||||
if (_hpContainer == null || _hpCellPrefab == null) return;
|
||||
// 复用现有 Cell,仅在数量不足时 Instantiate 补充,超出时 SetActive(false) 而非 Destroy
|
||||
for (int i = 0; i < max; i++)
|
||||
@@ -101,6 +107,7 @@ namespace BaseGames.UI.HUD
|
||||
|
||||
private void RebuildSpringIcons(int charges)
|
||||
{
|
||||
using var _ = s_RebuildSpringMarker.Auto();
|
||||
if (_springContainer == null || _springIconPrefab == null) return;
|
||||
// 复用已有图标,超出数量时 SetActive(false)
|
||||
for (int i = 0; i < charges; i++)
|
||||
|
||||
@@ -68,11 +68,26 @@ namespace BaseGames.UI
|
||||
|
||||
// ── 内部 ─────────────────────────────────────────────────────────────
|
||||
|
||||
// 缓存等待对象以避免典型路径(剩余时间 == _minDisplayTime)下的重复分配。
|
||||
private WaitForSecondsRealtime _cachedFullWait;
|
||||
|
||||
private IEnumerator HideAfterMinTime()
|
||||
{
|
||||
float elapsed = Time.unscaledTime - _shownAt;
|
||||
if (elapsed < _minDisplayTime)
|
||||
yield return new WaitForSecondsRealtime(_minDisplayTime - elapsed);
|
||||
float elapsed = Time.unscaledTime - _shownAt;
|
||||
float remaining = _minDisplayTime - elapsed;
|
||||
if (remaining > 0f)
|
||||
{
|
||||
// 完整剩余 ≈ 显示时间时复用缓存对象;否则按需新建(罕见路径)。
|
||||
if (Mathf.Approximately(remaining, _minDisplayTime))
|
||||
{
|
||||
_cachedFullWait ??= new WaitForSecondsRealtime(_minDisplayTime);
|
||||
yield return _cachedFullWait;
|
||||
}
|
||||
else
|
||||
{
|
||||
yield return new WaitForSecondsRealtime(remaining);
|
||||
}
|
||||
}
|
||||
if (_loadingRoot != null) _loadingRoot.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,12 @@ namespace BaseGames.UI
|
||||
[SerializeField] private Toggle _vSyncToggle;
|
||||
[SerializeField] private TMP_Dropdown _fpsDropdown; // 30 / 60 / 120 / 无限
|
||||
|
||||
[Header("可访问性")]
|
||||
[SerializeField] private Slider _uiScaleSlider; // 0.8 ~ 1.5
|
||||
[SerializeField] private TMP_Text _uiScaleValueText; // 实时显示 "100%"
|
||||
[SerializeField] private TMP_Dropdown _colorblindDropdown; // None / Prot / Deut / Trit
|
||||
[SerializeField] private Toggle _screenShakeToggle;
|
||||
|
||||
[Header("按键重绑定")]
|
||||
[SerializeField] private GameObject _rebindPanelRoot; // RebindPanel GameObject
|
||||
|
||||
@@ -59,10 +65,46 @@ namespace BaseGames.UI
|
||||
_settings.SetTargetFrameRate(FpsOptions[Mathf.Clamp(i, 0, FpsOptions.Length - 1)]));
|
||||
}
|
||||
|
||||
// ── 可访问性 ──────────────────────────────────────────────────────
|
||||
if (_uiScaleSlider != null)
|
||||
{
|
||||
_uiScaleSlider.onValueChanged.RemoveAllListeners();
|
||||
_uiScaleSlider.minValue = 0.8f;
|
||||
_uiScaleSlider.maxValue = 1.5f;
|
||||
_uiScaleSlider.value = Mathf.Clamp(data.UIScale, _uiScaleSlider.minValue, _uiScaleSlider.maxValue);
|
||||
UpdateUIScaleLabel(_uiScaleSlider.value);
|
||||
_uiScaleSlider.onValueChanged.AddListener(v =>
|
||||
{
|
||||
_settings.SetUIScale(v);
|
||||
UpdateUIScaleLabel(v);
|
||||
});
|
||||
}
|
||||
|
||||
if (_colorblindDropdown != null)
|
||||
{
|
||||
_colorblindDropdown.onValueChanged.RemoveAllListeners();
|
||||
_colorblindDropdown.value = (int)data.ColorblindMode;
|
||||
_colorblindDropdown.onValueChanged.AddListener(i =>
|
||||
_settings.SetColorblindMode((ColorblindMode)Mathf.Clamp(i, 0, 3)));
|
||||
}
|
||||
|
||||
if (_screenShakeToggle != null)
|
||||
{
|
||||
_screenShakeToggle.onValueChanged.RemoveAllListeners();
|
||||
_screenShakeToggle.isOn = data.ScreenShakeEnabled;
|
||||
_screenShakeToggle.onValueChanged.AddListener(v => _settings.SetScreenShakeEnabled(v));
|
||||
}
|
||||
|
||||
// 手柄导航:打开设置面板时将焦点置于主音量滑条
|
||||
EventSystem.current?.SetSelectedGameObject(_masterVolume?.gameObject);
|
||||
}
|
||||
|
||||
private void UpdateUIScaleLabel(float v)
|
||||
{
|
||||
if (_uiScaleValueText != null)
|
||||
_uiScaleValueText.text = Mathf.RoundToInt(v * 100f) + "%";
|
||||
}
|
||||
|
||||
// ── 辅助 ──────────────────────────────────────────────────────────────
|
||||
|
||||
private static void InitSlider(Slider slider, float value, UnityEngine.Events.UnityAction<float> onChange)
|
||||
|
||||
128
Assets/_Game/Scripts/UI/SubtitleManager.cs
Normal file
128
Assets/_Game/Scripts/UI/SubtitleManager.cs
Normal file
@@ -0,0 +1,128 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using BaseGames.Core;
|
||||
using BaseGames.Core.Events;
|
||||
|
||||
namespace BaseGames.UI
|
||||
{
|
||||
/// <summary>屏幕阅读器优先级(与 WCAG / ARIA live-region 等级对齐)。</summary>
|
||||
public enum ScreenReaderPriority
|
||||
{
|
||||
/// <summary>Polite —— 当前播报结束后再播报,不打断玩家。</summary>
|
||||
Polite = 0,
|
||||
/// <summary>Assertive —— 立即打断当前播报(如战斗死亡、关键提示)。</summary>
|
||||
Assertive = 1
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 屏幕阅读器接口(无障碍)。默认无实现;接入第三方 TTS / 平台 API(iOS VoiceOver、
|
||||
/// Android TalkBack、Steam Overlay TTS)时实现本接口并通过 ServiceLocator 注册。
|
||||
/// 设计为 string-only 接口而非富文本,避免不同 TTS 引擎对标签解析不一致。
|
||||
/// </summary>
|
||||
public interface IScreenReader
|
||||
{
|
||||
void Speak(string text, ScreenReaderPriority priority = ScreenReaderPriority.Polite);
|
||||
void StopAll();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 字幕管理器(架构 10_UIModule §11 无障碍补完)。
|
||||
///
|
||||
/// 职责:与对话气泡(DialogueUI)解耦,专门承担"叙述性字幕 / 关键提示文本 / 战斗播报"。
|
||||
/// 订阅 <see cref="StringEventChannelSO"/>(推荐项目内新建 EVT_SubtitleShow)显示底部居中字幕;
|
||||
/// 若 <see cref="ServiceLocator"/> 中存在 <see cref="IScreenReader"/> 实现则同步触发 TTS。
|
||||
///
|
||||
/// 队列策略:Polite 串行(FIFO);Assertive 立即清空队列并显示。
|
||||
/// </summary>
|
||||
public class SubtitleManager : MonoBehaviour
|
||||
{
|
||||
[Header("UI")]
|
||||
[SerializeField] private GameObject _root;
|
||||
[SerializeField] private TMP_Text _label;
|
||||
[SerializeField] private CanvasGroup _canvasGroup;
|
||||
|
||||
[Header("时长")]
|
||||
[Tooltip("每行字幕显示时间(秒);可被 Show(text, durationOverride) 覆盖。")]
|
||||
[SerializeField] private float _defaultDuration = 3f;
|
||||
[SerializeField] private float _fadeDuration = 0.2f;
|
||||
|
||||
[Header("事件频道(可选)")]
|
||||
[SerializeField] private StringEventChannelSO _onSubtitleShow;
|
||||
|
||||
private readonly Queue<(string text, float duration, ScreenReaderPriority pri)> _queue = new();
|
||||
private Coroutine _playCo;
|
||||
private readonly CompositeDisposable _subs = new();
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (_root != null) _root.SetActive(false);
|
||||
if (_canvasGroup != null) _canvasGroup.alpha = 0f;
|
||||
_onSubtitleShow?.Subscribe(t => Show(t, _defaultDuration, ScreenReaderPriority.Polite)).AddTo(_subs);
|
||||
}
|
||||
|
||||
private void OnDisable() => _subs.Clear();
|
||||
|
||||
/// <summary>显示一行字幕;自动按优先级排队。</summary>
|
||||
public void Show(string text, float duration = -1f,
|
||||
ScreenReaderPriority priority = ScreenReaderPriority.Polite)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text)) return;
|
||||
float d = duration > 0f ? duration : _defaultDuration;
|
||||
|
||||
if (priority == ScreenReaderPriority.Assertive)
|
||||
{
|
||||
_queue.Clear();
|
||||
if (_playCo != null) StopCoroutine(_playCo);
|
||||
_playCo = null;
|
||||
}
|
||||
_queue.Enqueue((text, d, priority));
|
||||
|
||||
// 同步触发 TTS
|
||||
var reader = ServiceLocator.GetOrDefault<IScreenReader>(null);
|
||||
reader?.Speak(text, priority);
|
||||
|
||||
if (_playCo == null) _playCo = StartCoroutine(PlayLoop());
|
||||
}
|
||||
|
||||
private IEnumerator PlayLoop()
|
||||
{
|
||||
while (_queue.Count > 0)
|
||||
{
|
||||
var (text, d, _) = _queue.Dequeue();
|
||||
yield return ShowOne(text, d);
|
||||
}
|
||||
_playCo = null;
|
||||
}
|
||||
|
||||
private IEnumerator ShowOne(string text, float duration)
|
||||
{
|
||||
if (_label != null) _label.text = text;
|
||||
if (_root != null) _root.SetActive(true);
|
||||
|
||||
yield return Fade(0f, 1f, _fadeDuration);
|
||||
yield return new WaitForSecondsRealtime(Mathf.Max(0f, duration - _fadeDuration * 2f));
|
||||
yield return Fade(1f, 0f, _fadeDuration);
|
||||
|
||||
if (_root != null) _root.SetActive(false);
|
||||
}
|
||||
|
||||
private IEnumerator Fade(float from, float to, float dur)
|
||||
{
|
||||
if (_canvasGroup == null || dur <= 0f)
|
||||
{
|
||||
if (_canvasGroup != null) _canvasGroup.alpha = to;
|
||||
yield break;
|
||||
}
|
||||
float t = 0f;
|
||||
while (t < dur)
|
||||
{
|
||||
t += Time.unscaledDeltaTime;
|
||||
_canvasGroup.alpha = Mathf.Lerp(from, to, t / dur);
|
||||
yield return null;
|
||||
}
|
||||
_canvasGroup.alpha = to;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/UI/SubtitleManager.cs.meta
Normal file
11
Assets/_Game/Scripts/UI/SubtitleManager.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e2dd2823178ffa347970cb5ad9313049
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/_Game/Scripts/UI/Theme.meta
Normal file
8
Assets/_Game/Scripts/UI/Theme.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 321bbb08e4696894bbd9b34e868b223d
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
99
Assets/_Game/Scripts/UI/Theme/UIThemeApplier.cs
Normal file
99
Assets/_Game/Scripts/UI/Theme/UIThemeApplier.cs
Normal file
@@ -0,0 +1,99 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using TMPro;
|
||||
|
||||
namespace BaseGames.UI.Theme
|
||||
{
|
||||
/// <summary>
|
||||
/// 将 <see cref="UIThemeSO"/> 中的视觉令牌应用到当前 GameObject 及其子节点。
|
||||
///
|
||||
/// 用法:
|
||||
/// 1. 在 Prefab 根节点挂载此组件并指定 <see cref="_theme"/>;
|
||||
/// 2. 在每个需要主题化的子节点添加 <see cref="UIThemeRole"/> 组件指定角色;
|
||||
/// 3. <see cref="Apply"/> 会被 <see cref="OnEnable"/> 自动调用一次,
|
||||
/// 也可在主题切换后手动调用。
|
||||
///
|
||||
/// 性能:使用 <see cref="Component.GetComponentsInChildren{T}(bool)"/> 一次性收集,
|
||||
/// 仅在 Enable / 显式刷新时执行;运行时无每帧成本。
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
public class UIThemeApplier : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private UIThemeSO _theme;
|
||||
[Tooltip("启用时自动应用一次。运行时切换主题可手动调用 Apply()。")]
|
||||
[SerializeField] private bool _applyOnEnable = true;
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (_applyOnEnable) Apply();
|
||||
}
|
||||
|
||||
/// <summary>用当前 <see cref="_theme"/> 覆盖子节点上所有 <see cref="UIThemeRole"/> 标记。</summary>
|
||||
public void Apply()
|
||||
{
|
||||
if (_theme == null) return;
|
||||
|
||||
var roles = GetComponentsInChildren<UIThemeRole>(includeInactive: true);
|
||||
for (int i = 0; i < roles.Length; i++)
|
||||
ApplyRole(roles[i], _theme);
|
||||
}
|
||||
|
||||
/// <summary>运行时切换主题。</summary>
|
||||
public void SetTheme(UIThemeSO theme)
|
||||
{
|
||||
_theme = theme;
|
||||
Apply();
|
||||
}
|
||||
|
||||
private static void ApplyRole(UIThemeRole role, UIThemeSO theme)
|
||||
{
|
||||
if (role == null) return;
|
||||
switch (role.Kind)
|
||||
{
|
||||
case UIThemeRoleKind.Graphic_Primary: SetGraphicColor(role, theme.Primary); break;
|
||||
case UIThemeRoleKind.Graphic_Secondary: SetGraphicColor(role, theme.Secondary); break;
|
||||
case UIThemeRoleKind.Graphic_Accent: SetGraphicColor(role, theme.Accent); break;
|
||||
case UIThemeRoleKind.Graphic_Background: SetGraphicColor(role, theme.Background); break;
|
||||
case UIThemeRoleKind.Graphic_Success: SetGraphicColor(role, theme.Success); break;
|
||||
case UIThemeRoleKind.Graphic_Warning: SetGraphicColor(role, theme.Warning); break;
|
||||
case UIThemeRoleKind.Graphic_Danger: SetGraphicColor(role, theme.Danger); break;
|
||||
|
||||
case UIThemeRoleKind.Text_Primary: SetTextStyle(role, theme.TextPrimary, theme.BodyFont, theme.BodyFontSize); break;
|
||||
case UIThemeRoleKind.Text_Secondary: SetTextStyle(role, theme.TextSecondary, theme.BodyFont, theme.SmallFontSize); break;
|
||||
case UIThemeRoleKind.Text_Header: SetTextStyle(role, theme.TextPrimary, theme.HeaderFont, theme.HeaderFontSize); break;
|
||||
case UIThemeRoleKind.Text_Disabled: SetTextStyle(role, theme.TextDisabled, theme.BodyFont, theme.BodyFontSize); break;
|
||||
|
||||
case UIThemeRoleKind.Button: SetButtonColors(role, theme); break;
|
||||
}
|
||||
}
|
||||
|
||||
private static void SetGraphicColor(UIThemeRole role, Color c)
|
||||
{
|
||||
var g = role.GetComponent<Graphic>();
|
||||
if (g != null) g.color = c;
|
||||
}
|
||||
|
||||
private static void SetTextStyle(UIThemeRole role, Color c, TMP_FontAsset font, float size)
|
||||
{
|
||||
var tmp = role.GetComponent<TMP_Text>();
|
||||
if (tmp != null)
|
||||
{
|
||||
tmp.color = c;
|
||||
if (font != null) tmp.font = font;
|
||||
if (role.OverrideFontSize) tmp.fontSize = size;
|
||||
}
|
||||
}
|
||||
|
||||
private static void SetButtonColors(UIThemeRole role, UIThemeSO theme)
|
||||
{
|
||||
var btn = role.GetComponent<Button>();
|
||||
if (btn == null) return;
|
||||
var cb = btn.colors;
|
||||
cb.normalColor = theme.ButtonNormal;
|
||||
cb.highlightedColor = theme.ButtonHighlighted;
|
||||
cb.pressedColor = theme.ButtonPressed;
|
||||
cb.disabledColor = theme.ButtonDisabled;
|
||||
btn.colors = cb;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/UI/Theme/UIThemeApplier.cs.meta
Normal file
11
Assets/_Game/Scripts/UI/Theme/UIThemeApplier.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 50572ad108fd3354da04ea8fba66370b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
39
Assets/_Game/Scripts/UI/Theme/UIThemeRole.cs
Normal file
39
Assets/_Game/Scripts/UI/Theme/UIThemeRole.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.UI.Theme
|
||||
{
|
||||
/// <summary>主题角色种类。控制 <see cref="UIThemeApplier"/> 应用何种令牌到目标组件。</summary>
|
||||
public enum UIThemeRoleKind
|
||||
{
|
||||
// Graphic(Image / RawImage / Panel 等)
|
||||
Graphic_Primary,
|
||||
Graphic_Secondary,
|
||||
Graphic_Accent,
|
||||
Graphic_Background,
|
||||
Graphic_Success,
|
||||
Graphic_Warning,
|
||||
Graphic_Danger,
|
||||
|
||||
// TMP_Text
|
||||
Text_Primary,
|
||||
Text_Secondary,
|
||||
Text_Header,
|
||||
Text_Disabled,
|
||||
|
||||
// Button(应用 ColorBlock)
|
||||
Button,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 标记组件:告诉 <see cref="UIThemeApplier"/> 当前节点扮演的视觉角色。
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
public class UIThemeRole : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private UIThemeRoleKind _kind = UIThemeRoleKind.Text_Primary;
|
||||
[SerializeField] private bool _overrideFontSize = false;
|
||||
|
||||
public UIThemeRoleKind Kind => _kind;
|
||||
public bool OverrideFontSize => _overrideFontSize;
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/UI/Theme/UIThemeRole.cs.meta
Normal file
11
Assets/_Game/Scripts/UI/Theme/UIThemeRole.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1fa7f5f3910b010489199ef7ddee773e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
46
Assets/_Game/Scripts/UI/Theme/UIThemeSO.cs
Normal file
46
Assets/_Game/Scripts/UI/Theme/UIThemeSO.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using UnityEngine;
|
||||
using TMPro;
|
||||
|
||||
namespace BaseGames.UI.Theme
|
||||
{
|
||||
/// <summary>
|
||||
/// 全局 UI 主题资产(架构 10_UIModule §1 视觉风格统一)。
|
||||
///
|
||||
/// 设计动机:避免每个 Prefab 各自硬编码颜色 / 字体,统一替换或本地化主题成本极低。
|
||||
/// 通过 <see cref="UIThemeApplier"/> 在运行时拉取并应用到子节点。
|
||||
///
|
||||
/// 资产路径建议:Assets/Data/UI/Themes/UI_Theme_Default.asset
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "BaseGames/UI/Theme")]
|
||||
public class UIThemeSO : ScriptableObject
|
||||
{
|
||||
[Header("Palette")]
|
||||
public Color Primary = new Color(0.10f, 0.55f, 0.95f);
|
||||
public Color Secondary = new Color(0.20f, 0.20f, 0.25f);
|
||||
public Color Accent = new Color(1.00f, 0.78f, 0.20f);
|
||||
public Color Background = new Color(0.06f, 0.07f, 0.10f);
|
||||
public Color TextPrimary = Color.white;
|
||||
public Color TextSecondary = new Color(0.75f, 0.78f, 0.82f);
|
||||
public Color TextDisabled = new Color(0.40f, 0.42f, 0.45f);
|
||||
public Color Success = new Color(0.30f, 0.85f, 0.45f);
|
||||
public Color Warning = new Color(0.95f, 0.70f, 0.10f);
|
||||
public Color Danger = new Color(0.95f, 0.30f, 0.30f);
|
||||
|
||||
[Header("Typography")]
|
||||
public TMP_FontAsset HeaderFont;
|
||||
public TMP_FontAsset BodyFont;
|
||||
[Min(8)] public float HeaderFontSize = 36f;
|
||||
[Min(8)] public float BodyFontSize = 20f;
|
||||
[Min(8)] public float SmallFontSize = 14f;
|
||||
|
||||
[Header("Button States")]
|
||||
public Color ButtonNormal = new Color(0.10f, 0.55f, 0.95f);
|
||||
public Color ButtonHighlighted = new Color(0.20f, 0.65f, 1.00f);
|
||||
public Color ButtonPressed = new Color(0.05f, 0.40f, 0.80f);
|
||||
public Color ButtonDisabled = new Color(0.30f, 0.30f, 0.32f);
|
||||
|
||||
[Header("Audio (可选)")]
|
||||
public AudioClip ClickSound;
|
||||
public AudioClip HoverSound;
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/UI/Theme/UIThemeSO.cs.meta
Normal file
11
Assets/_Game/Scripts/UI/Theme/UIThemeSO.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e1d97086a1337cc47a5d2ee9c85a944d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -62,17 +62,7 @@ namespace BaseGames.UI
|
||||
}
|
||||
|
||||
private IEnumerator FadeTo(float target)
|
||||
{
|
||||
float start = _cg.alpha;
|
||||
float elapsed = 0f;
|
||||
while (elapsed < _fadeDuration)
|
||||
{
|
||||
_cg.alpha = Mathf.Lerp(start, target, elapsed / _fadeDuration);
|
||||
elapsed += Time.unscaledDeltaTime;
|
||||
yield return null;
|
||||
}
|
||||
_cg.alpha = target;
|
||||
}
|
||||
=> UITween.FadeCanvasGroup(_cg, target, _fadeDuration);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.AddressableAssets;
|
||||
using UnityEngine.ResourceManagement.AsyncOperations;
|
||||
using BaseGames.Core;
|
||||
using BaseGames.Core.Events;
|
||||
|
||||
@@ -22,17 +24,23 @@ namespace BaseGames.UI
|
||||
[DefaultExecutionOrder(+50)]
|
||||
public class UIManager : MonoBehaviour, IUIManager
|
||||
{
|
||||
// ── 状态驱动根节点(不进入面板栈,仅根据 GameState 显示/隐藏)────────
|
||||
// ── 状态驱动根节点(不进入面板栈,仅根据 GameState 显示/隐藏)─────────
|
||||
[Header("状态驱动根节点(非面板栈)")]
|
||||
[SerializeField] private GameObject _hudRoot;
|
||||
[SerializeField] private GameObject _deathScreenRoot;
|
||||
|
||||
// ── 面板栈注册表 ──────────────────────────────────────────────────────
|
||||
[Header("面板栈注册表(Inspector 配置,可运行时扩展)")]
|
||||
[Tooltip("将 PanelId 与对应的根 GameObject 绑定。" +
|
||||
"新增面板只需在此添加一行,无需修改 UIManager 代码。")]
|
||||
[Tooltip("将 PanelId 与对应的根 GameObject 绑定。新增面板只需在此添加一行,无需修改 UIManager 代码。")]
|
||||
[SerializeField] private PanelRegistration[] _panels;
|
||||
|
||||
[Header("Addressable 面板(按需异步加载,首次打开时实例化)")]
|
||||
[Tooltip("配置 PanelId → AssetReferenceGameObject;同 _panels 重复定义时,本表优先生效。" +
|
||||
"未在此处也未在 _panels 中注册的 PanelId 调用 OpenPanel 将记录警告。")]
|
||||
[SerializeField] private AddressablePanelRegistration[] _addressablePanels;
|
||||
[Tooltip("Addressable 面板实例化后挂载的父节点;为空时挂在 UIManager.transform。")]
|
||||
[SerializeField] private Transform _addressablePanelParent;
|
||||
|
||||
[Header("Event Channels")]
|
||||
[SerializeField] private GameStateEventChannelSO _onGameStateChanged;
|
||||
[SerializeField] private VoidEventChannelSO _onPauseRequested;
|
||||
@@ -43,11 +51,11 @@ namespace BaseGames.UI
|
||||
[SerializeField] private VoidEventChannelSO _onSpellSelectOpen;
|
||||
|
||||
// ── 面板栈结构 ────────────────────────────────────────────────────────
|
||||
private readonly Stack<GameObject> _panelStack = new();
|
||||
private readonly Stack<GameObject> _panelStack = new();
|
||||
/// <summary>O(1) 成员判断,与 _panelStack 保持同步,替代 Stack.Contains O(n)。</summary>
|
||||
private readonly HashSet<GameObject> _openPanelSet = new();
|
||||
private readonly HashSet<GameObject> _openPanelSet = new();
|
||||
private readonly Dictionary<PanelId, GameObject> _panelRegistry = new();
|
||||
private readonly CompositeDisposable _subs = new();
|
||||
private readonly CompositeDisposable _subs = new();
|
||||
|
||||
// ── 序列化辅助结构 ────────────────────────────────────────────────────
|
||||
[System.Serializable]
|
||||
@@ -59,13 +67,42 @@ namespace BaseGames.UI
|
||||
public GameObject root;
|
||||
}
|
||||
|
||||
// ── 生命周期 ──────────────────────────────────────────────────────────
|
||||
[System.Serializable]
|
||||
private struct AddressablePanelRegistration
|
||||
{
|
||||
[Tooltip("面板标识符。")]
|
||||
public PanelId id;
|
||||
[Tooltip("Addressable 引用;首次 OpenPanel 时异步加载并实例化。")]
|
||||
public AssetReferenceGameObject reference;
|
||||
[Tooltip("关闭后保留实例(仅隐藏,不释放)。开启时常驻内存,避免反复加载抖动;适合频繁打开的面板。")]
|
||||
public bool keepLoadedAfterClose;
|
||||
}
|
||||
|
||||
/// <summary>记录 Addressable 加载状态,避免重复加载与正确释放。</summary>
|
||||
private class AddressablePanelHandle
|
||||
{
|
||||
public AsyncOperationHandle<GameObject> Handle;
|
||||
public GameObject Instance;
|
||||
public bool IsLoading;
|
||||
public bool KeepLoaded;
|
||||
}
|
||||
private readonly Dictionary<PanelId, AddressablePanelHandle> _addressableHandles = new();
|
||||
private readonly Dictionary<PanelId, AssetReferenceGameObject> _addressableRefs = new();
|
||||
private readonly Dictionary<PanelId, bool> _addressableKeep = new();
|
||||
|
||||
// ── 生命周期 ──────────────────────────────────────────────────────────
|
||||
private void Awake()
|
||||
{
|
||||
if (_panels != null)
|
||||
foreach (var p in _panels)
|
||||
if (p.root != null) _panelRegistry[p.id] = p.root;
|
||||
|
||||
if (_addressablePanels != null)
|
||||
foreach (var p in _addressablePanels)
|
||||
{
|
||||
if (p.reference != null) _addressableRefs[p.id] = p.reference;
|
||||
_addressableKeep[p.id] = p.keepLoadedAfterClose;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
@@ -84,21 +121,36 @@ namespace BaseGames.UI
|
||||
{
|
||||
ServiceLocator.Unregister<IUIManager>(this);
|
||||
_subs.Clear();
|
||||
ReleaseAllAddressablePanels();
|
||||
}
|
||||
|
||||
/// <summary>释放所有由 Addressables 加载的面板(防止场景切换泄漏)。</summary>
|
||||
private void ReleaseAllAddressablePanels()
|
||||
{
|
||||
foreach (var kv in _addressableHandles)
|
||||
{
|
||||
var h = kv.Value;
|
||||
if (h == null) continue;
|
||||
if (h.Instance != null && _addressableRefs.TryGetValue(kv.Key, out var aref))
|
||||
{
|
||||
aref.ReleaseInstance(h.Instance);
|
||||
}
|
||||
else if (h.Handle.IsValid())
|
||||
{
|
||||
Addressables.Release(h.Handle);
|
||||
}
|
||||
}
|
||||
_addressableHandles.Clear();
|
||||
}
|
||||
|
||||
// ── 面板注册(运行时动态扩展入口)────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 运行时注册或覆盖面板绑定(如场景加载后动态添加的面板)。
|
||||
/// Inspector 中已配置的面板无需调用此方法。
|
||||
/// </summary>
|
||||
/// <summary>运行时注册或覆盖面板绑定(如场景加载后动态添加的面板)。</summary>
|
||||
public void RegisterPanel(PanelId id, GameObject root)
|
||||
{
|
||||
if (root != null) _panelRegistry[id] = root;
|
||||
}
|
||||
|
||||
// ── 状态响应 ──────────────────────────────────────────────────────────
|
||||
|
||||
private void HandleGameStateChanged(GameStateId state)
|
||||
{
|
||||
bool showHud = state == GameStates.Gameplay || state == GameStates.BossFight;
|
||||
@@ -118,19 +170,72 @@ namespace BaseGames.UI
|
||||
}
|
||||
|
||||
// ── 面板栈 API ────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>通过 ID 打开已注册的面板。</summary>
|
||||
/// <summary>通过 ID 打开面板:优先同步注册表,其次 Addressable 异步加载。</summary>
|
||||
public void OpenPanel(PanelId id)
|
||||
{
|
||||
if (_panelRegistry.TryGetValue(id, out var panel))
|
||||
{
|
||||
OpenPanel(panel);
|
||||
return;
|
||||
}
|
||||
if (_addressableRefs.ContainsKey(id))
|
||||
{
|
||||
OpenPanelAsync(id);
|
||||
return;
|
||||
}
|
||||
Debug.LogWarning($"[UIManager] PanelId.{id} 未注册(Inspector 中既无 _panels 也无 _addressablePanels)。", this);
|
||||
}
|
||||
|
||||
/// <summary>异步加载并打开 Addressable 面板。首次触发 InstantiateAsync,后续复用。</summary>
|
||||
private void OpenPanelAsync(PanelId id)
|
||||
{
|
||||
if (!_addressableRefs.TryGetValue(id, out var aref) || aref == null)
|
||||
{
|
||||
Debug.LogWarning($"[UIManager] Addressable 面板 {id} 未配置 AssetReference。", this);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_addressableHandles.TryGetValue(id, out var entry))
|
||||
{
|
||||
_addressableKeep.TryGetValue(id, out var keep);
|
||||
entry = new AddressablePanelHandle { IsLoading = true, KeepLoaded = keep };
|
||||
_addressableHandles[id] = entry;
|
||||
}
|
||||
|
||||
// 已加载完成:直接复用实例。
|
||||
if (entry.Instance != null)
|
||||
{
|
||||
_panelRegistry[id] = entry.Instance;
|
||||
OpenPanel(entry.Instance);
|
||||
return;
|
||||
}
|
||||
|
||||
// 加载中:忽略重复请求。
|
||||
if (entry.IsLoading && entry.Handle.IsValid()) return;
|
||||
|
||||
entry.IsLoading = true;
|
||||
var parent = _addressablePanelParent != null ? _addressablePanelParent : transform;
|
||||
entry.Handle = aref.InstantiateAsync(parent, instantiateInWorldSpace: false);
|
||||
entry.Handle.Completed += op =>
|
||||
{
|
||||
entry.IsLoading = false;
|
||||
if (op.Status != AsyncOperationStatus.Succeeded || op.Result == null)
|
||||
{
|
||||
Debug.LogError($"[UIManager] Addressable 面板 {id} 加载失败: {op.OperationException}", this);
|
||||
return;
|
||||
}
|
||||
entry.Instance = op.Result;
|
||||
entry.Instance.SetActive(false); // 与 PanelRegistration 同语义:默认隐藏
|
||||
_panelRegistry[id] = entry.Instance;
|
||||
OpenPanel(entry.Instance);
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>打开指定 GameObject 面板并压栈;已在栈中则忽略(O(1) 判断)。</summary>
|
||||
public void OpenPanel(GameObject panel)
|
||||
{
|
||||
if (panel == null) return;
|
||||
if (!_openPanelSet.Add(panel)) return; // HashSet.Add 返回 false = 已存在
|
||||
if (!_openPanelSet.Add(panel)) return;
|
||||
if (_panelStack.Count > 0) _panelStack.Peek().SetActive(false);
|
||||
panel.SetActive(true);
|
||||
_panelStack.Push(panel);
|
||||
@@ -152,7 +257,6 @@ namespace BaseGames.UI
|
||||
}
|
||||
|
||||
// ── 快捷事件回调 ──────────────────────────────────────────────────────
|
||||
|
||||
private void TogglePause()
|
||||
{
|
||||
if (_panelRegistry.TryGetValue(PanelId.Pause, out var pausePanel)
|
||||
@@ -166,35 +270,53 @@ namespace BaseGames.UI
|
||||
private void OpenCharmPanel() => OpenPanel(PanelId.CharmPanel);
|
||||
private void OpenSpellSelect() => OpenPanel(PanelId.SpellSelect);
|
||||
|
||||
// ── 编辑器工具 (不入构建) ──────────────────────────────────────────────────
|
||||
|
||||
/// <summary>验证面板注册表是否完整、无重复、无空引用。</summary>
|
||||
// ── 编辑器工具 ────────────────────────────────────────────────────────
|
||||
[ContextMenu("验证面板注册表")]
|
||||
private void EditorValidateRegistry()
|
||||
{
|
||||
if (_panels == null || _panels.Length == 0)
|
||||
if ((_panels == null || _panels.Length == 0) && (_addressablePanels == null || _addressablePanels.Length == 0))
|
||||
{
|
||||
Debug.LogWarning("[UIManager] 面板注册表为空!", this);
|
||||
return;
|
||||
}
|
||||
|
||||
var seen = new System.Collections.Generic.HashSet<PanelId>();
|
||||
var seen = new HashSet<PanelId>();
|
||||
bool ok = true;
|
||||
foreach (var p in _panels)
|
||||
if (_panels != null)
|
||||
{
|
||||
if (p.root == null)
|
||||
foreach (var p in _panels)
|
||||
{
|
||||
Debug.LogWarning($"[UIManager] PanelId.{p.id} 的 GameObject 引用为 null!", this);
|
||||
ok = false;
|
||||
if (p.root == null)
|
||||
{
|
||||
Debug.LogWarning($"[UIManager] PanelId.{p.id} 的 GameObject 引用为 null!", this);
|
||||
ok = false;
|
||||
}
|
||||
if (!seen.Add(p.id))
|
||||
{
|
||||
Debug.LogError($"[UIManager] PanelId.{p.id} 在 _panels 中重复!", this);
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
if (!seen.Add(p.id))
|
||||
}
|
||||
if (_addressablePanels != null)
|
||||
{
|
||||
var seenAddr = new HashSet<PanelId>();
|
||||
foreach (var p in _addressablePanels)
|
||||
{
|
||||
Debug.LogError($"[UIManager] PanelId.{p.id} 重复注册!", this);
|
||||
ok = false;
|
||||
if (p.reference == null)
|
||||
{
|
||||
Debug.LogWarning($"[UIManager] Addressable PanelId.{p.id} 的 reference 为 null!", this);
|
||||
ok = false;
|
||||
}
|
||||
if (!seenAddr.Add(p.id))
|
||||
{
|
||||
Debug.LogError($"[UIManager] PanelId.{p.id} 在 _addressablePanels 中重复!", this);
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ok)
|
||||
Debug.Log($"[UIManager] 验证通过 ✔ 已注册 {_panels.Length} 个面板。", this);
|
||||
Debug.Log($"[UIManager] 验证通过 ✔ 同步 {_panels?.Length ?? 0} 个 / Addressable {_addressablePanels?.Length ?? 0} 个。", this);
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
@@ -203,15 +325,15 @@ namespace BaseGames.UI
|
||||
#endif
|
||||
|
||||
[ContextMenu("测试:打开 Pause 面板")]
|
||||
private void EditorOpenPause() => OpenPanel(PanelId.Pause);
|
||||
private void EditorOpenPause() => OpenPanel(PanelId.Pause);
|
||||
|
||||
[ContextMenu("测试:打开 Map 面板")]
|
||||
private void EditorOpenMap() => OpenPanel(PanelId.Map);
|
||||
private void EditorOpenMap() => OpenPanel(PanelId.Map);
|
||||
|
||||
[ContextMenu("测试:打开 Shop 面板")]
|
||||
private void EditorOpenShop() => OpenPanel(PanelId.Shop);
|
||||
private void EditorOpenShop() => OpenPanel(PanelId.Shop);
|
||||
|
||||
[ContextMenu("测试:关闭栈顶面板")]
|
||||
private void EditorCloseTop() => CloseTopPanel();
|
||||
private void EditorCloseTop() => CloseTopPanel();
|
||||
}
|
||||
}
|
||||
|
||||
8
Assets/_Game/Scripts/UI/Utility.meta
Normal file
8
Assets/_Game/Scripts/UI/Utility.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f68dca2fd99c2f24699b91734614eb2b
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
66
Assets/_Game/Scripts/UI/Utility/ColorblindApplier.cs
Normal file
66
Assets/_Game/Scripts/UI/Utility/ColorblindApplier.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/UI/Utility/ColorblindApplier.cs.meta
Normal file
11
Assets/_Game/Scripts/UI/Utility/ColorblindApplier.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7c8f1d9c11cc5cb43bdf3177b8028532
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
67
Assets/_Game/Scripts/UI/Utility/UIScaleApplier.cs
Normal file
67
Assets/_Game/Scripts/UI/Utility/UIScaleApplier.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/UI/Utility/UIScaleApplier.cs.meta
Normal file
11
Assets/_Game/Scripts/UI/Utility/UIScaleApplier.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9eae5e71f8a9b5b4582bfa1f0b41ac52
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
131
Assets/_Game/Scripts/UI/Utility/UITween.cs
Normal file
131
Assets/_Game/Scripts/UI/Utility/UITween.cs
Normal 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"/> <= 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>将 Graphic(Image / 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/UI/Utility/UITween.cs.meta
Normal file
11
Assets/_Game/Scripts/UI/Utility/UITween.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 96fd9d373d6d2784c8d04d8e56d0bb3e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -37,6 +37,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BaseGames.World", "BaseGame
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BaseGames.Platform", "BaseGames.Platform.csproj", "{AC970A4B-2B91-4466-A501-5CF17F4F4604}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BaseGames.Core", "BaseGames.Core.csproj", "{FE8FDA48-F779-850A-348D-48764F9384AF}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Assembly-CSharp-Editor", "Assembly-CSharp-Editor.csproj", "{278D6C47-C52B-D206-DB1C-429D79FFAD5A}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Assembly-CSharp", "Assembly-CSharp.csproj", "{BDE6E0A0-CE2D-39A5-53EB-DCA516DEF547}"
|
||||
@@ -61,8 +63,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BaseGames.Enemies.Navigatio
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Assembly-CSharp-firstpass", "Assembly-CSharp-firstpass.csproj", "{A4F2B84C-88C0-47A4-3127-6C338342D39C}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BaseGames.Core", "BaseGames.Core.csproj", "{FE8FDA48-F779-850A-348D-48764F9384AF}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BaseGames.Core.Save", "BaseGames.Core.Save.csproj", "{A5810B4A-14B6-7A34-A71A-B7E3515E986C}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PathBerserker2d.Editor", "PathBerserker2d.Editor.csproj", "{3CF69C34-D2B4-7E0A-F787-6B929B19762B}"
|
||||
@@ -83,6 +83,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Opsive.BehaviorDesigner.Edi
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BaseGames.Combat", "BaseGames.Combat.csproj", "{8BEFFA97-0E4E-2B59-7C2F-634A5BA5B0E5}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BaseGames.Tests.PlayMode", "BaseGames.Tests.PlayMode.csproj", "{E3F8AFC1-C10D-B715-91EF-4AD9F9B46B1F}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BaseGames.Progression", "BaseGames.Progression.csproj", "{E2BCDEF1-1468-78C0-F805-059C42BD31D8}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BaseGames.Input", "BaseGames.Input.csproj", "{535FDFCA-99B2-B315-0AA7-5D76D079BD0A}"
|
||||
@@ -199,6 +201,10 @@ Global
|
||||
{AC970A4B-2B91-4466-A501-5CF17F4F4604}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{AC970A4B-2B91-4466-A501-5CF17F4F4604}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{AC970A4B-2B91-4466-A501-5CF17F4F4604}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{FE8FDA48-F779-850A-348D-48764F9384AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{FE8FDA48-F779-850A-348D-48764F9384AF}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{FE8FDA48-F779-850A-348D-48764F9384AF}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{FE8FDA48-F779-850A-348D-48764F9384AF}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{278D6C47-C52B-D206-DB1C-429D79FFAD5A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{278D6C47-C52B-D206-DB1C-429D79FFAD5A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{278D6C47-C52B-D206-DB1C-429D79FFAD5A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
@@ -247,10 +253,6 @@ Global
|
||||
{A4F2B84C-88C0-47A4-3127-6C338342D39C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A4F2B84C-88C0-47A4-3127-6C338342D39C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A4F2B84C-88C0-47A4-3127-6C338342D39C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{FE8FDA48-F779-850A-348D-48764F9384AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{FE8FDA48-F779-850A-348D-48764F9384AF}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{FE8FDA48-F779-850A-348D-48764F9384AF}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{FE8FDA48-F779-850A-348D-48764F9384AF}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{A5810B4A-14B6-7A34-A71A-B7E3515E986C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A5810B4A-14B6-7A34-A71A-B7E3515E986C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A5810B4A-14B6-7A34-A71A-B7E3515E986C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
@@ -291,6 +293,10 @@ Global
|
||||
{8BEFFA97-0E4E-2B59-7C2F-634A5BA5B0E5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{8BEFFA97-0E4E-2B59-7C2F-634A5BA5B0E5}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{8BEFFA97-0E4E-2B59-7C2F-634A5BA5B0E5}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{E3F8AFC1-C10D-B715-91EF-4AD9F9B46B1F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E3F8AFC1-C10D-B715-91EF-4AD9F9B46B1F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E3F8AFC1-C10D-B715-91EF-4AD9F9B46B1F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{E3F8AFC1-C10D-B715-91EF-4AD9F9B46B1F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{E2BCDEF1-1468-78C0-F805-059C42BD31D8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E2BCDEF1-1468-78C0-F805-059C42BD31D8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E2BCDEF1-1468-78C0-F805-059C42BD31D8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
|
||||
Reference in New Issue
Block a user