(client) feat:健康给予,路径优化,结算界面,商店界面 (#60)

Co-authored-by: m0_75251201 <m0_75251201@noreply.gitcode.com>
Reviewed-on: http://47.107.252.169:3000/Roguelite-Game-Developing-Team/Gen_Hack-and-Slash-Roguelite/pulls/60
This commit is contained in:
2025-10-10 14:08:23 +08:00
parent 9a797479ff
commit 16b49f3d3a
1900 changed files with 114053 additions and 34157 deletions

View File

@@ -1,13 +0,0 @@
using System;
namespace Base
{
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
public class NeedSaveAttribute : Attribute
{
}
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = true)]
public class UnSaveAttribute : Attribute
{
}
}

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: bb4eb0442fc54e27955bb860d6dc2fd3
timeCreated: 1752635644

View File

@@ -1,12 +1,16 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
using Utils; // 假设此命名空间包含MonoSingleton
using Utils;
using Object = UnityEngine.Object;
// 假设此命名空间包含MonoSingleton
namespace Base
{
/// <summary>
/// 定义一个Tick更新接口用于在常规Update中执行逻辑。
/// 定义一个Tick更新接口用于在常规Update中执行逻辑。
/// </summary>
public interface ITick
{
@@ -14,7 +18,7 @@ namespace Base
}
/// <summary>
/// 定义一个TickPhysics更新接口用于在FixedUpdate中执行物理逻辑。
/// 定义一个TickPhysics更新接口用于在FixedUpdate中执行物理逻辑。
/// </summary>
public interface ITickPhysics
{
@@ -22,7 +26,7 @@ namespace Base
}
/// <summary>
/// 定义一个TickUI更新接口用于在常规Update中执行UI逻辑。
/// 定义一个TickUI更新接口用于在常规Update中执行UI逻辑。
/// </summary>
public interface ITickUI
{
@@ -30,17 +34,44 @@ namespace Base
}
/// <summary>
/// 全局计时器和更新管理器负责在Unity的Update、FixedUpdate和LateUpdate生命周期中调度注册的Ticks。
/// 支持游戏暂停、场景加载时自动重置以及缓冲区的添加/移除操作以避免迭代器错误。
/// 全局计时器和更新管理器负责在Unity的Update、FixedUpdate和LateUpdate生命周期中调度注册的Ticks。
/// 支持游戏暂停、场景加载时自动重置以及缓冲区的添加/移除操作以避免迭代器错误。
/// </summary>
public class Clock : MonoSingleton<Clock>
{
// 存储所有需要在FixedUpdate中执行物理Tick逻辑的对象
private readonly HashSet<ITickPhysics> _tickPhysics = new();
// 待添加的ITickPhysics对象缓冲区
private readonly HashSet<ITickPhysics> _tickPhysicsToAdd = new();
// 待移除的ITickPhysics对象缓冲区
private readonly HashSet<ITickPhysics> _tickPhysicsToRemove = new();
// 存储所有需要在常规Update中执行Tick逻辑的对象
private readonly HashSet<ITick> _ticks = new();
// 待添加的ITick对象缓冲区
private readonly HashSet<ITick> _ticksToAdd = new();
// 待移除的ITick对象缓冲区
private readonly HashSet<ITick> _ticksToRemove = new();
// 存储所有需要在常规Update中执行UI Tick逻辑的对象
private readonly HashSet<ITickUI> _tickUIs = new();
// 待添加的ITickUI对象缓冲区
private readonly HashSet<ITickUI> _tickUIsToAdd = new();
// 待移除的ITickUI对象缓冲区
private readonly HashSet<ITickUI> _tickUIsToRemove = new();
// 游戏是否暂停
private bool _pause;
/// <summary>
/// 获取或设置游戏的暂停状态。当设置为true时Time.timeScale将变为0当设置为false时Time.timeScale将恢复为1。
/// 该操作会检查当前状态避免重复设置Time.timeScale。
/// 获取或设置游戏的暂停状态。当设置为true时Time.timeScale将变为0当设置为false时Time.timeScale将恢复为1。
/// 该操作会检查当前状态避免重复设置Time.timeScale。
/// </summary>
public bool Pause
{
@@ -56,38 +87,61 @@ namespace Base
}
}
// 存储所有需要在常规Update中执行Tick逻辑的对象
private readonly HashSet<ITick> _ticks = new();
// 存储所有需要在FixedUpdate中执行物理Tick逻辑的对象
private readonly HashSet<ITickPhysics> _tickPhysics = new();
// 存储所有需要在常规Update中执行UI Tick逻辑的对象
private readonly HashSet<ITickUI> _tickUIs = new();
// 待添加的ITick对象缓冲区
private readonly HashSet<ITick> _ticksToAdd = new();
// 待添加的ITickPhysics对象缓冲区
private readonly HashSet<ITickPhysics> _tickPhysicsToAdd = new();
// 待添加的ITickUI对象缓冲区
private readonly HashSet<ITickUI> _tickUIsToAdd = new();
// 待移除的ITick对象缓冲区
private readonly HashSet<ITick> _ticksToRemove = new();
// 待移除的ITickPhysics对象缓冲区
private readonly HashSet<ITickPhysics> _tickPhysicsToRemove = new();
// 待移除的ITickUI对象缓冲区
private readonly HashSet<ITickUI> _tickUIsToRemove = new();
/// <summary>
/// 在单例首次创建时调用
/// 每帧更新方法
/// 如果游戏未暂停则执行所有注册的ITick对象的Tick方法
/// 执行所有注册的ITickUI对象的TickUI方法UI通常不受暂停影响
/// </summary>
protected override void OnStart()
private void Update()
{
SceneManager.sceneLoaded += OnSceneLoadedCallback;
ApplyBufferedChanges();
// 如果游戏未暂停则执行常规的Tick更新。
if (!_pause)
foreach (var tick in _ticks)
try
{
tick.Tick();
}
catch (Exception e)
{
Debug.LogError($"[Clock] ITick对象 '{tick.GetType().Name}' 在Tick()执行期间抛出异常: {e}");
}
// UI更新通常不受游戏暂停影响例如菜单动画、UI计时器等
foreach (var uiTick in _tickUIs)
try
{
uiTick.TickUI();
}
catch (Exception e)
{
Debug.LogError($"[Clock] ITickUI对象 '{uiTick.GetType().Name}' 在TickUI()执行期间抛出异常: {e}");
}
}
/// <summary>
/// 在Clock组件被销毁时调用
/// 取消订阅场景加载事件并清理所有内部Tick列表
/// 每固定帧更新方法
/// 如果游戏未暂停则执行所有注册的ITickPhysics对象的TickPhysics方法
/// </summary>
private void FixedUpdate()
{
// 如果游戏未暂停则执行物理Tick更新。
if (!_pause)
foreach (var physicsTick in _tickPhysics)
try
{
physicsTick.TickPhysics();
}
catch (Exception e)
{
Debug.LogError(
$"[Clock] ITickPhysics对象 '{physicsTick.GetType().Name}' 在TickPhysics()执行期间抛出异常: {e}");
}
}
/// <summary>
/// 在Clock组件被销毁时调用。
/// 取消订阅场景加载事件并清理所有内部Tick列表。
/// </summary>
protected void OnDestroy()
{
@@ -96,8 +150,16 @@ namespace Base
}
/// <summary>
/// 场景加载完成时的回调方法
/// 用于在加载新场景后清理所有Tick列表并重置游戏暂停状态。
/// 在单例首次创建时调用
/// </summary>
protected override void OnStart()
{
SceneManager.sceneLoaded += OnSceneLoadedCallback;
}
/// <summary>
/// 场景加载完成时的回调方法。
/// 用于在加载新场景后清理所有Tick列表并重置游戏暂停状态。
/// </summary>
/// <param name="scene">被加载的场景。</param>
/// <param name="mode">场景加载模式。</param>
@@ -107,75 +169,8 @@ namespace Base
}
/// <summary>
/// 每帧更新方法
/// 如果游戏未暂停则执行所有注册的ITick对象的Tick方法
/// 执行所有注册的ITickUI对象的TickUI方法UI通常不受暂停影响
/// </summary>
private void Update()
{
// 如果游戏未暂停则执行常规的Tick更新。
if (!_pause)
{
foreach (var tick in _ticks)
{
try
{
tick.Tick();
}
catch (System.Exception e)
{
Debug.LogError($"[Clock] ITick对象 '{tick.GetType().Name}' 在Tick()执行期间抛出异常: {e}");
}
}
}
// UI更新通常不受游戏暂停影响例如菜单动画、UI计时器等
foreach (var uiTick in _tickUIs)
{
try
{
uiTick.TickUI();
}
catch (System.Exception e)
{
Debug.LogError($"[Clock] ITickUI对象 '{uiTick.GetType().Name}' 在TickUI()执行期间抛出异常: {e}");
}
}
}
/// <summary>
/// 每固定帧更新方法。
/// 如果游戏未暂停则执行所有注册的ITickPhysics对象的TickPhysics方法。
/// </summary>
private void FixedUpdate()
{
// 如果游戏未暂停则执行物理Tick更新。
if (!_pause)
{
foreach (var physicsTick in _tickPhysics)
{
try
{
physicsTick.TickPhysics();
}
catch (System.Exception e)
{
Debug.LogError($"[Clock] ITickPhysics对象 '{physicsTick.GetType().Name}' 在TickPhysics()执行期间抛出异常: {e}");
}
}
}
}
/// <summary>
/// 在所有Update操作完成后应用Tick注册和注销的缓冲区更改确保列表在迭代期间不被修改。
/// </summary>
private void LateUpdate()
{
ApplyBufferedChanges();
}
/// <summary>
/// 将一个ITick对象添加到待添加缓冲区。它将在下一个LateUpdate中被添加到主Tick列表。
/// 如果对象已经在待移除列表中,则会先从待移除列表中移除,以处理添加/移除冲突。
/// 将一个ITick对象添加到待添加缓冲区。它将在下一个LateUpdate中被添加到主Tick列表
/// 如果对象已经在待移除列表中,则会先从待移除列表中移除,以处理添加/移除冲突。
/// </summary>
/// <param name="tick">要添加的ITick对象。</param>
public static void AddTick(ITick tick)
@@ -188,8 +183,8 @@ namespace Base
}
/// <summary>
/// 将一个ITick对象添加到待移除缓冲区。它将在下一个LateUpdate中从主Tick列表移除。
/// 如果对象已经在待添加列表中,则会先从待添加列表中移除,以处理添加/移除冲突。
/// 将一个ITick对象添加到待移除缓冲区。它将在下一个LateUpdate中从主Tick列表移除。
/// 如果对象已经在待添加列表中,则会先从待添加列表中移除,以处理添加/移除冲突。
/// </summary>
/// <param name="tick">要移除的ITick对象。</param>
public static void RemoveTick(ITick tick)
@@ -202,8 +197,8 @@ namespace Base
}
/// <summary>
/// 将一个ITickPhysics对象添加到待添加缓冲区。它将在下一个LateUpdate中被添加到主物理Tick列表。
/// 如果对象已经在待移除列表中,则会先从待移除列表中移除,以处理添加/移除冲突。
/// 将一个ITickPhysics对象添加到待添加缓冲区。它将在下一个LateUpdate中被添加到主物理Tick列表。
/// 如果对象已经在待移除列表中,则会先从待移除列表中移除,以处理添加/移除冲突。
/// </summary>
/// <param name="physics">要添加的ITickPhysics对象。</param>
public static void AddTickPhysics(ITickPhysics physics)
@@ -216,8 +211,8 @@ namespace Base
}
/// <summary>
/// 将一个ITickPhysics对象添加到待移除缓冲区。它将在下一个LateUpdate中从主物理Tick列表移除。
/// 如果对象已经在待添加列表中,则会先从待添加列表中移除,以处理添加/移除冲突。
/// 将一个ITickPhysics对象添加到待移除缓冲区。它将在下一个LateUpdate中从主物理Tick列表移除。
/// 如果对象已经在待添加列表中,则会先从待添加列表中移除,以处理添加/移除冲突。
/// </summary>
/// <param name="physics">要移除的ITickPhysics对象。</param>
public static void RemoveTickPhysics(ITickPhysics physics)
@@ -230,8 +225,8 @@ namespace Base
}
/// <summary>
/// 将一个ITickUI对象添加到待添加缓冲区。它将在下一个LateUpdate中被添加到主UI Tick列表。
/// 如果对象已经在待移除列表中,则会先从待移除列表中移除,以处理添加/移除冲突。
/// 将一个ITickUI对象添加到待添加缓冲区。它将在下一个LateUpdate中被添加到主UI Tick列表。
/// 如果对象已经在待移除列表中,则会先从待移除列表中移除,以处理添加/移除冲突。
/// </summary>
/// <param name="ui">要添加的ITickUI对象。</param>
public static void AddTickUI(ITickUI ui)
@@ -244,8 +239,8 @@ namespace Base
}
/// <summary>
/// 将一个ITickUI对象添加到待移除缓冲区。它将在下一个LateUpdate中从主UI Tick列表移除。
/// 如果对象已经在待添加列表中,则会先从待添加列表中移除,以处理添加/移除冲突。
/// 将一个ITickUI对象添加到待移除缓冲区。它将在下一个LateUpdate中从主UI Tick列表移除。
/// 如果对象已经在待添加列表中,则会先从待添加列表中移除,以处理添加/移除冲突。
/// </summary>
/// <param name="ui">要移除的ITickUI对象。</param>
public static void RemoveTickUI(ITickUI ui)
@@ -258,35 +253,26 @@ namespace Base
}
/// <summary>
/// 将缓冲区中的添加和移除操作应用到主Tick列表中。
/// 此方法应在LateUpdate中调用以确保在所有Tick执行完毕后进行列表修改从而避免迭代器错误。
/// 将缓冲区中的添加和移除操作应用到主Tick列表中。
/// 此方法应在LateUpdate中调用以确保在所有Tick执行完毕后进行列表修改从而避免迭代器错误。
/// </summary>
private void ApplyBufferedChanges()
{
if (_ticksToRemove.Count > 0)
{
foreach (var tick in _ticksToRemove)
{
_ticks.Remove(tick);
}
foreach (var tick in _ticksToRemove) _ticks.Remove(tick);
_ticksToRemove.Clear();
}
if (_tickPhysicsToRemove.Count > 0)
{
foreach (var physicsTick in _tickPhysicsToRemove)
{
_tickPhysics.Remove(physicsTick);
}
foreach (var physicsTick in _tickPhysicsToRemove) _tickPhysics.Remove(physicsTick);
_tickPhysicsToRemove.Clear();
}
if (_tickUIsToRemove.Count > 0)
{
foreach (var uiTick in _tickUIsToRemove)
{
_tickUIs.Remove(uiTick);
}
foreach (var uiTick in _tickUIsToRemove) _tickUIs.Remove(uiTick);
_tickUIsToRemove.Clear();
}
@@ -294,16 +280,10 @@ namespace Base
{
foreach (var tick in _ticksToAdd)
{
// 逻辑修改:
// 防止将已销毁的Unity对象添加到主Tick列表中。
// Unity的UnityEngine.Object重载了"=="运算符,因此对于已销毁的对象,
// (unityObject == null) 会返回 true。
if (tick is Object unityObject && unityObject == null)
{
continue; // 跳过已销毁的Unity对象
}
if (tick is Object unityObject && !unityObject) continue; // 跳过已销毁的Unity对象
_ticks.Add(tick);
}
_ticksToAdd.Clear();
}
@@ -311,12 +291,10 @@ namespace Base
{
foreach (var physicsTick in _tickPhysicsToAdd)
{
if (physicsTick is Object unityObject && unityObject == null)
{
continue;
}
if (physicsTick is Object unityObject && !unityObject) continue;
_tickPhysics.Add(physicsTick);
}
_tickPhysicsToAdd.Clear();
}
@@ -324,19 +302,17 @@ namespace Base
{
foreach (var uiTick in _tickUIsToAdd)
{
if (uiTick is Object unityObject && unityObject == null)
{
continue;
}
if (uiTick is Object unityObject && unityObject == null) continue;
_tickUIs.Add(uiTick);
}
_tickUIsToAdd.Clear();
}
}
/// <summary>
/// 集中处理所有Tick列表和缓冲区的清理。
/// 此方法会在场景加载、Clock被禁用或销毁时调用。
/// 集中处理所有Tick列表和缓冲区的清理。
/// 此方法会在场景加载、Clock被禁用或销毁时调用。
/// </summary>
/// <param name="clearPauseState">指示是否同时重置Pause状态和Time.timeScale。</param>
private void ClearAllTicksInternal(bool clearPauseState)
@@ -358,11 +334,7 @@ namespace Base
_tickPhysicsToRemove.Clear();
_tickUIsToRemove.Clear();
if (clearPauseState)
{
Pause = false;
}
if (clearPauseState) Pause = false;
}
}
}
}

View File

@@ -1,66 +1,79 @@
using System;
using System.Collections;
using System.Collections.Generic;
using Configs; // 提供List等通用集合类型
using Logging; // 提供日志记录功能
using Managers; // 提供管理器接口和实现
using System.Threading.Tasks; // 引入Task命名空间
using Configs;
using Logging;
using Managers;
using TMPro;
using UI; // TextMeshPro文本组件命名空间
using UnityEngine; // Unity引擎核心命名空间
using UnityEngine.SceneManagement; // 场景管理命名空间
using UI;
using UnityEngine;
using UnityEngine.SceneManagement;
// 提供List等通用集合类型
// 提供日志记录功能
// 提供管理器接口和实现
// TextMeshPro文本组件命名空间
// Unity引擎核心命名空间
// 场景管理命名空间
namespace Base
{
/// <summary>
/// <c>Launcher</c> 类负责管理游戏的初始化加载流程,包括管理器加载、进度条显示和场景切换。
/// <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>
/// <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的根游戏对象。
/// 加载界面UI的根游戏对象。
/// </summary>
public GameObject loadingUI;
/// <summary>
/// 用于显示加载进度的自定义进度条组件。
/// <para>请参阅类注释以了解其声明类型与预期API的差异。</para>
/// 用于显示加载进度的自定义进度条组件。
/// <para>请参阅类注释以了解其声明类型与预期API的差异。</para>
/// </summary>
public ColorBar progressBar;
/// <summary>
/// 用于显示当前加载步骤描述的文本组件。
/// 用于显示当前加载步骤描述的文本组件。
/// </summary>
public TMP_Text describeText;
/// <summary>
/// 进度条每个加载步骤的平滑过渡时间(秒)。
/// 进度条每个加载步骤的平滑过渡时间(秒)。
/// </summary>
public float duration = 0.5f;
/// <summary>
/// 加载完成后UI元素渐隐的时间
/// 加载完成后UI元素渐隐的时间
/// </summary>
public float fadeDuration = 2f;
private float _currentProgressValue = 0f; // 实际的当前进度值在0到1之间
private Color _initialTextColor; // 描述文本的原始颜色,用于渐隐效果
private float _currentProgressValue; // 实际的当前进度值在0到1之间
private Color _initialProgressBarColor1; // 进度条颜色1的原始值用于渐变
private Color _initialProgressBarColor2; // 进度条颜色2的原始值用于渐变
private Color _initialTextColor; // 描述文本的原始颜色,用于渐隐效果
/// <summary>
/// 存储所有需要在启动时加载和在重载时清理的管理器实例列表。
/// 存储所有需要在启动时加载和在重载时清理的管理器实例列表。
/// </summary>
private List<ILaunchManager> _managersToLoad;
/// <summary>
/// 获取或设置加载进度值范围为0到1。
/// 设置此属性将更新进度条的视觉显示和颜色。
/// 获取或设置加载进度值范围为0到1。
/// 设置此属性将更新进度条的视觉显示和颜色。
/// </summary>
public float Progress
{
@@ -69,20 +82,23 @@ namespace Base
// 确保进度值在0到1之间提高系统健壮性
_currentProgressValue = Mathf.Clamp01(value);
if (progressBar)
if (progressBar != null) // 对外部引用的组件进行空检查是必要的
{
// 根据进度值平滑改变进度条的颜色
if (_currentProgressValue < 0.5f)
{
// 前半段color2 从初始色渐变到白色
progressBar.color2 = Color.Lerp(_initialProgressBarColor2, Color.white, _currentProgressValue * 2);
progressBar.color2 = Color.Lerp(_initialProgressBarColor2, Color.white,
_currentProgressValue * 2);
}
else
{
progressBar.color2 = Color.white;
// 后半段color2 保持白色color1 从初始色渐变到白色
progressBar.color1 = Color.Lerp(_initialProgressBarColor1, Color.white, (_currentProgressValue - 0.5f) * 2);
progressBar.color1 = Color.Lerp(_initialProgressBarColor1, Color.white,
(_currentProgressValue - 0.5f) * 2);
}
// 通知自定义进度条组件更新显示。
// 注意UnityEngine.Gradient 作为数据结构没有 Refresh() 方法,
// 此调用表明 progressBar 实际上预期是一个具有此方法的自定义组件。
@@ -93,8 +109,8 @@ namespace Base
}
/// <summary>
/// 获取或设置UI元素的整体不透明度范围0完全透明到1完全不透明
/// 设置此属性将更新进度条和描述文本的透明度。
/// 获取或设置UI元素的整体不透明度范围0完全透明到1完全不透明
/// 设置此属性将更新进度条和描述文本的透明度。
/// </summary>
public float Opacity
{
@@ -102,7 +118,7 @@ namespace Base
{
var alpha = Mathf.Clamp01(value); // 确保透明度值在0到1之间
if (progressBar)
if (progressBar != null) // 对外部引用的组件进行空检查是必要的
{
// 更新进度条颜色的透明度
Color c1 = progressBar.color1;
@@ -119,19 +135,20 @@ namespace Base
progressBar.Refresh();
}
if (describeText)
if (describeText != null) // 对外部引用的组件进行空检查是必要的
{
// 更新描述文本颜色的透明度
// 渐隐时,描述文本的透明度在 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);
describeText.color = new Color(_initialTextColor.r, _initialTextColor.g, _initialTextColor.b,
textAlpha);
}
}
}
/// <summary>
/// 当脚本实例被启用时调用一次。
/// 用于初始化管理器列表并缓存UI元素的初始颜色。
/// 当脚本实例被启用时调用一次。
/// 用于初始化管理器列表并缓存UI元素的初始颜色。
/// </summary>
private void Awake()
{
@@ -148,15 +165,18 @@ namespace Base
EventManager.Instance,
AudioManager.Instance,
SkillTreeManager.Instance,
KeyValueArchiveManager.Instance,
SaveManager.Instance, // 存档管理器应在其他依赖管理器之后加载
HediffManager.Instance
};
// 缓存UI的初始颜色以便后续操作如渐隐或重置
_initialTextColor = describeText != null ? describeText.color : Color.white; // 如果为空,则使用默认值
if (progressBar != null)
// 对UnityEngine.Object的组件进行空检查是必要的
_initialTextColor = (describeText != null) ? describeText.color : Color.white;
if (progressBar != null) // 对UnityEngine.Object的组件进行空检查是必要的
{
_initialProgressBarColor1 = progressBar.color1;
_initialProgressBarColor2 = progressBar.color2;
@@ -169,12 +189,13 @@ namespace Base
}
/// <summary>
/// 在Awake方法之后、首次帧更新之前调用。
/// 用于判断是否需要执行完整的加载流程并根据需要启动加载或隐藏加载UI。
/// 在Awake方法之后、首次帧更新之前调用。
/// 用于判断是否需要执行完整的加载流程并根据需要启动加载或隐藏加载UI。
/// </summary>
private void Start()
{
// 如果 Program.Instance.needLoad 为 false表示游戏已加载或不需要重新加载直接隐藏加载UI
// Program.Instance 假设是一个单例,不进行空检查
// Program.Instance.NeedLoad 为 false表示游戏已加载或不需要重新加载直接隐藏加载UI
if (!Program.Instance.NeedLoad)
{
loadingUI.SetActive(false);
@@ -184,21 +205,21 @@ namespace Base
// 如果需要加载,则先清理所有管理器(用于重载或确保干净启动),再启动加载流程
ClearAllManagers();
// 初始化游戏设置
// 初始化游戏设置 (Setting.Instance 假设是一个单例,不进行空检查)
Setting.Instance.Init();
#if !DEBUG
// 在非DEBUG模式下从游戏设置中获取加载和渐隐的持续时间
duration = Base.Setting.Instance.CurrentSettings.progressStepDuration;
fadeDuration = Base.Setting.Instance.CurrentSettings.exitAnimationDuration;
// 在非DEBUG模式下从游戏设置中获取加载和渐隐的持续时间 (Setting.Instance 假设是一个单例,不进行空检查)
duration = Setting.Instance.CurrentSettings.progressStepDuration;
fadeDuration = Setting.Instance.CurrentSettings.exitAnimationDuration;
#endif
Load(); // 启动加载流程内部会调用LoadAllManagers
Program.Instance.NeedLoad = false; // 加载完成后重置加载标志
}
/// <summary>
/// 启动游戏的加载流程。
/// 该方法会激活加载UI重置进度与透明度并启动所有管理器的异步加载协程。
/// 启动游戏的加载流程。
/// 该方法会激活加载UI重置进度与透明度并启动所有管理器的异步加载协程。
/// </summary>
public void Load()
{
@@ -209,8 +230,8 @@ namespace Base
}
/// <summary>
/// 清理所有管理器,然后重新加载游戏数据。
/// 此方法适用于游戏重载、场景切换后需要重新初始化所有数据的情况。
/// 清理所有管理器,然后重新加载游戏数据。
/// 此方法适用于游戏重载、场景切换后需要重新初始化所有数据的情况。
/// </summary>
public void Reload()
{
@@ -222,29 +243,29 @@ namespace Base
}
/// <summary>
/// 遍历所有已注册的管理器,并安全地调用它们的 Clear 方法以释放资源或重置状态。
/// 遍历所有已注册的管理器,并安全地调用它们的 Clear 方法以释放资源或重置状态。
/// </summary>
private void ClearAllManagers()
{
foreach (var manager in _managersToLoad)
{
try
{
manager.Clear();
}
catch (System.Exception ex)
catch (Exception ex)
{
// 错误日志:清理管理器时发生异常。
Debug.LogError($"<color=red>清理管理器 '{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>
/// <returns>一个 <see cref="IEnumerator" />,用于协程。</returns>
private IEnumerator LoadAllManagers()
{
for (var i = 0; i < _managersToLoad.Count; i++)
@@ -252,10 +273,9 @@ namespace Base
var manager = _managersToLoad[i];
// 更新描述文本,显示当前正在加载的管理器步骤描述
if (describeText != null)
{
// 对TMP_Text组件进行空检查是必要的
if (describeText)
describeText.text = manager.StepDescription;
}
// 计算当前阶段的目标进度值
var targetProgress = (float)(i + 1) / _managersToLoad.Count;
@@ -264,16 +284,15 @@ namespace Base
yield return SmoothTransitionTo(targetProgress);
// 初始化对应的管理器,并处理可能发生的异常
// InitializeManagerSafely现在内部会等待异步任务完成
yield return InitializeManagerSafely(manager);
// yield return new WaitForSeconds(0.1f); // 此行代码可用于模拟每个管理器加载的耗时,默认不启用。
}
// 所有管理器加载完成后的处理
if (describeText != null)
{
if (describeText != null) // 对TMP_Text组件进行空检查是必要的
describeText.text = "加载完成!";
}
// 确保进度条最终达到100%
Progress = 1f;
@@ -285,47 +304,48 @@ namespace Base
}
/// <summary>
/// 尝试安全地初始化单个管理器实例,并捕获任何可能发生的异常。
/// 如果初始化失败将记录错误日志并更新UI提示。
/// 尝试安全地初始化单个管理器实例,并捕获任何可能发生的异常。
/// 如果初始化失败将记录错误日志并更新UI提示。
/// </summary>
/// <param name="manager">要初始化的管理器实例。</param>
/// <returns>一个 <see cref="IEnumerator"/>,用于协程。</returns>
/// <returns>一个 <see cref="IEnumerator" />,用于协程。</returns>
private IEnumerator InitializeManagerSafely(ILaunchManager manager)
{
var initSuccess = false;
System.Exception initException = null;
// 启动异步初始化任务,不再直接同步调用
var initTask = manager.Init();
try
// 持续等待,直到任务完成 (非阻塞式等待)
while (!initTask.IsCompleted)
{
manager.Init(); // 调用管理器的 Init 方法进行初始化
initSuccess = true;
}
catch (System.Exception ex)
{
initException = ex; // 捕获初始化过程中抛出的异常
yield return null; // 等待下一帧
}
if (!initSuccess && initException != null)
// 检查任务是否因异常而失败
if (initTask.IsFaulted)
{
// 记录错误日志:初始化管理器时出现错误。
Debug.LogError($"<color=red>初始化管理器 '{manager.StepDescription}' 时出错:</color> {initException.Message}\n{initException.StackTrace}");
if (describeText != null)
{
describeText.text = $"{manager.StepDescription} (初始化失败)"; // 更新UI显示失败信息
}
// 这里可以添加更复杂的错误处理逻辑,例如显示错误弹窗、记录到特定日志文件或允许用户选择重试。
// yield break; // 如果错误严重到无法继续,可以选择停止后续加载流程。
// 提取任务链中的实际异常信息AggregateException可能包含多个内部异常
// 使用 ?? 运算符确保即使 InnerException 为 null 也能获取到异常信息
var initException = initTask.Exception.InnerException ?? initTask.Exception;
Debug.LogError(
$"<color=red>初始化管理器 '{manager.StepDescription}' 时出错: {initException.Message}\n{initException.StackTrace}</color>");
if (describeText) // 对UnityEngine.Object的组件进行空检查是必要的
describeText.text = $"{manager.StepDescription} (初始化失败)";
}
yield return null; // 确保协程继续执行,无论初始化成功与否
else if (initTask.IsCanceled) // 额外增加对取消状态的处理
{
Debug.LogWarning($"<color=orange>初始化管理器 '{manager.StepDescription}' 被取消。</color>");
if (describeText) // 对UnityEngine.Object的组件进行空检查是必要的
describeText.text = $"{manager.StepDescription} (已取消)";
}
// 协程会自然结束,不再需要额外的 yield return null
}
/// <summary>
/// 协程:平滑地过渡进度条的当前值到指定的目标进度。
/// 使用 <c>Mathf.SmoothStep</c> 实现自然加速和减速的过渡效果。
/// 协程:平滑地过渡进度条的当前值到指定的目标进度。
/// 使用 <c>Mathf.SmoothStep</c> 实现自然加速和减速的过渡效果。
/// </summary>
/// <param name="targetProgress">目标进度值 (0到1之间)。</param>
/// <returns>一个 <see cref="IEnumerator"/>,用于协程。</returns>
/// <returns>一个 <see cref="IEnumerator" />,用于协程。</returns>
private IEnumerator SmoothTransitionTo(float targetProgress)
{
var startProgress = _currentProgressValue; // 获取当前进度作为过渡的起始点
@@ -344,9 +364,9 @@ namespace Base
}
/// <summary>
/// 协程平滑渐隐加载UI的所有元素使其透明度从完全不透明过渡到完全透明。
/// 协程平滑渐隐加载UI的所有元素使其透明度从完全不透明过渡到完全透明。
/// </summary>
/// <returns>一个 <see cref="IEnumerator"/>,用于协程。</returns>
/// <returns>一个 <see cref="IEnumerator" />,用于协程。</returns>
private IEnumerator FadeOutProgressBar()
{
var elapsedTime = 0f;
@@ -365,7 +385,7 @@ namespace Base
}
/// <summary>
/// 静态方法加载指定名称的Unity场景。
/// 静态方法加载指定名称的Unity场景。
/// </summary>
/// <param name="scene">要加载的场景名称。</param>
public static void ToScene(string scene)

View File

@@ -1,9 +1,8 @@
using System;
using UnityEngine;
namespace Base
{
public class MainMenuSet:MonoBehaviour
public class MainMenuSet : MonoBehaviour
{
private void Start()
{

View File

@@ -12,5 +12,4 @@ namespace Base
Graphics.Blit(source, destination, effectMaterial);
}
}
}

View File

@@ -1,87 +1,143 @@
using System;
using Configs;
using Data;
using Entity;
using Managers;
using Map;
using UI;
using UnityEngine;
using UnityEngine.SceneManagement;
namespace Base
{
public class PlayGameSceneControl:MonoBehaviour,ITick
public enum GameEnding
{
// 区域肃清
AreaSecured,
// 干员失联 (也可以理解为任务失败,干员失踪)
AgentMissing,
// 据点被毁
BaseDestroyed
}
public class PlayGameSceneControl : MonoBehaviour, ITick
{
public Dimension dimensionPrefab;
public WaitStartUI waitStartUI;
public bool AllDimensionsLoaded => OutsideDimensionsLoaded && InsideDimensionsLoaded;
public bool OutsideDimensionsLoaded { get; private set; }
public bool InsideDimensionsLoaded { get; private set; }
public DimensionDef OutsideDef{get; private set;}
public DimensionDef InsideDef{get; private set;}
public DimensionDef OutsideDef { get; private set; }
public DimensionDef InsideDef { get; private set; }
public Dimension OutsideDimension { get; private set; }
public Dimension InsideDimension { get; private set; }
public bool TaskCompleted { get; private set; }
private int totalThreat;
private int monstersKilled;
public Dimension OutsideDimension{get; private set;}
public Dimension InsideDimension{get; private set;}
public SettlementUI settlementUI;
private string playerAffiliation;
public bool TaskCompleted{get; private set;}
public Entity.Entity playerEntity;
public BaseBuildingHPBarUI baseBuildingHPBarUI;
private void Start()
{
playerAffiliation = Configs.ConfigManager.Instance.GetValue<string>("playerAffiliation");
OutsideDef = Program.Instance.OutsidePlayDimension;
InsideDef = Program.Instance.CurrentCharacter.insideDimensionDef ??
DefineManager.Instance.FindDefine<DimensionDef>(
Configs.ConfigManager.Instance.GetValue<string>("InsideDimension"));
ConfigManager.Instance.GetValue<string>("InsideDimension"));
if (OutsideDef == null || InsideDef == null)
{
MessageManager.Instance.DisplayMessage("初始化失败检查XML文件完整性",PromptDisplayCategory.ScreenCenterLargeText,Color.brown);
MessageManager.Instance.DisplayMessage("初始化失败检查XML文件完整性", PromptDisplayCategory.ScreenCenterLargeText,
Color.brown);
Invoke(nameof(ReturnMainMenu), 3);
return;
}
OutsideDimension=Instantiate(dimensionPrefab);
OutsideDimension = Instantiate(dimensionPrefab);
InsideDimension = Instantiate(dimensionPrefab);
OutsideDimension.transform.localPosition = new Vector3(0, 0, 1);
InsideDimension.transform.localPosition = new Vector3(0, 1000, 1);
OutsideDimension.transform.localPosition = new Vector3(-0.5f, -0.5f, 1);
InsideDimension.transform.localPosition = new Vector3(-0.5f, 999.5f, 1);
_ = OutsideDimension.ApplyDimensionDef(OutsideDef);
_ = InsideDimension.ApplyDimensionDef(InsideDef);
OutsideDimension.OnDimensionLoaded += OutsideDimensionLoaded;
OutsideDimension.OnDimensionLoaded += InsideDimensionLoaded;
InsideDimension.OnDimensionLoaded += InsideDimensionLoaded;
OutsideDimension.name = "OutsideDimension";
InsideDimension.name = "InsideDimension";
Program.Instance.SetFocusedDimension(OutsideDimension.DimensionId);
EntityManager.Instance.OnCreateEntity += OnCreateEntity;
Clock.AddTick(this);
}
private void OnDestroy()
{
EntityManager.Instance.OnCreateEntity -= OnCreateEntity;
Clock.RemoveTick(this);
}
public void Tick()
{
if (!AllDimensionsLoaded)
return;
if (EventManager.Instance.HasStoryDisplay || TaskCompleted ||
EntityManager.Instance.ExistsHostile(OutsideDimension.DimensionId,
Program.Instance.FocusedEntity.entityPrefab))
return;
EndGame(GameEnding.AreaSecured);
}
public void EndGame(GameEnding gameEnding)
{
KeyValueArchiveManager.Instance.Set("LastGameTotalThreat", totalThreat);
KeyValueArchiveManager.Instance.Set("LastGameMonstersKilled", monstersKilled);
KeyValueArchiveManager.Instance.Set("LastGameEnding", gameEnding);
TaskCompleted = true;
UIInputControl.Instance.Show("SettlementUI");
}
private void StartGame()
{
if (!AllDimensionsLoaded)
{
return;
}
if (!AllDimensionsLoaded) return;
waitStartUI.Hide();
Program.Instance.PutPlayer();
playerEntity = Program.Instance.FocusedEntity;
playerEntity.OnEntityDiedEvent += OnPlayerDied;
if (OutsideDef.story != null)
{
EventManager.Instance.PlayStory(OutsideDef.story.defName, OutsideDimension.DimensionId);
}
if (InsideDef.story != null)
{
EventManager.Instance.PlayStory(InsideDef.story.defName, InsideDimension.DimensionId);
}
Program.Instance.OnFocusedDimensionChanged += OnSwitchDimension;
baseBuildingHPBarUI.Init();
}
private void ReturnBase()
{
Program.Instance.ReturnBase();
}
private void ReturnMainMenu()
{
EscUI.ReturnMainMenu();
@@ -98,29 +154,50 @@ namespace Base
InsideDimensionsLoaded = true;
StartGame();
}
public void Tick()
private void OnCreateEntity(Entity.Entity entity)
{
if(!AllDimensionsLoaded)
if (entity is not Entity.Monster monster)
return;
if (EventManager.Instance.HasStoryDisplay || TaskCompleted)
return;
MessageManager.Instance.DisplayMessage("清理完毕!", PromptDisplayCategory.ScreenCenterLargeText,
Color.softYellow);
TaskCompleted = true;
Invoke(nameof(ReturnBase), 5);
var player = Program.Instance.FocusedEntity as Character;
if(!player)
return;
Program.Instance.CoinCount += player.Coin.Quantity;
if (AffiliationManager.Instance.GetRelation(playerAffiliation,
entity.affiliation) == Relation.Hostile)
{
monster.OnEntityDiedEvent += OnMonsterKilled;
}
else if (entity.affiliation==Configs.ConfigManager.Instance.GetValue<string>("NanorobotsAffiliation"))
{
}
}
public void ReturnBase()
private void OnPlayerDied(Entity.Entity entity)
{
Program.Instance.EndPlayGame();
SceneManager.LoadScene("Base");
EndGame(GameEnding.AgentMissing);
}
private void OnMonsterKilled(Entity.Entity entity)
{
monstersKilled++;
var monster=entity as Entity.Monster;
if (monster && monster.entityDef is MonsterDef monsterDef)
{
totalThreat += monsterDef.threat;
}
}
private void OnSwitchDimension(Dimension dimension)
{
if (dimension == OutsideDimension)
{
Program.Instance.SetFocusedEntity(playerEntity);
}
else if (dimension == InsideDimension)
{
var nanorobots = EntityManager.Instance.FindEntitiesByFaction(dimension.DimensionId,
Configs.ConfigManager.Instance.GetValue<string>("NanorobotsAffiliation"));
if (nanorobots is { Length: > 0 })
Program.Instance.SetFocusedEntity(nanorobots[0].entity);
}
}
}
}

View File

@@ -1,39 +1,24 @@
// Setting.cs
using System;
using Newtonsoft.Json;
using UnityEngine;
using Utils;
namespace Base
{
public class Setting : Utils.Singleton<Setting>
public class Setting : Singleton<Setting>
{
// 游戏设置数据类(用于序列化)
[System.Serializable]
public class GameSettings
// 窗口模式枚举
public enum WindowMode
{
public float progressStepDuration = 0.5f;
public float exitAnimationDuration = 1f;
public bool developerMode = false; // 默认值仍为 false在编辑器中会被覆盖
public bool friendlyFire = false;
public bool showMiniMap = true;
public float globalVolume = 1.0f;
public WindowMode currentWindowMode = WindowMode.Fullscreen;
public Vector2Int windowResolution = new(1920, 1080);
public string[] loadOrder;
public bool showHealthBarByHit = true;
public bool alwaysShowHealthBar = false;
public bool showHitNumber = true;
Fullscreen,
Windowed,
Borderless
}
// 当前游戏设置
public GameSettings CurrentSettings = new();
// 窗口模式枚举
public enum WindowMode { Fullscreen, Windowed, Borderless }
// 常用分辨率选项
public static readonly Vector2Int[] CommonResolutions = new Vector2Int[]
public static readonly Vector2Int[] CommonResolutions =
{
new(800, 600),
new(1024, 768),
@@ -45,6 +30,9 @@ namespace Base
new(3840, 2160)
};
// 当前游戏设置
public GameSettings CurrentSettings = new();
// 初始化加载设置
public void Init()
{
@@ -57,7 +45,7 @@ namespace Base
PlayerPrefs.SetString("GameSettings", json);
PlayerPrefs.Save();
}
public void LoadSettings()
{
if (PlayerPrefs.HasKey("GameSettings"))
@@ -80,7 +68,7 @@ namespace Base
ApplyAudioSettings();
ApplyWindowSettings();
}
// 应用音频设置
private void ApplyAudioSettings()
{
@@ -93,15 +81,38 @@ namespace Base
switch (CurrentSettings.currentWindowMode)
{
case WindowMode.Fullscreen:
Screen.SetResolution(CurrentSettings.windowResolution.x, CurrentSettings.windowResolution.y, FullScreenMode.FullScreenWindow);
Screen.SetResolution(CurrentSettings.windowResolution.x, CurrentSettings.windowResolution.y,
FullScreenMode.FullScreenWindow);
break;
case WindowMode.Windowed:
Screen.SetResolution(CurrentSettings.windowResolution.x, CurrentSettings.windowResolution.y, FullScreenMode.Windowed);
Screen.SetResolution(CurrentSettings.windowResolution.x, CurrentSettings.windowResolution.y,
FullScreenMode.Windowed);
break;
case WindowMode.Borderless:
Screen.SetResolution(CurrentSettings.windowResolution.x, CurrentSettings.windowResolution.y, FullScreenMode.MaximizedWindow);
Screen.SetResolution(CurrentSettings.windowResolution.x, CurrentSettings.windowResolution.y,
FullScreenMode.MaximizedWindow);
break;
}
}
// 游戏设置数据类(用于序列化)
[Serializable]
public class GameSettings
{
public float progressStepDuration = 0.5f;
public float exitAnimationDuration = 1f;
public bool developerMode; // 默认值仍为 false在编辑器中会被覆盖
public bool friendlyFire;
public bool showMiniMap = true;
public float globalVolume = 1.0f;
public WindowMode currentWindowMode = WindowMode.Fullscreen;
public Vector2Int windowResolution = new(1920, 1080);
public string[] loadOrder;
public bool showHealthBarByHit = true;
public bool alwaysShowHealthBar;
public bool showHitNumber = true;
}
}
}
}

View File

@@ -4,108 +4,52 @@ using System.Linq;
using UI;
using UnityEngine;
using UnityEngine.SceneManagement;
using Utils;
namespace Base
{
/// <summary>
/// UI窗口输入控制和管理类。
/// 负责根据输入显示/隐藏UI并根据UI状态管理游戏暂停。
/// UI窗口输入控制和管理类。
/// 负责根据输入显示/隐藏UI并根据UI状态管理游戏暂停。
/// </summary>
public class UIInputControl : Utils.MonoSingleton<UIInputControl>, ITickUI
public class UIInputControl : MonoSingleton<UIInputControl>, ITickUI
{
// 存储场景中所有UIBase的实例
private List<UIBase> _allWindows = new List<UIBase>();
// 缓存当前可见的窗口
private readonly List<UIBase> _visibleWindows = new List<UIBase>();
private readonly List<UIBase> _visibleWindows = new();
// 存储场景中所有UIBase的实例
private List<UIBase> _allWindows = new();
// 标记是否需要更新可见窗口缓存和暂停状态
private bool needUpdate = false;
private bool needUpdate;
/// <summary>
/// 当UI窗口的可见性状态发生改变时触发的事件
/// <list type="bullet">
/// <item>参数1: 发生改变的UIBase实例。</item>
/// <item>参数2: 窗口的新可见状态 (true为显示false为隐藏)。</item>
/// </list>
/// </summary>
public event Action<UIBase, bool> OnWindowVisibilityChanged;
/// <summary>
/// 获取所有已注册的UI窗口的总数量。
/// 获取所有已注册的UI窗口的总数量
/// </summary>
public int AllWindowCount => _allWindows.Count;
/// <summary>
/// 获取当前可见的UI窗口的数量。
/// 获取当前可见的UI窗口的数量。
/// </summary>
public int VisibleWindowCount => _visibleWindows.Count;
public bool HasWindowOpen => _visibleWindows.Any();
public bool HasInputExclusionWindow { get; private set; }
/// <summary>
/// 查询指定名称的UI窗口是否当前可见
/// 当脚本实例被销毁时调用
/// 用于在销毁时取消订阅场景加载事件,防止内存泄漏。
/// </summary>
/// <param name="uiName">UI窗口的名称。</param>
/// <returns>如果窗口可见则返回true否则返回false。</returns>
public bool IsWindowVisible(string uiName)
private void OnDestroy()
{
return _visibleWindows.Any(window => window != null && window.name == uiName && window.IsVisible);
SceneManager.sceneLoaded -= OnSceneLoaded;
Clock.RemoveTickUI(this);
}
/// <summary>
/// 查询指定名称的UI窗口是否当前处于可见状态且占用了输入
/// </summary>
/// <param name="uiName">UI窗口的名称。</param>
/// <returns>如果窗口可见且占用了输入则返回true否则返回false。</returns>
public bool IsWindowInputOccupied(string uiName)
{
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)
{
return _allWindows.FirstOrDefault(window => window != null && window.name == uiName);
}
/// <summary>
/// 查找并注册场景中所有的UI窗口包括非激活状态的。
/// </summary>
private void RegisterAllWindows()
{
_allWindows.Clear();
var activeScene = UnityEngine.SceneManagement.SceneManager.GetActiveScene();
if (!activeScene.isLoaded)
{
return;
}
foreach (var rootGameObject in activeScene.GetRootGameObjects())
{
var windows = rootGameObject.GetComponentsInChildren<UIBase>(true);
_allWindows.AddRange(windows);
}
_allWindows = _allWindows.Distinct().ToList();
foreach (var window in _allWindows)
{
if (window != null && window.gameObject != null)
{
window.Hide();
}
}
needUpdate = true;
}
/// <summary>
/// UI逻辑更新循环需要被外部的某个管理器在Update中调用。
/// UI逻辑更新循环需要被外部的某个管理器在Update中调用
/// </summary>
public void TickUI()
{
@@ -129,10 +73,7 @@ namespace Base
if (window.IsVisible)
{
if (!window.isInputOccupied)
{
Hide(window);
}
if (!window.isInputOccupied) Hide(window);
}
else
{
@@ -142,7 +83,72 @@ namespace Base
}
/// <summary>
/// 公开的显示窗口方法
/// 当UI窗口的可见性状态发生改变时触发的事件
/// <list type="bullet">
/// <item>参数1: 发生改变的UIBase实例。</item>
/// <item>参数2: 窗口的新可见状态 (true为显示false为隐藏)。</item>
/// </list>
/// </summary>
public event Action<UIBase, bool> OnWindowVisibilityChanged;
/// <summary>
/// 查询指定名称的UI窗口是否当前可见。
/// </summary>
/// <param name="uiName">UI窗口的名称。</param>
/// <returns>如果窗口可见则返回true否则返回false。</returns>
public bool IsWindowVisible(string uiName)
{
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)
{
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)
{
return _allWindows.FirstOrDefault(window => window != null && window.name == uiName);
}
/// <summary>
/// 查找并注册场景中所有的UI窗口包括非激活状态的。
/// </summary>
private void RegisterAllWindows()
{
_allWindows.Clear();
var activeScene = SceneManager.GetActiveScene();
if (!activeScene.isLoaded) return;
foreach (var rootGameObject in activeScene.GetRootGameObjects())
{
var windows = rootGameObject.GetComponentsInChildren<UIBase>(true);
_allWindows.AddRange(windows);
}
_allWindows = _allWindows.Distinct().ToList();
foreach (var window in _allWindows)
if (window != null && window.gameObject != null)
window.Hide();
needUpdate = true;
}
/// <summary>
/// 公开的显示窗口方法。
/// </summary>
/// <param name="windowToShow">要显示的窗口。</param>
public void Show(UIBase windowToShow)
@@ -153,12 +159,8 @@ namespace Base
{
var windowsToHide = _visibleWindows.ToList();
foreach (var visibleWindow in windowsToHide)
{
if (visibleWindow && visibleWindow != windowToShow && visibleWindow.IsVisible)
{
Hide(visibleWindow);
}
}
}
windowToShow.Show();
@@ -169,7 +171,7 @@ namespace Base
}
/// <summary>
/// 根据名称显示UI窗口。
/// 根据名称显示UI窗口。
/// </summary>
/// <param name="uiName">要显示的UI窗口名称。</param>
public void Show(string uiName)
@@ -180,12 +182,13 @@ namespace Base
Show(window);
return;
}
Debug.LogWarning($"[UIInputControl] 未找到名称为 '{uiName}' 的窗口来显示。");
}
/// <summary>
/// 公开的隐藏窗口方法。
/// 公开的隐藏窗口方法。
/// </summary>
/// <param name="windowToHide">要隐藏的窗口。</param>
public void Hide(UIBase windowToHide)
@@ -200,80 +203,70 @@ namespace Base
}
/// <summary>
/// 根据名称隐藏UI窗口。
/// 根据名称隐藏UI窗口。
/// </summary>
/// <param name="uiName">要隐藏的UI窗口名称。</param>
public void Hide(string uiName)
{
var visibleWindowToHide = _visibleWindows.FirstOrDefault(window => window != null && window.name == uiName && window.IsVisible);
var visibleWindowToHide =
_visibleWindows.FirstOrDefault(window => window != null && window.name == uiName && window.IsVisible);
if (visibleWindowToHide != null)
{
Hide(visibleWindowToHide);
return;
}
Debug.LogWarning($"[UIInputControl] 未找到名称为 '{uiName}' 且当前可见的窗口来隐藏。");
}
/// <summary>
/// 隐藏所有当前可见的UI窗口。
/// 隐藏所有当前可见的UI窗口。
/// </summary>
public void HideAll()
{
var windowsToHide = _visibleWindows.ToList();
foreach (var visibleWindow in windowsToHide)
{
if (visibleWindow != null && visibleWindow.IsVisible)
{
Hide(visibleWindow);
}
}
}
/// <summary>
/// 根据当前所有可见窗口的 needPause 属性来更新游戏时钟的暂停状态。
/// 根据当前所有可见窗口的 needPause 属性来更新游戏时钟的暂停状态。
/// </summary>
private void UpdatePauseState()
{
var shouldPause = _visibleWindows.Any(w => w && w.needPause);
if (Base.Clock.Instance.Pause != shouldPause)
{
Base.Clock.Instance.Pause = shouldPause;
}
if (Clock.Instance.Pause != shouldPause) Clock.Instance.Pause = shouldPause;
}
/// <summary>
/// 更新当前可见窗口的缓存列表。
/// 更新当前可见窗口的缓存列表。
/// </summary>
private void UpdateVisibleWindowsCache()
{
_visibleWindows.Clear();
HasInputExclusionWindow = false;
var exclusiveWindow = _allWindows.FirstOrDefault(window => window && window.IsVisible && window.exclusive);
if (exclusiveWindow)
{
_visibleWindows.Add(exclusiveWindow);
HasInputExclusionWindow = exclusiveWindow.isInputOccupied;
}
else
{
_visibleWindows.AddRange(_allWindows.Where(window => window && window.IsVisible));
foreach (var window in _allWindows.Where(window => window.IsVisible))
{
if (window.isInputOccupied)
HasInputExclusionWindow = true;
_visibleWindows.Add(window);
}
}
}
/// <summary>
/// 当脚本实例被销毁时调用
/// 用于在销毁时取消订阅场景加载事件,防止内存泄漏
/// </summary>
private void OnDestroy()
{
SceneManager.sceneLoaded -= OnSceneLoaded;
Clock.RemoveTickUI(this);
}
/// <summary>
/// MonoSingleton 的 OnStart 方法,在单例首次创建并激活时调用,早于普通的 Start 方法。
/// 用于订阅场景加载事件并在首次启动时注册UI窗口。
/// MonoSingleton 的 OnStart 方法,在单例首次创建并激活时调用,早于普通的 Start 方法
/// 用于订阅场景加载事件并在首次启动时注册UI窗口
/// </summary>
protected override void OnStart()
{
@@ -282,8 +275,8 @@ namespace Base
}
/// <summary>
/// 当场景加载完成时调用。
/// 用于在场景加载后重新查找并注册所有UI窗口。
/// 当场景加载完成时调用。
/// 用于在场景加载后重新查找并注册所有UI窗口。
/// </summary>
/// <param name="scene">已加载的场景。</param>
/// <param name="mode">场景加载模式。</param>
@@ -293,4 +286,4 @@ namespace Base
Clock.AddTickUI(this);
}
}
}
}