(client) feat:添加游玩时UI相关贴图,添加3d模型场景按钮,添加多级调色进度条,添加SVG图片包,添加事件定义以及管理器,添加音频管理器,定义部分怪物,添加通信协议定义;fix:修复维度切换错误,修复LogUI显示不正确 (#55)

Co-authored-by: m0_75251201 <m0_75251201@noreply.gitcode.com>
Reviewed-on: Roguelite-Game-Developing-Team/Gen_Hack-and-Slash-Roguelite#55
This commit is contained in:
2025-09-03 19:59:22 +08:00
parent 450b15e4df
commit 78849e0cc5
208 changed files with 16296 additions and 2228 deletions

View File

@ -1,7 +1,7 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
using Utils;
using Utils; // 假设此命名空间包含MonoSingleton
namespace Base
{
@ -25,120 +25,239 @@ namespace Base
private bool _pause = false;
public bool Pause
{
get
{
return _pause;
}
get => _pause;
set
{
if (value)
if (value != _pause) // 逻辑修改说明避免重复设置Time.timeScale
{
Time.timeScale = 0;
Time.timeScale = value ? 0 : 1;
_pause = value;
}
else
{
Time.timeScale = 1;
}
_pause = value;
}
}
public List<ITickPhysics> tickPhysics = new();
public List<ITick> ticks = new();
public List<ITickUI> tickUIs = new();
// 修改点 1.1:主列表用于迭代
private readonly List<ITick> _ticks = new();
private readonly List<ITickPhysics> _tickPhysics = new();
private readonly List<ITickUI> _tickUIs = new();
// 修改点 1.1:缓冲列表用于添加
private readonly HashSet<ITick> _ticksToAdd = new(); // 逻辑修改说明使用HashSet避免重复添加提高查找效率
private readonly HashSet<ITickPhysics> _tickPhysicsToAdd = new();
private readonly HashSet<ITickUI> _tickUIsToAdd = new();
// 修改点 1.1:缓冲列表用于移除
private readonly HashSet<ITick> _ticksToRemove = new(); // 逻辑修改说明使用HashSet避免重复移除提高查找效率
private readonly HashSet<ITickPhysics> _tickPhysicsToRemove = new();
private readonly HashSet<ITickUI> _tickUIsToRemove = new();
private void Update()
{
// 逻辑修改说明UI部分的Tick不应受_pause影响
if (!_pause)
foreach (var tick in ticks)
{
// 逻辑修改说明:迭代时使用只读副本或确保不会被修改
// 这里是直接迭代_ticks确保_ticks在Update生命周期内不会被Add/Remove直接修改
// 添加和移除操作通过缓冲区在LateUpdate处理
foreach (var tick in _ticks)
{
tick.Tick();
foreach (var uiTick in tickUIs) uiTick.TickUI();
//if (timer > 1)
//{
// timer -= 1;
// Debug.Log("滴答");
//}
//timer += Time.deltaTime;
}
}
// UI更新通常不受游戏暂停影响例如菜单动画、UI计时器等除非UI本身也需要暂停逻辑。
// 根据需求决定是否受_pause影响。此处假设UI不暂停。
foreach (var uiTick in _tickUIs)
{
uiTick.TickUI();
}
}
private void FixedUpdate()
{
if (!_pause)
foreach (var physicsTick in tickPhysics)
{
foreach (var physicsTick in _tickPhysics)
{
physicsTick.TickPhysics();
}
}
}
// 修改点 1.4在LateUpdate应用缓冲区的更改
private void LateUpdate()
{
ApplyBufferedChanges();
}
// 修改点 3.1OnDestroy保持不变确保事件移除
private void OnDestroy()
{
// 移除事件监听
SceneManager.sceneLoaded -= OnSceneLoaded;
}
protected override void OnStart()
// 修改点 3.1OnStart保持不变负责事件注册和初始化
protected override void OnStart() // MonoSingleton的Awake后调用
{
// 注册场景加载事件
SceneManager.sceneLoaded += OnSceneLoaded;
// 初始化时调用一次
Init();
// 逻辑修改说明Initial清理所有列表而不是填充
Init();
}
private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
// 场景加载完成后调用 Init 方法
Init();
}
/// <summary>
/// 初始化方法
/// 初始化/重置方法。清空所有注册列表和缓冲列表。
/// </summary>
// 修改点 2.1Init() 方法不再负责FindObjectsByType
public void Init()
{
ticks.Clear();
tickPhysics.Clear();
tickUIs.Clear();
_ticks.Clear();
_tickPhysics.Clear();
_tickUIs.Clear();
_ticksToAdd.Clear();
_tickPhysicsToAdd.Clear();
_tickUIsToAdd.Clear();
_ticksToRemove.Clear();
_tickPhysicsToRemove.Clear();
_tickUIsToRemove.Clear();
// 逻辑修改说明:移除 FindObjectsByType 逻辑
// 对象应通过其各自的 OnEnable/OnDisable 生命周期来注册/反注册
foreach (var obj in FindObjectsByType<MonoBehaviour>(FindObjectsSortMode.None))
{
if (obj is ITick tickObj) ticks.Add(tickObj);
if (obj is ITickPhysics physicsObj) tickPhysics.Add(physicsObj);
if (obj is ITickUI uiObj) tickUIs.Add(uiObj);
if (obj is ITick tickObj) _ticks.Add(tickObj);
if (obj is ITickPhysics physicsObj) _tickPhysics.Add(physicsObj);
if (obj is ITickUI uiObj) _tickUIs.Add(uiObj);
}
}
// 修改点 1.2 & 1.3将更改放入缓冲并在LateUpdate统一处理
public static void AddTick(ITick tick)
{
if (Instance != null && !Instance.ticks.Contains(tick))
Instance.ticks.Add(tick);
if (Instance != null && tick != null) // 逻辑修改说明添加null检查
{
Instance._ticksToAdd.Add(tick);
Instance._ticksToRemove.Remove(tick); // 逻辑修改说明:如果在待移除列表,则先移除
}
}
public static void RemoveTick(ITick tick)
{
if (Instance != null)
Instance.ticks.Remove(tick);
if (Instance != null && tick != null) // 逻辑修改说明添加null检查
{
Instance._ticksToRemove.Add(tick);
Instance._ticksToAdd.Remove(tick); // 逻辑修改说明:如果在待添加列表,则先移除
}
}
public static void AddTickPhysics(ITickPhysics physics)
{
if (Instance != null && !Instance.tickPhysics.Contains(physics))
Instance.tickPhysics.Add(physics);
if (Instance != null && physics != null)
{
Instance._tickPhysicsToAdd.Add(physics);
Instance._tickPhysicsToRemove.Remove(physics);
}
}
public static void RemoveTickPhysics(ITickPhysics physics)
{
if (Instance != null)
Instance.tickPhysics.Remove(physics);
if (Instance != null && physics != null)
{
Instance._tickPhysicsToRemove.Add(physics);
Instance._tickPhysicsToAdd.Remove(physics);
}
}
public static void AddTickUI(ITickUI ui)
{
if (Instance != null && !Instance.tickUIs.Contains(ui))
Instance.tickUIs.Add(ui);
if (Instance != null && ui != null)
{
Instance._tickUIsToAdd.Add(ui);
Instance._tickUIsToRemove.Remove(ui);
}
}
public static void RemoveTickUI(ITickUI ui)
{
if (Instance != null)
Instance.tickUIs.Remove(ui);
if (Instance != null && ui != null)
{
Instance._tickUIsToRemove.Add(ui);
Instance._tickUIsToAdd.Remove(ui);
}
}
// 修改点 1.3:应用缓冲区的更改
private void ApplyBufferedChanges()
{
// 先处理移除
if (_ticksToRemove.Count > 0)
{
foreach (var tick in _ticksToRemove)
{
_ticks.Remove(tick);
}
_ticksToRemove.Clear();
}
if (_tickPhysicsToRemove.Count > 0)
{
foreach (var physicsTick in _tickPhysicsToRemove)
{
_tickPhysics.Remove(physicsTick);
}
_tickPhysicsToRemove.Clear();
}
if (_tickUIsToRemove.Count > 0)
{
foreach (var uiTick in _tickUIsToRemove)
{
_tickUIs.Remove(uiTick);
}
_tickUIsToRemove.Clear();
}
// 后处理添加
if (_ticksToAdd.Count > 0)
{
foreach (var tick in _ticksToAdd)
{
if (!_ticks.Contains(tick)) // 逻辑修改说明:避免重复添加到主列表
{
_ticks.Add(tick);
}
}
_ticksToAdd.Clear();
}
if (_tickPhysicsToAdd.Count > 0)
{
foreach (var physicsTick in _tickPhysicsToAdd)
{
if (!_tickPhysics.Contains(physicsTick))
{
_tickPhysics.Add(physicsTick);
}
}
_tickPhysicsToAdd.Clear();
}
if (_tickUIsToAdd.Count > 0)
{
foreach (var uiTick in _tickUIsToAdd)
{
if (!_tickUIs.Contains(uiTick))
{
_tickUIs.Add(uiTick);
}
}
_tickUIsToAdd.Clear();
}
}
}
}
}

View File

@ -1,42 +1,70 @@
// C#
using Managers; // 确保日志命名空间正确
using System.Collections;
using System.Collections.Generic; // 新增用于List
using TMPro;
using UnityEngine;
using UnityEngine.SceneManagement;
using System.Collections.Generic; // 提供List等通用集合类型
using Logging; // 提供日志记录功能
using Managers; // 提供管理器接口和实现
using TMPro; // TextMeshPro文本组件命名空间
using UnityEngine; // Unity引擎核心命名空间
using UnityEngine.SceneManagement; // 场景管理命名空间
namespace Base
{
// 请确保你在Unity Editor中将 'projectile' 的Inspector引用从 Gradient 更改为 CustomProgressBar
// 或者替换为你实际的进度条组件类型
// 如果你没有 CustomProgressBar而想用 Image 或者别的,这些 Progress/Opacity 属性需要大幅修改。
/// <summary>
/// <c>Launcher</c> 类负责管理游戏的初始化加载流程,包括管理器加载、进度条显示和场景切换
/// </summary>
/// <remarks>
/// <para>请注意:虽然此处的 <c>progressBar</c> 变量类型被声明为 <c>Gradient</c>
/// 但其在代码中的使用方式(如访问 <c>color1</c>, <c>color2</c> 属性和调用 <c>Refresh()</c> 方法)
/// 强烈暗示它应引用一个具有类似 API 的自定义进度条组件,例如 <c>CustomProgressBar</c>。</para>
/// <para>如果您使用的是标准 <c>UnityEngine.UI.Image</c> 或其他组件来显示进度,
/// 则 <c>Progress</c> 和 <c>Opacity</c> 属性的实现逻辑需要根据实际组件的 API 进行大幅修改。</para>
/// </remarks>
public class Launcher : MonoBehaviour
{
/// <summary>
/// 加载界面UI的根游戏对象。
/// </summary>
public GameObject loadingUI;
// 假设 CustomProgressBar 是一个自定义组件,其 API 与原代码使用方式一致
// 如果这里是 UnityEngine.UI.Image需要完全改变 Progress 和 Opacity 的实现
public Gradient progressBar; // 自定义进度条组件
public TMP_Text describeText; // 描述文本
public float duration = 0.5f; // 每个步骤的过渡时间
public float fadeDuration = 2f; // 不透明度渐隐的时间
/// <summary>
/// 用于显示加载进度的自定义进度条组件。
/// <para>请参阅类注释以了解其声明类型与预期API的差异。</para>
/// </summary>
public Gradient progressBar;
private float _currentProgressValue = 0f; // 实际的当前进度值0-1
private Color _initialTextColor; // 原始文本颜色,用于渐隐
private Color _initialProgressBarColor1; // 原始进度条颜色1
private Color _initialProgressBarColor2; // 原始进度条颜色2
/// <summary>
/// 用于显示当前加载步骤描述的文本组件。
/// </summary>
public TMP_Text describeText;
// 存储所有需要加载和清理的管理器
/// <summary>
/// 进度条每个加载步骤的平滑过渡时间(秒)。
/// </summary>
public float duration = 0.5f;
/// <summary>
/// 加载完成后UI元素渐隐的时间
/// </summary>
public float fadeDuration = 2f;
private float _currentProgressValue = 0f; // 实际的当前进度值在0到1之间
private Color _initialTextColor; // 描述文本的原始颜色,用于渐隐效果
private Color _initialProgressBarColor1; // 进度条颜色1的原始值用于渐变
private Color _initialProgressBarColor2; // 进度条颜色2的原始值用于渐变
/// <summary>
/// 存储所有需要在启动时加载和在重载时清理的管理器实例列表。
/// </summary>
private List<ILaunchManager> _managersToLoad;
// Progress 属性,更新进度条和颜色
/// <summary>
/// 获取或设置加载进度值范围为0到1。
/// 设置此属性将更新进度条的视觉显示和颜色。
/// </summary>
public float Progress
{
set
{
// 确保进度值在0到1之间提高健壮性
// 确保进度值在0到1之间提高系统健壮性
_currentProgressValue = Mathf.Clamp01(value);
if (progressBar)
@ -49,17 +77,23 @@ namespace Base
}
else
{
// 后半段color2 保持白色color1 从初始色渐变到白色
progressBar.color2 = Color.white;
// 后半段color2 保持白色color1 从初始色渐变到白色
progressBar.color1 = Color.Lerp(_initialProgressBarColor1, Color.white, (_currentProgressValue - 0.5f) * 2);
}
progressBar.Refresh(); // 通知自定义进度条组件更新显示
// 通知自定义进度条组件更新显示
// 注意UnityEngine.Gradient 作为数据结构没有 Refresh() 方法,
// 此调用表明 progressBar 实际上预期是一个具有此方法的自定义组件。
progressBar.Refresh();
}
}
get => _currentProgressValue;
}
// Opacity 属性,更新 UI 元素的透明度
/// <summary>
/// 获取或设置UI元素的整体不透明度范围0完全透明到1完全不透明
/// 设置此属性将更新进度条和描述文本的透明度。
/// </summary>
public float Opacity
{
set
@ -77,38 +111,44 @@ namespace Base
c2.a = alpha;
progressBar.color2 = c2;
// 通知自定义进度条组件更新显示。
// 注意UnityEngine.Gradient 作为数据结构没有 Refresh() 方法,
// 此调用表明 progressBar 实际上预期是一个具有此方法的自定义组件。
progressBar.Refresh();
}
if (describeText)
{
// 更新描述文本颜色的透明度
// 渐隐时,描述文本的透明度在 Opacity > 0.5f 时才开始渐变可见
// 渐隐时,描述文本的透明度在 Opacity > 0.5f 时才开始从0渐变到其原始透明度
var textAlpha = alpha > 0.5f ? Mathf.Lerp(0f, _initialTextColor.a, (alpha - 0.5f) * 2) : 0f;
describeText.color = new Color(_initialTextColor.r, _initialTextColor.g, _initialTextColor.b, textAlpha);
}
}
}
/// <summary>
/// 当脚本实例被启用时调用一次。
/// 用于初始化管理器列表并缓存UI元素的初始颜色。
/// </summary>
private void Awake()
{
// 在 Awake 中初始化管理器列表并注册,确保在 Start 之前完成
// 在 Awake 中初始化管理器列表,确保在 Start 之前完成所有管理器实例的注册
_managersToLoad = new List<ILaunchManager>
{
Logging.UnityLogger.Instance, // UnityLogger的包装器
Managers.DefineManager.Instance,
Managers.PackagesImageManager.Instance,
Managers.TileManager.Instance,
Managers.AffiliationManager.Instance,
Managers.ItemResourceManager.Instance
// 当未来有新的管理器时,只需在这里添加 Managers.NewManager.Instance
new LoggerManagerWrapper(), // Unity日志的包装器
DefineManager.Instance,
PackagesImageManager.Instance,
TileManager.Instance,
AffiliationManager.Instance,
ItemResourceManager.Instance,
EventManager.Instance,
AudioManager.Instance,
};
// 缓存UI的初始颜色以便后续操作或重置
if (describeText != null)
{
_initialTextColor = describeText.color;
}
// 缓存UI的初始颜色以便后续操作(如渐隐)或重置
_initialTextColor = describeText != null ? describeText.color : Color.white; // 如果为空,则使用默认值
if (progressBar != null)
{
_initialProgressBarColor1 = progressBar.color1;
@ -116,61 +156,66 @@ namespace Base
}
else
{
Debug.LogWarning("CustomProgressBar component not assigned to Launcher. Progress bar display may not work.");
// 警告日志:自定义进度条组件未赋值给启动器。
Debug.LogWarning("自定义进度条组件未赋值给启动器,进度条可能无法正常显示。");
}
}
/// <summary>
/// 在Awake方法之后、首次帧更新之前调用。
/// 用于判断是否需要执行完整的加载流程并根据需要启动加载或隐藏加载UI。
/// </summary>
private void Start()
{
// 如果 Program.Instance.needLoad 为 true表示需要完整的重新加载流程
// 否则如果不需要加载直接禁用UI并返回
// 如果 Program.Instance.needLoad 为 false表示游戏已加载或不需要重新加载直接隐藏加载UI
if (!Program.Instance.needLoad)
{
loadingUI.SetActive(false);
return;
}
// 根据 Program.Instance.needLoad 的含义(需要重载),
// 在开始新的加载流程之前,先清理所有可能存在的旧管理器状态。
// 这确保了每次需要加载时,系统都处于一个干净的初始状态。
ClearAllManagers(); // <--- 关键修改在Load()之前调用ClearAllManagers()
// 如果需要加载,则先清理所有管理器(用于重载或确保干净启动),再启动加载流程
ClearAllManagers();
// 初始化游戏设置
Setting.Instance.Init();
// 初始化设置
Base.Setting.Instance.Init();
#if !DEBUG
// 在非DEBUG模式下从设置中获取过渡时间
duration = Base.Setting.Instance.CurrentSettings.progressStepDuration;
fadeDuration = Base.Setting.Instance.CurrentSettings.exitAnimationDuration;
// 在非DEBUG模式下游戏设置中获取加载和渐隐的持续时间
duration = Base.Setting.Instance.CurrentSettings.progressStepDuration;
fadeDuration = Base.Setting.Instance.CurrentSettings.exitAnimationDuration;
#endif
Load(); // 启动加载流程内部会调用LoadAllManagers
Program.Instance.needLoad = false; // 加载完成后重置标志
Program.Instance.needLoad = false; // 加载完成后重置加载标志
}
/// <summary>
/// 启动游戏的加载流程。
/// 该方法会激活加载UI重置进度与透明度并启动所有管理器的异步加载协程。
/// </summary>
public void Load()
{
loadingUI.SetActive(true); // 激活加载UI
Progress = 0f; // 重置进度
Opacity = 1f; // 重置透明度为完全不透明
Progress = 0f; // 将进度重置为0
Opacity = 1f; // 将UI透明度重置为完全不透明
StartCoroutine(LoadAllManagers());
}
/// <summary>
/// 清理所有管理器,然后重新加载。用于游戏重载或场景切换后需要重新初始化所有数据的情况
/// 清理所有管理器,然后重新加载游戏数据
/// 此方法适用于游戏重载、场景切换后需要重新初始化所有数据的情况。
/// </summary>
public void Reload()
{
Debug.Log("<color=yellow>Performing a full reload of all managers...</color>");
// 日志:正在执行所有管理器的完整重新加载...
Debug.Log("<color=yellow>正在执行所有管理器的完整重新加载...</color>");
StopAllCoroutines(); // 停止当前所有正在运行的加载协程,避免冲突
ClearAllManagers(); // 清理所有管理器
ClearAllManagers(); // 清理所有管理器数据
Load(); // 重新加载游戏数据
}
/// <summary>
/// 遍历所有已注册的管理器调用它们的 Clear 方法。
/// 遍历所有已注册的管理器,并安全地调用它们的 Clear 方法以释放资源或重置状态
/// </summary>
private void ClearAllManagers()
{
@ -182,37 +227,39 @@ namespace Base
}
catch (System.Exception ex)
{
Debug.LogError($"<color=red>Error clearing manager {manager.StepDescription}:</color> {ex.Message}\n{ex.StackTrace}");
// 错误日志:清理管理器时发生异常。
Debug.LogError($"<color=red>清理管理器 '{manager.StepDescription}' 时出错:</color> {ex.Message}\n{ex.StackTrace}");
}
}
}
/// <summary>
/// 协程:按顺序加载所有管理器。
/// 协程:按顺序加载所有注册的管理器。
/// 该方法会在加载过程中更新描述文本、平滑过渡进度条,并安全初始化每个管理器。
/// </summary>
/// <returns>一个 <see cref="IEnumerator"/>,用于协程。</returns>
private IEnumerator LoadAllManagers()
{
for (var i = 0; i < _managersToLoad.Count; i++)
{
var manager = _managersToLoad[i];
// 更新描述文本,从管理器实例中获取
// 更新描述文本,显示当前正在加载的管理器步骤描述
if (describeText != null)
{
describeText.text = manager.StepDescription;
}
// 计算当前阶段的目标进度
// 计算当前阶段的目标进度
var targetProgress = (float)(i + 1) / _managersToLoad.Count;
// 平滑过渡进度条到下一个目标进度
yield return SmoothTransitionTo(targetProgress);
// 初始化对应的管理器,并进行异常处理
// 初始化对应的管理器,并处理可能发生的异常
yield return InitializeManagerSafely(manager);
// 模拟耗时如果管理器Init方法本身很快可以 uncomment
// yield return new WaitForSeconds(0.1f);
// yield return new WaitForSeconds(0.1f); // 此行代码可用于模拟每个管理器加载的耗时,默认不启用。
}
// 所有管理器加载完成后的处理
@ -220,19 +267,22 @@ namespace Base
{
describeText.text = "加载完成!";
}
Progress = 1f; // 确保进度条最终达到100%
// 确保进度条最终达到100%
Progress = 1f;
// 开始渐隐效果
// 启动加载UI的渐隐效果
yield return FadeOutProgressBar();
// 所有加载和动画结束后可以考虑卸载加载UI或跳转到主场景
// ToScene("MainGameScene"); // 如果有需要,在这里跳转到主游戏场景
// ToScene("MainGameScene"); // 如果有需要,可以在此处调用静态方法跳转到主游戏场景
}
/// <summary>
/// 尝试安全地初始化单个管理器,并捕获任何异常。
/// 尝试安全地初始化单个管理器实例,并捕获任何可能发生的异常。
/// 如果初始化失败将记录错误日志并更新UI提示。
/// </summary>
/// <param name="manager">要初始化的管理器实例。</param>
/// <returns>一个 <see cref="IEnumerator"/>,用于协程。</returns>
private IEnumerator InitializeManagerSafely(ILaunchManager manager)
{
var initSuccess = false;
@ -240,36 +290,38 @@ namespace Base
try
{
manager.Init(); // 调用管理器的 Init 方法
manager.Init(); // 调用管理器的 Init 方法进行初始化
initSuccess = true;
}
catch (System.Exception ex)
{
initException = ex; // 捕获异常
initException = ex; // 捕获初始化过程中抛出的异常
}
if (!initSuccess && initException != null)
{
// 记录错误日志
// 记录错误日志:初始化管理器时出现错误。
Debug.LogError($"<color=red>初始化管理器 '{manager.StepDescription}' 时出错:</color> {initException.Message}\n{initException.StackTrace}");
if (describeText != null)
{
describeText.text = $"{manager.StepDescription} (初始化失败)";
describeText.text = $"{manager.StepDescription} (初始化失败)"; // 更新UI显示失败信息
}
// 这里可以添加更复杂的错误处理,例如显示错误弹窗,或者重试逻辑
// yield break; // 如果错误严重,可以选择停止后续加载
// 这里可以添加更复杂的错误处理逻辑,例如显示错误弹窗、记录到特定日志文件或允许用户选择重试。
// yield break; // 如果错误严重到无法继续,可以选择停止后续加载流程。
}
yield return null; // 确保协程继续执行,即使没有做任何事
yield return null; // 确保协程继续执行,无论初始化成功与否
}
/// <summary>
/// 协程:平滑过渡进度条到指定的目标进度。
/// 协程:平滑过渡进度条的当前值到指定的目标进度。
/// 使用 <c>Mathf.SmoothStep</c> 实现自然加速和减速的过渡效果。
/// </summary>
/// <param name="targetProgress">目标进度值 (0-1)。</param>
/// <param name="targetProgress">目标进度值 (0到1之间)。</param>
/// <returns>一个 <see cref="IEnumerator"/>,用于协程。</returns>
private IEnumerator SmoothTransitionTo(float targetProgress)
{
var startProgress = _currentProgressValue; // 获取当前进度作为起始点
var startProgress = _currentProgressValue; // 获取当前进度作为过渡的起始点
var elapsedTime = 0f;
while (elapsedTime < duration)
@ -277,7 +329,7 @@ namespace Base
elapsedTime += Time.deltaTime;
// 使用 SmoothStep 实现更自然的加速和减速过渡效果
var t = Mathf.SmoothStep(0f, 1f, elapsedTime / duration);
Progress = Mathf.Lerp(startProgress, targetProgress, t);
Progress = Mathf.Lerp(startProgress, targetProgress, t); // 在起始和目标进度之间插值
yield return null;
}
@ -285,8 +337,9 @@ namespace Base
}
/// <summary>
/// 协程渐隐加载UI的透明
/// 协程:平滑渐隐加载UI的所有元素,使其透明度从完全不透明过渡到完全透明。
/// </summary>
/// <returns>一个 <see cref="IEnumerator"/>,用于协程。</returns>
private IEnumerator FadeOutProgressBar()
{
var elapsedTime = 0f;
@ -305,7 +358,7 @@ namespace Base
}
/// <summary>
/// 静态方法:加载指定名称的场景。
/// 静态方法:加载指定名称的Unity场景。
/// </summary>
/// <param name="scene">要加载的场景名称。</param>
public static void ToScene(string scene)
@ -313,7 +366,4 @@ namespace Base
SceneManager.LoadScene(scene);
}
}
}

View File

@ -19,11 +19,13 @@ namespace Base
public float globalVolume = 1.0f;
public WindowMode currentWindowMode = WindowMode.Fullscreen;
public Vector2Int windowResolution = new(1920, 1080);
public string[] loadOrder;
}
// 当前游戏设置
public GameSettings CurrentSettings = new();
// 窗口模式枚举
public enum WindowMode { Fullscreen, Windowed, Borderless }

View File

@ -1,3 +1,4 @@
using System; // Added for Action
using System.Collections.Generic;
using System.Linq;
using UI;
@ -7,7 +8,7 @@ using UnityEngine.SceneManagement;
namespace Base
{
/// <summary>
/// UI窗口输入控制和管理类
/// UI窗口输入控制和管理类
/// 负责根据输入显示/隐藏UI并根据UI状态管理游戏暂停。
/// </summary>
public class UIInputControl : Utils.MonoSingleton<UIInputControl>, ITickUI
@ -18,8 +19,59 @@ namespace Base
private readonly List<UIBase> _visibleWindows = new List<UIBase>();
private bool needUpdate = false;
/// <summary>
/// 查找并注册场景中所有的UI窗口包括非激活状态的
/// 当UI窗口的可见性状态发生改变时触发的事件。
/// 参数1: 发生改变的UIBase实例。
/// 参数2: 窗口的新可见状态 (true为显示false为隐藏)。
/// </summary>
public event Action<UIBase, bool> OnWindowVisibilityChanged; // <--- 新增
/// <summary>
/// 获取所有已注册的UI窗口的总数量。
/// </summary>
public int AllWindowCount => _allWindows.Count;
/// <summary>
/// 获取当前可见的UI窗口的数量。
/// </summary>
public int VisibleWindowCount => _visibleWindows.Count;
/// <summary>
/// 查询指定名称的UI窗口是否当前可见。
/// </summary>
/// <param name="uiName">UI窗口的名称。</param>
/// <returns>如果窗口可见则返回true否则返回false。</returns>
public bool IsWindowVisible(string uiName)
{
// 使用 Any() 方法检查 _visibleWindows 列表中是否存在名称匹配且可见的窗口
return _visibleWindows.Any(window => window != null && window.name == uiName && window.IsVisible);
}
/// <summary>
/// 查询指定名称的UI窗口是否当前处于可见状态且占用了输入。
/// </summary>
/// <param name="uiName">UI窗口的名称。</param>
/// <returns>如果窗口可见且占用了输入则返回true否则返回false。</returns>
public bool IsWindowInputOccupied(string uiName)
{
// 检查 _visibleWindows 列表中是否存在名称匹配且同时占用输入的窗口
return _visibleWindows.Any(window => window != null && window.name == uiName && window.IsVisible && window.isInputOccupied);
}
/// <summary>
/// 根据名称获取已注册的UI窗口实例。
/// </summary>
/// <param name="uiName">UI窗口的名称。</param>
/// <returns>匹配的UIBase实例如果未找到则返回null。</returns>
public UIBase GetWindow(string uiName)
{
// 从 _allWindows 列表中查找第一个名称匹配的窗口
return _allWindows.FirstOrDefault(window => window != null && window.name == uiName);
}
/// <summary>
/// 查找并注册场景中所有的UI窗口包括非激活状态的。
/// </summary>
private void RegisterAllWindows()
{
@ -29,7 +81,7 @@ namespace Base
var activeScene = UnityEngine.SceneManagement.SceneManager.GetActiveScene();
if (!activeScene.isLoaded)
{
Debug.LogWarning("当前场景未加载,无法注册窗口!");
Debug.LogWarning("[UIInputControl] 当前场景未加载,无法注册窗口!");
return;
}
@ -46,22 +98,22 @@ namespace Base
// 初始化所有窗口为隐藏状态
foreach (var window in _allWindows)
{
// 确保窗口不为空且其GameObject未被销毁
if (window != null && window.gameObject != null)
{
window.Hide();
window.Hide(); // 隐藏操作会触发 OnWindowVisibilityChanged 事件
}
}
needUpdate = true;
Debug.Log($"窗口数量{_allWindows.Count}");
}
/// <summary>
/// UI逻辑更新循环需要被外部的某个管理器在Update中调用
/// UI逻辑更新循环需要被外部的某个管理器在Update中调用
/// </summary>
public void TickUI()
{
//使用这个是为了让输入独占窗口关闭自己后不会立即激活其他窗口的按键,延迟一帧
// 使用这个是为了让输入独占窗口关闭自己后不会立即激活其他窗口的按键,延迟一帧
if (needUpdate)
{
// 更新可见窗口缓存和暂停状态
@ -70,12 +122,19 @@ namespace Base
needUpdate = false;
return;
}
if (_visibleWindows.Any(window => window.isInputOccupied))
// 如果有任何可见窗口占用了输入,则阻止其他窗口通过按键进行操作
if(_visibleWindows.Any(window => window && window.isInputOccupied))
return;
foreach (var window in _allWindows)
{
// 确保窗口不为空且其GameObject未被销毁
if (!window || !window.gameObject) continue;
// 检查窗口是否设置了有效的激活按键,并且该按键在本帧被按下
if (window.actionButton == KeyCode.None || !Input.GetKeyDown(window.actionButton)) continue;
if (window.IsVisible)
{
// 如果窗口当前是可见的,且未占用输入,则通过按键隐藏它
@ -93,20 +152,26 @@ namespace Base
}
/// <summary>
/// 公开的显示窗口方法
/// 公开的显示窗口方法
/// </summary>
/// <param name="windowToShow">要显示的窗口</param>
/// <param name="windowToShow">要显示的窗口</param>
public void Show(UIBase windowToShow)
{
if (!windowToShow || windowToShow.IsVisible) return;
// 确保窗口不为空且未被销毁,并且当前不可见
if (!windowToShow || !windowToShow.gameObject || windowToShow.IsVisible) return;
// 如果窗口是独占的,隐藏所有其他窗口
if (windowToShow.exclusive)
{
var windowsToHide = new List<UIBase>(_visibleWindows);
// 创建一个副本进行迭代,防止在 Hide() 调用中修改 _visibleWindows 导致迭代器失效
var windowsToHide = _visibleWindows.ToList();
foreach (var visibleWindow in windowsToHide)
{
Hide(visibleWindow);
// 确保窗口不是要显示的窗口本身,且仍然可见
if (visibleWindow && visibleWindow != windowToShow && visibleWindow.IsVisible)
{
Hide(visibleWindow);
}
}
}
@ -115,62 +180,93 @@ namespace Base
var itick = windowToShow as ITickUI;
if (itick != null)
Base.Clock.AddTickUI(itick);
// 触发事件通知窗口可见性已改变
OnWindowVisibilityChanged?.Invoke(windowToShow, true); // <--- 修改点 2
needUpdate = true;
}
/// <summary>
/// 根据名称显示UI窗口。
/// </summary>
/// <param name="uiName">要显示的UI窗口名称。</param>
public void Show(string uiName)
{
foreach (var window in _allWindows)
// 使用 GetWindow 方法获取窗口实例
var window = GetWindow(uiName);
if (window != null)
{
if (window.name == uiName)
{
Show(window);
return;
}
Show(window);
return;
}
Debug.LogWarning($"未找到窗口{uiName}");
Debug.LogWarning($"[UIInputControl] 未找到名称为 '{uiName}' 的窗口来显示。");
}
/// <summary>
/// 公开的隐藏窗口方法
/// 公开的隐藏窗口方法
/// </summary>
/// <param name="windowToHide">要隐藏的窗口</param>
/// <param name="windowToHide">要隐藏的窗口</param>
public void Hide(UIBase windowToHide)
{
if (!windowToHide || !windowToHide.IsVisible) return;
// 确保窗口不为空且未被销毁,并且当前可见
if (!windowToHide || !windowToHide.gameObject || !windowToHide.IsVisible) return;
// 隐藏目标窗口并更新缓存与暂停状态
// 隐藏目标窗口
windowToHide.Hide();
// 当UI窗口被隐藏时如果它实现了ITickUI接口则必须将其从Clock中移除。
// 这防止了隐藏窗口继续被Tick避免性能开销和潜在的NullReferenceException。
if (windowToHide is ITickUI iTick)
Base.Clock.RemoveTickUI(iTick);
// 触发事件通知窗口可见性已改变
OnWindowVisibilityChanged?.Invoke(windowToHide, false); // <--- 修改点 3
needUpdate = true;
}
/// <summary>
/// 根据名称隐藏UI窗口。
/// </summary>
/// <param name="uiName">要隐藏的UI窗口名称。</param>
public void Hide(string uiName)
{
foreach (var visibleWindow in _visibleWindows)
// 仅在可见窗口中查找并隐藏更符合HideByName的即时操作期望
// 如果需要隐藏所有名称匹配的窗口(包括隐藏的),则需要遍历 _allWindows
var visibleWindowToHide = _visibleWindows.FirstOrDefault(window => window != null && window.name == uiName && window.IsVisible);
if (visibleWindowToHide != null)
{
if (visibleWindow.name == uiName)
Hide(visibleWindowToHide);
return;
}
Debug.LogWarning($"[UIInputControl] 未找到名称为 '{uiName}' 且当前可见的窗口来隐藏。");
}
/// <summary>
/// 隐藏所有当前可见的UI窗口。
/// </summary>
public void HideAll()
{
// 创建 _visibleWindows 的一个副本进行迭代,以避免在循环中修改原列表导致迭代器失效
var windowsToHide = _visibleWindows.ToList();
foreach (var visibleWindow in windowsToHide)
{
// 再次检查窗口是否仍然可见,因为其他操作可能已经隐藏了它
if (visibleWindow != null && visibleWindow.IsVisible)
{
Hide(visibleWindow);
break;
Hide(visibleWindow); // Hide() 方法会触发 OnWindowVisibilityChanged 事件
}
}
}
public void HideAll()
{
foreach (var visibleWindow in _visibleWindows)
{
Hide(visibleWindow);
}
}
/// <summary>
/// 根据当前所有可见窗口的 needPause 属性来更新游戏时钟的暂停状态
/// 根据当前所有可见窗口的 needPause 属性来更新游戏时钟的暂停状态
/// </summary>
private void UpdatePauseState()
{
var shouldPause = _visibleWindows.Any(w => w.needPause);
// 确保 _visibleWindows 中的窗口都有效
var shouldPause = _visibleWindows.Any(w => w && w.needPause);
if (Base.Clock.Instance.Pause != shouldPause)
{
Base.Clock.Instance.Pause = shouldPause;
@ -178,35 +274,55 @@ namespace Base
}
/// <summary>
/// 更新当前可见窗口的缓存列表
/// 更新当前可见窗口的缓存列表
/// </summary>
private void UpdateVisibleWindowsCache()
{
_visibleWindows.Clear();
foreach (var window in _allWindows)
{
if (window.IsVisible)
if (window && window.IsVisible) // 确保窗口有效且可见
{
_visibleWindows.Add(window);
}
}
}
/// <summary>
/// 当脚本实例被销毁时调用。
/// 用于在销毁时取消订阅场景加载事件,防止内存泄漏。
/// </summary>
private void OnDestroy()
{
SceneManager.sceneLoaded -= OnSceneLoaded;
// 在销毁时清空所有订阅者防止因MonoSingleton持久化导致下一场景重新加载时出现旧的订阅者如果单例不销毁事件本身也不会自动清空订阅
// 如果 UIInputControl 是一个会随场景销毁的普通 MonoBehaviour 而不是持久化的 MonoSingleton, 某些情况下清除订阅者可以防止跨场景的引用问题。
// 但对于 MonoSingleton它通常是持久化的所以事件在重新加载场景后仍然保留。明确清空可以避免不必要的资源占用虽然在应用程序关闭时会自动释放。
// OnWindowVisibilityChanged = null; // 谨慎使用如果外部有长期订阅的需求清空可能导致问题。通常更推荐外部在OnDestroy中取消订阅。
}
/// <summary>
/// MonoSingleton 的 OnStart 方法,在单例首次创建并激活时调用,早于普通的 Start 方法。
/// 用于订阅场景加载事件并在首次启动时注册UI窗口。
/// </summary>
protected override void OnStart()
{
// 订阅场景加载事件以便在新场景加载后重新注册UI窗口
SceneManager.sceneLoaded += OnSceneLoaded;
// 首次启动时也注册一次窗口(例如在进入第一个场景时)。
RegisterAllWindows();
}
/// <summary>
/// 当场景加载完成时调用。
/// 用于在场景加载后重新查找并注册所有UI窗口。
/// </summary>
/// <param name="scene">已加载的场景。</param>
/// <param name="mode">场景加载模式。</param>
private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
// 当场景加载时重新查找并注册所有UI窗口
RegisterAllWindows();
}
}
}
}