diff --git a/Client/Assets/Resources/Audio.meta b/Client/Assets/Resources/Audio.meta new file mode 100644 index 0000000..dcf8efc --- /dev/null +++ b/Client/Assets/Resources/Audio.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 342041e4bfc22ae4ba9199554f7ab489 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Client/Assets/Resources/Audio/default.mp3 b/Client/Assets/Resources/Audio/default.mp3 new file mode 100644 index 0000000..a8351b2 Binary files /dev/null and b/Client/Assets/Resources/Audio/default.mp3 differ diff --git a/Client/Assets/Resources/Audio/default.mp3.meta b/Client/Assets/Resources/Audio/default.mp3.meta new file mode 100644 index 0000000..e855d0e --- /dev/null +++ b/Client/Assets/Resources/Audio/default.mp3.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 79c8bcaaa43bb0a48ab60558de473015 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Client/Assets/Resources/Prefab/ButtonTemplate.prefab b/Client/Assets/Resources/Prefab/ButtonTemplate.prefab index 1d4f62b..30e364f 100644 --- a/Client/Assets/Resources/Prefab/ButtonTemplate.prefab +++ b/Client/Assets/Resources/Prefab/ButtonTemplate.prefab @@ -38,7 +38,7 @@ RectTransform: m_AnchorMin: {x: 0.5, y: 0.5} m_AnchorMax: {x: 0.5, y: 0.5} m_AnchoredPosition: {x: 0, y: 0} - m_SizeDelta: {x: 160, y: 30} + m_SizeDelta: {x: 200, y: 40} m_Pivot: {x: 0.5, y: 0.5} --- !u!222 &6659858081700418582 CanvasRenderer: @@ -228,8 +228,8 @@ MonoBehaviour: m_faceColor: serializedVersion: 2 rgba: 4294967295 - m_fontSize: 12 - m_fontSizeBase: 12 + m_fontSize: 15 + m_fontSizeBase: 15 m_fontWeight: 400 m_enableAutoSizing: 0 m_fontSizeMin: 18 diff --git a/Client/Assets/Resources/Prefab/TextTemplate.prefab b/Client/Assets/Resources/Prefab/TextTemplate.prefab index 77ae69a..110db59 100644 --- a/Client/Assets/Resources/Prefab/TextTemplate.prefab +++ b/Client/Assets/Resources/Prefab/TextTemplate.prefab @@ -36,7 +36,7 @@ RectTransform: m_AnchorMin: {x: 0.5, y: 0.5} m_AnchorMax: {x: 0.5, y: 0.5} m_AnchoredPosition: {x: 0, y: 0} - m_SizeDelta: {x: 200, y: 14.01} + m_SizeDelta: {x: 200, y: 25} m_Pivot: {x: 0.5, y: 1} --- !u!222 &6803973761688315705 CanvasRenderer: @@ -93,8 +93,8 @@ MonoBehaviour: m_faceColor: serializedVersion: 2 rgba: 4294967295 - m_fontSize: 14 - m_fontSizeBase: 14 + m_fontSize: 20 + m_fontSizeBase: 20 m_fontWeight: 400 m_enableAutoSizing: 0 m_fontSizeMin: 18 diff --git a/Client/Assets/Resources/Prefab/UI/DevMenu.prefab b/Client/Assets/Resources/Prefab/UI/DevMenu.prefab index e31c40e..142980e 100644 --- a/Client/Assets/Resources/Prefab/UI/DevMenu.prefab +++ b/Client/Assets/Resources/Prefab/UI/DevMenu.prefab @@ -68,10 +68,10 @@ RectTransform: m_Children: [] m_Father: {fileID: 4227482396833377269} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} - m_AnchorMin: {x: 0, y: 0} - m_AnchorMax: {x: 0, y: 0} - m_AnchoredPosition: {x: 0, y: 0} - m_SizeDelta: {x: 800, y: 20} + m_AnchorMin: {x: 0, y: 1} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: -15} + m_SizeDelta: {x: 0, y: 30} m_Pivot: {x: 0.5, y: 0.5} --- !u!222 &6707957854610092783 CanvasRenderer: @@ -335,9 +335,9 @@ RectTransform: m_Father: {fileID: 4227482396833377269} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0, y: 0} - m_AnchorMax: {x: 0, y: 0} - m_AnchoredPosition: {x: 0, y: 0} - m_SizeDelta: {x: 800, y: 430} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: -15} + m_SizeDelta: {x: 0, y: -30} m_Pivot: {x: 0.5, y: 0.5} --- !u!222 &6275009909671072884 CanvasRenderer: @@ -464,8 +464,8 @@ MonoBehaviour: m_ChildAlignment: 0 m_StartCorner: 0 m_StartAxis: 1 - m_CellSize: {x: 100, y: 25} - m_Spacing: {x: 7, y: 1} + m_CellSize: {x: 200, y: 50} + m_Spacing: {x: 10, y: 1} m_Constraint: 0 m_ConstraintCount: 2 --- !u!114 &753172512531292965 @@ -602,7 +602,6 @@ GameObject: serializedVersion: 6 m_Component: - component: {fileID: 4227482396833377269} - - component: {fileID: 7986066519765048852} - component: {fileID: 7904664609186573076} m_Layer: 5 m_Name: DevMenu @@ -632,32 +631,6 @@ RectTransform: m_AnchoredPosition: {x: 0, y: 0} m_SizeDelta: {x: 0, y: 0} m_Pivot: {x: 0.5, y: 0.5} ---- !u!114 &7986066519765048852 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 7588369677482817409} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 59f8146938fff824cb5fd77236b75775, type: 3} - m_Name: - m_EditorClassIdentifier: - m_Padding: - m_Left: 0 - m_Right: 0 - m_Top: 0 - m_Bottom: 0 - m_ChildAlignment: 0 - m_Spacing: 0 - m_ChildForceExpandWidth: 1 - m_ChildForceExpandHeight: 1 - m_ChildControlWidth: 0 - m_ChildControlHeight: 0 - m_ChildScaleWidth: 0 - m_ChildScaleHeight: 0 - m_ReverseArrangement: 0 --- !u!114 &7904664609186573076 MonoBehaviour: m_ObjectHideFlags: 0 diff --git a/Client/Assets/Resources/Prefab/UI/Pause.prefab b/Client/Assets/Resources/Prefab/UI/Pause.prefab index af9ef76..2f98224 100644 --- a/Client/Assets/Resources/Prefab/UI/Pause.prefab +++ b/Client/Assets/Resources/Prefab/UI/Pause.prefab @@ -152,7 +152,7 @@ GameObject: m_Icon: {fileID: 0} m_NavMeshLayer: 0 m_StaticEditorFlags: 0 - m_IsActive: 0 + m_IsActive: 1 --- !u!224 &6806539210387795853 RectTransform: m_ObjectHideFlags: 0 diff --git a/Client/Assets/Scenes/Game.unity b/Client/Assets/Scenes/Game.unity index 616d8b2..1b3892c 100644 --- a/Client/Assets/Scenes/Game.unity +++ b/Client/Assets/Scenes/Game.unity @@ -4486,6 +4486,10 @@ PrefabInstance: serializedVersion: 3 m_TransformParent: {fileID: 1236970686} m_Modifications: + - target: {fileID: 318369690686405214, guid: 72cde32427f7d914692a7b0d22fb791d, type: 3} + propertyPath: m_AnchorMax.x + value: 1 + objectReference: {fileID: 0} - target: {fileID: 318369690686405214, guid: 72cde32427f7d914692a7b0d22fb791d, type: 3} propertyPath: m_AnchorMax.y value: 1 @@ -4494,13 +4498,21 @@ PrefabInstance: propertyPath: m_AnchorMin.y value: 1 objectReference: {fileID: 0} + - target: {fileID: 318369690686405214, guid: 72cde32427f7d914692a7b0d22fb791d, type: 3} + propertyPath: m_SizeDelta.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 318369690686405214, guid: 72cde32427f7d914692a7b0d22fb791d, type: 3} + propertyPath: m_SizeDelta.y + value: 30 + objectReference: {fileID: 0} - target: {fileID: 318369690686405214, guid: 72cde32427f7d914692a7b0d22fb791d, type: 3} propertyPath: m_AnchoredPosition.x - value: 400 + value: 0 objectReference: {fileID: 0} - target: {fileID: 318369690686405214, guid: 72cde32427f7d914692a7b0d22fb791d, type: 3} propertyPath: m_AnchoredPosition.y - value: -10 + value: -15 objectReference: {fileID: 0} - target: {fileID: 1599592022407895033, guid: 72cde32427f7d914692a7b0d22fb791d, type: 3} propertyPath: m_AnchorMax.x @@ -4512,7 +4524,7 @@ PrefabInstance: objectReference: {fileID: 0} - target: {fileID: 1929317511491337533, guid: 72cde32427f7d914692a7b0d22fb791d, type: 3} propertyPath: m_AnchorMax.x - value: 0.99999994 + value: 1 objectReference: {fileID: 0} - target: {fileID: 1929317511491337533, guid: 72cde32427f7d914692a7b0d22fb791d, type: 3} propertyPath: m_AnchorMax.y @@ -4532,7 +4544,11 @@ PrefabInstance: objectReference: {fileID: 0} - target: {fileID: 3070964228632539618, guid: 72cde32427f7d914692a7b0d22fb791d, type: 3} propertyPath: m_SizeDelta.y - value: 4 + value: 9 + objectReference: {fileID: 0} + - target: {fileID: 4028021113825693505, guid: 72cde32427f7d914692a7b0d22fb791d, type: 3} + propertyPath: m_AnchorMax.x + value: 1 objectReference: {fileID: 0} - target: {fileID: 4028021113825693505, guid: 72cde32427f7d914692a7b0d22fb791d, type: 3} propertyPath: m_AnchorMax.y @@ -4540,15 +4556,23 @@ PrefabInstance: objectReference: {fileID: 0} - target: {fileID: 4028021113825693505, guid: 72cde32427f7d914692a7b0d22fb791d, type: 3} propertyPath: m_AnchorMin.y - value: 1 + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4028021113825693505, guid: 72cde32427f7d914692a7b0d22fb791d, type: 3} + propertyPath: m_SizeDelta.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4028021113825693505, guid: 72cde32427f7d914692a7b0d22fb791d, type: 3} + propertyPath: m_SizeDelta.y + value: -30 objectReference: {fileID: 0} - target: {fileID: 4028021113825693505, guid: 72cde32427f7d914692a7b0d22fb791d, type: 3} propertyPath: m_AnchoredPosition.x - value: 400 + value: 0 objectReference: {fileID: 0} - target: {fileID: 4028021113825693505, guid: 72cde32427f7d914692a7b0d22fb791d, type: 3} propertyPath: m_AnchoredPosition.y - value: -235 + value: -15 objectReference: {fileID: 0} - target: {fileID: 4227482396833377269, guid: 72cde32427f7d914692a7b0d22fb791d, type: 3} propertyPath: m_Pivot.x @@ -4666,6 +4690,22 @@ PrefabInstance: propertyPath: entityPlacementUI value: objectReference: {fileID: 1672332561} + - target: {fileID: 7986066519765048852, guid: 72cde32427f7d914692a7b0d22fb791d, type: 3} + propertyPath: m_ChildScaleWidth + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 7986066519765048852, guid: 72cde32427f7d914692a7b0d22fb791d, type: 3} + propertyPath: m_ChildScaleHeight + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 7986066519765048852, guid: 72cde32427f7d914692a7b0d22fb791d, type: 3} + propertyPath: m_ChildControlWidth + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 7986066519765048852, guid: 72cde32427f7d914692a7b0d22fb791d, type: 3} + propertyPath: m_ChildControlHeight + value: 0 + objectReference: {fileID: 0} - target: {fileID: 8099462081536526843, guid: 72cde32427f7d914692a7b0d22fb791d, type: 3} propertyPath: m_AnchorMax.x value: 1 @@ -4682,7 +4722,8 @@ PrefabInstance: propertyPath: m_SizeDelta.y value: -17 objectReference: {fileID: 0} - m_RemovedComponents: [] + m_RemovedComponents: + - {fileID: 7986066519765048852, guid: 72cde32427f7d914692a7b0d22fb791d, type: 3} m_RemovedGameObjects: [] m_AddedGameObjects: [] m_AddedComponents: [] @@ -5097,6 +5138,10 @@ PrefabInstance: propertyPath: m_Name value: Pause objectReference: {fileID: 0} + - target: {fileID: 8396046402299122671, guid: 620f2670398686943a232c5a71a6f1d5, type: 3} + propertyPath: m_IsActive + value: 0 + objectReference: {fileID: 0} m_RemovedComponents: [] m_RemovedGameObjects: [] m_AddedGameObjects: [] @@ -5110,18 +5155,62 @@ PrefabInstance: serializedVersion: 3 m_TransformParent: {fileID: 1236970686} m_Modifications: + - target: {fileID: 992367162028424711, guid: 1b140ccde6242874785fb30e18322eab, type: 3} + propertyPath: m_SizeDelta.x + value: 250 + objectReference: {fileID: 0} + - target: {fileID: 1914629702951577723, guid: 1b140ccde6242874785fb30e18322eab, type: 3} + propertyPath: m_SizeDelta.x + value: 300 + objectReference: {fileID: 0} + - target: {fileID: 1914629702951577723, guid: 1b140ccde6242874785fb30e18322eab, type: 3} + propertyPath: m_AnchoredPosition.x + value: 160 + objectReference: {fileID: 0} - target: {fileID: 2669798517361862450, guid: 1b140ccde6242874785fb30e18322eab, type: 3} propertyPath: m_Name value: EntityPlacementUI objectReference: {fileID: 0} - target: {fileID: 2669798517361862450, guid: 1b140ccde6242874785fb30e18322eab, type: 3} propertyPath: m_IsActive - value: 0 + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 4475653812953191369, guid: 1b140ccde6242874785fb30e18322eab, type: 3} + propertyPath: m_fontSize + value: 40 + objectReference: {fileID: 0} + - target: {fileID: 4475653812953191369, guid: 1b140ccde6242874785fb30e18322eab, type: 3} + propertyPath: m_fontSizeBase + value: 40 + objectReference: {fileID: 0} + - target: {fileID: 4541848766221811720, guid: 1b140ccde6242874785fb30e18322eab, type: 3} + propertyPath: m_text + value: "\u8BF4\u660E\u6587\u672C\u8BF4\u660E\u6587\u672C\u8BF4\u660E\u6587\u672C\u8BF4\u660E\u6587\u672C\u8BF4\u660E\u6587\u672C\u8BF4\u660E\u6587\u672C\u8BF4\u660E\u6587\u672C\u8BF4\u660E\u6587\u672C\u8BF4\u660E\u6587\u672C\u8BF4\u660E\u6587\u672C\u8BF4\u660E\u6587\u672C\u8BF4\u660E\u6587\u672C\u8BF4\u660E\u6587\u672C\u8BF4\u660E\u6587\u672C\u8BF4\u660E\u6587\u672C\u8BF4\u660E\u6587\u672C\u8BF4\u660E\u6587\u672C\u8BF4\u660E\u6587\u672C\u8BF4\u660E\u6587\u672C\u8BF4\u660E\u6587\u672C\u8BF4\u660E\u6587\u672C\u8BF4\u660E\u6587\u672C\u8BF4\u660E\u6587\u672C\u8BF4\u660E\u6587\u672C\u8BF4\u660E\u6587\u672C\u8BF4\u660E\u6587\u672C" + objectReference: {fileID: 0} + - target: {fileID: 4541848766221811720, guid: 1b140ccde6242874785fb30e18322eab, type: 3} + propertyPath: m_fontSize + value: 20 + objectReference: {fileID: 0} + - target: {fileID: 4541848766221811720, guid: 1b140ccde6242874785fb30e18322eab, type: 3} + propertyPath: m_fontSizeBase + value: 20 + objectReference: {fileID: 0} + - target: {fileID: 5585349297193151846, guid: 1b140ccde6242874785fb30e18322eab, type: 3} + propertyPath: m_fontSize + value: 16 + objectReference: {fileID: 0} + - target: {fileID: 5585349297193151846, guid: 1b140ccde6242874785fb30e18322eab, type: 3} + propertyPath: m_fontSizeBase + value: 16 objectReference: {fileID: 0} - target: {fileID: 7402578691222625163, guid: 1b140ccde6242874785fb30e18322eab, type: 3} propertyPath: focusBox value: objectReference: {fileID: 111982856} + - target: {fileID: 7782696062465364445, guid: 1b140ccde6242874785fb30e18322eab, type: 3} + propertyPath: m_SizeDelta.x + value: 300 + objectReference: {fileID: 0} - target: {fileID: 8654739876786722101, guid: 1b140ccde6242874785fb30e18322eab, type: 3} propertyPath: m_Pivot.x value: 0.5 diff --git a/Client/Assets/Scenes/Initiate.unity b/Client/Assets/Scenes/Initiate.unity index afab2fa..873a70e 100644 --- a/Client/Assets/Scenes/Initiate.unity +++ b/Client/Assets/Scenes/Initiate.unity @@ -986,6 +986,55 @@ RectTransform: m_CorrespondingSourceObject: {fileID: 1633840166558733415, guid: 97884168f61531647ba02870b2e2160f, type: 3} m_PrefabInstance: {fileID: 360627886} m_PrefabAsset: {fileID: 0} +--- !u!1 &911482831 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 911482833} + - component: {fileID: 911482832} + m_Layer: 0 + m_Name: CameraControl + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &911482832 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 911482831} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: f749a65b381541fab3f5dd5e487a457b, type: 3} + m_Name: + m_EditorClassIdentifier: + isGlobal: 1 + _zoomSpeed: 5 + _minZoom: 2 + _maxZoom: 20 + _focusLerpSpeed: 5 +--- !u!4 &911482833 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 911482831} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 1680.2731, y: 36.15279, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1001 &1141579897 PrefabInstance: m_ObjectHideFlags: 0 @@ -2118,3 +2167,4 @@ SceneRoots: - {fileID: 597126023} - {fileID: 1200712023} - {fileID: 117511565} + - {fileID: 911482833} diff --git a/Client/Assets/Scripts/Base/Launcher.cs b/Client/Assets/Scripts/Base/Launcher.cs index e835435..9d34ebb 100644 --- a/Client/Assets/Scripts/Base/Launcher.cs +++ b/Client/Assets/Scripts/Base/Launcher.cs @@ -143,6 +143,7 @@ namespace Base AffiliationManager.Instance, ItemResourceManager.Instance, EventManager.Instance, + AudioManager.Instance, }; // 缓存UI的初始颜色,以便后续操作(如渐隐)或重置 diff --git a/Client/Assets/Scripts/Base/UIInputControl.cs b/Client/Assets/Scripts/Base/UIInputControl.cs index 62c8961..97db711 100644 --- a/Client/Assets/Scripts/Base/UIInputControl.cs +++ b/Client/Assets/Scripts/Base/UIInputControl.cs @@ -1,15 +1,13 @@ -using System; using System.Collections.Generic; using System.Linq; using UI; using UnityEngine; using UnityEngine.SceneManagement; -using Object = UnityEngine.Object; namespace Base { /// - /// UI窗口输入控制和管理类 + /// UI窗口输入控制和管理类。 /// 负责根据输入显示/隐藏UI,并根据UI状态管理游戏暂停。 /// public class UIInputControl : Utils.MonoSingleton, ITickUI @@ -20,8 +18,52 @@ namespace Base private readonly List _visibleWindows = new List(); private bool needUpdate = false; + /// - /// 查找并注册场景中所有的UI窗口,包括非激活状态的 + /// 获取所有已注册的UI窗口的总数量。 + /// + public int AllWindowCount => _allWindows.Count; + + /// + /// 获取当前可见的UI窗口的数量。 + /// + public int VisibleWindowCount => _visibleWindows.Count; + + /// + /// 查询指定名称的UI窗口是否当前可见。 + /// + /// UI窗口的名称。 + /// 如果窗口可见则返回true,否则返回false。 + public bool IsWindowVisible(string uiName) + { + // 使用 Any() 方法检查 _visibleWindows 列表中是否存在名称匹配且可见的窗口 + return _visibleWindows.Any(window => window != null && window.name == uiName && window.IsVisible); + } + + /// + /// 查询指定名称的UI窗口是否当前处于可见状态且占用了输入。 + /// + /// UI窗口的名称。 + /// 如果窗口可见且占用了输入则返回true,否则返回false。 + public bool IsWindowInputOccupied(string uiName) + { + // 检查 _visibleWindows 列表中是否存在名称匹配且同时占用输入的窗口 + return _visibleWindows.Any(window => window != null && window.name == uiName && window.IsVisible && window.isInputOccupied); + } + + /// + /// 根据名称获取已注册的UI窗口实例。 + /// + /// UI窗口的名称。 + /// 匹配的UIBase实例,如果未找到则返回null。 + public UIBase GetWindow(string uiName) + { + // 从 _allWindows 列表中查找第一个名称匹配的窗口 + return _allWindows.FirstOrDefault(window => window != null && window.name == uiName); + } + + /// + /// 查找并注册场景中所有的UI窗口,包括非激活状态的。 /// private void RegisterAllWindows() { @@ -31,7 +73,7 @@ namespace Base var activeScene = UnityEngine.SceneManagement.SceneManager.GetActiveScene(); if (!activeScene.isLoaded) { - Debug.LogWarning("当前场景未加载,无法注册窗口!"); + Debug.LogWarning("[UIInputControl] 当前场景未加载,无法注册窗口!"); return; } @@ -48,6 +90,7 @@ namespace Base // 初始化所有窗口为隐藏状态 foreach (var window in _allWindows) { + // 确保窗口不为空且其GameObject未被销毁 if (window != null && window.gameObject != null) { window.Hide(); @@ -55,15 +98,14 @@ namespace Base } needUpdate = true; - Debug.Log($"窗口数量{_allWindows.Count}"); } /// - /// UI逻辑更新循环,需要被外部的某个管理器在Update中调用 + /// UI逻辑更新循环,需要被外部的某个管理器在Update中调用。 /// public void TickUI() { - //使用这个是为了让输入独占窗口关闭自己后不会立即激活其他窗口的按键,延迟一帧 + // 使用这个是为了让输入独占窗口关闭自己后不会立即激活其他窗口的按键,延迟一帧 if (needUpdate) { // 更新可见窗口缓存和暂停状态 @@ -72,12 +114,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) { // 如果窗口当前是可见的,且未占用输入,则通过按键隐藏它 @@ -95,84 +144,115 @@ namespace Base } /// - /// 公开的显示窗口方法 + /// 公开的显示窗口方法。 /// - /// 要显示的窗口 + /// 要显示的窗口。 public void Show(UIBase windowToShow) { - if (!windowToShow || windowToShow.IsVisible) return; + // 确保窗口不为空且未被销毁,并且当前不可见 + if (!windowToShow || !windowToShow.gameObject || windowToShow.IsVisible) return; // 如果窗口是独占的,隐藏所有其他窗口 if (windowToShow.exclusive) { - var windowsToHide = new List(_visibleWindows); + // 创建一个副本进行迭代,防止在 Hide() 调用中修改 _visibleWindows 导致迭代器失效 + var windowsToHide = _visibleWindows.ToList(); foreach (var visibleWindow in windowsToHide) { - Hide(visibleWindow); + // 确保窗口不是要显示的窗口本身,且仍然可见 + if (visibleWindow && visibleWindow != windowToShow && visibleWindow.IsVisible) + { + Hide(visibleWindow); + } } } // 显示目标窗口并更新缓存与暂停状态 windowToShow.Show(); - var itick=windowToShow as ITickUI; + var itick = windowToShow as ITickUI; if (itick != null) Base.Clock.AddTickUI(itick); needUpdate = true; } + /// + /// 根据名称显示UI窗口。 + /// + /// 要显示的UI窗口名称。 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}' 的窗口来显示。"); } - - + /// - /// 公开的隐藏窗口方法 + /// 公开的隐藏窗口方法。 /// - /// 要隐藏的窗口 + /// 要隐藏的窗口。 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); + needUpdate = true; } + + /// + /// 根据名称隐藏UI窗口。 + /// + /// 要隐藏的UI窗口名称。 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}' 且当前可见的窗口来隐藏。"); + } + + /// + /// 隐藏所有当前可见的UI窗口。 + /// + public void HideAll() + { + // 创建 _visibleWindows 的一个副本进行迭代,以避免在循环中修改原列表导致迭代器失效 + var windowsToHide = _visibleWindows.ToList(); + foreach (var visibleWindow in windowsToHide) + { + // 再次检查窗口是否仍然可见,因为其他操作可能已经隐藏了它 + if (visibleWindow != null && visibleWindow.IsVisible) { Hide(visibleWindow); - break; } } } - public void HideAll() - { - foreach (var visibleWindow in _visibleWindows) - { - Hide(visibleWindow); - } - } /// - /// 根据当前所有可见窗口的 needPause 属性来更新游戏时钟的暂停状态 + /// 根据当前所有可见窗口的 needPause 属性来更新游戏时钟的暂停状态。 /// 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; @@ -180,33 +260,51 @@ namespace Base } /// - /// 更新当前可见窗口的缓存列表 + /// 更新当前可见窗口的缓存列表。 /// private void UpdateVisibleWindowsCache() { _visibleWindows.Clear(); foreach (var window in _allWindows) { - if (window.IsVisible) + if (window && window.IsVisible) // 确保窗口有效且可见 { _visibleWindows.Add(window); } } } + /// + /// 当脚本实例被销毁时调用。 + /// 用于在销毁时取消订阅场景加载事件,防止内存泄漏。 + /// private void OnDestroy() { SceneManager.sceneLoaded -= OnSceneLoaded; } + /// + /// MonoSingleton 的 OnStart 方法,在单例首次创建并激活时调用,早于普通的 Start 方法。 + /// 用于订阅场景加载事件并在首次启动时注册UI窗口。 + /// protected override void OnStart() { + // 订阅场景加载事件,以便在新场景加载后重新注册UI窗口 SceneManager.sceneLoaded += OnSceneLoaded; + // 首次启动时也注册一次窗口(例如在进入第一个场景时)。 + RegisterAllWindows(); } + /// + /// 当场景加载完成时调用。 + /// 用于在场景加载后重新查找并注册所有UI窗口。 + /// + /// 已加载的场景。 + /// 场景加载模式。 private void OnSceneLoaded(Scene scene, LoadSceneMode mode) { + // 当场景加载时,重新查找并注册所有UI窗口 RegisterAllWindows(); } } -} \ No newline at end of file +} diff --git a/Client/Assets/Scripts/CameraControl/CameraControl.cs b/Client/Assets/Scripts/CameraControl/CameraControl.cs index 6a53f4b..fd085e1 100644 --- a/Client/Assets/Scripts/CameraControl/CameraControl.cs +++ b/Client/Assets/Scripts/CameraControl/CameraControl.cs @@ -6,21 +6,21 @@ namespace CameraControl { /// /// 控制游戏摄像机的移动、缩放和维度切换。 - /// 继承自 MonoSingleton 以确保场景中只有一个实例,并实现 ITick 和 ITickUI 接口以在游戏循环和UI循环中更新。 + /// 继承自 MonoSingleton 以确保场景中只有一个实例,并实现 ITick 接口以在游戏循环中更新。 /// - public class CameraControl : Utils.MonoSingleton, ITick, ITickUI + public class CameraControl : Utils.MonoSingleton, ITick { // 摄像机移动相关变量 - [SerializeField] private float _zoomSpeed = 5f; // 摄像机缩放速度 - [SerializeField] private float _minZoom = 2f; // 摄像机最小缩放级别(正交摄像机的 orthographicSize) - [SerializeField] private float _maxZoom = 20f; // 摄像机最大缩放级别 - [SerializeField] private float _focusLerpSpeed = 5f; // 摄像机跟随目标时的平滑插值速度 + [SerializeField] private float _zoomSpeed = 5f; // 摄像机缩放速度 + [SerializeField] private float _minZoom = 2f; // 摄像机最小缩放级别(正交摄像机的 orthographicSize) + [SerializeField] private float _maxZoom = 20f; // 摄像机最大缩放级别 + [SerializeField] private float _focusLerpSpeed = 5f; // 摄像机跟随目标时的平滑插值速度 - private Vector3 _dragOrigin; // 拖拽操作的起始世界坐标 - private bool _isDragging; // 标记摄像机是否正在被拖拽 + private Vector3 _dragOrigin; // 拖拽操作的起始世界坐标 + private bool _isDragging; // 标记摄像机是否正在被拖拽 + + private Camera _cachedCamera; // 缓存的场景摄像机引用 - private Camera _cachedCamera; // 缓存的场景摄像机引用 - /// /// 获取当前生效的场景摄像机。懒加载并缓存。 /// @@ -30,28 +30,24 @@ namespace CameraControl { if (!_cachedCamera) { - Debug.Log("[CameraControl] Attempting to get main camera..."); _cachedCamera = Camera.main; // 尝试获取主摄像机 if (!_cachedCamera) // 如果主摄像机不存在或未标记,则尝试查找场景中的第一个摄像机 { - Debug.LogWarning("[CameraControl] Main camera not found, attempting to find first object of type Camera."); + Debug.LogWarning("[CameraControl] 未找到主摄像机,正在尝试查找场景中第一个Camera类型的对象。"); _cachedCamera = FindFirstObjectByType(); } if (!_cachedCamera) { - Debug.LogError("[CameraControl] No available camera found in scene! CameraControl functionality will be limited."); - } - else - { - Debug.Log($"[CameraControl] Camera cached: {_cachedCamera.name}"); + Debug.LogError("[CameraControl] 场景中未找到可用的摄像机!摄像机控制功能将受限。"); } } + return _cachedCamera; } } - - private int dimensionId; // 当前摄像机控制器关注的维度索引 + + private int dimensionId; // 当前摄像机控制器关注的维度索引 private string[] dimensionList; // 维度名称列表 /// @@ -60,8 +56,7 @@ namespace CameraControl /// protected override void OnStart() { - Debug.Log("[CameraControl] OnStart called. Subscribing to OnFocusedDimensionChanged event."); - Program.Instance.OnFocusedDimensionChanged += Init; + Program.Instance.OnFocusedDimensionChanged += Init; } /// @@ -70,10 +65,9 @@ namespace CameraControl /// private void OnDestroy() { - Debug.Log("[CameraControl] OnDestroy called. Unsubscribing from OnFocusedDimensionChanged event."); Program.Instance.OnFocusedDimensionChanged -= Init; } - + /// /// 根据指定的维度对象初始化摄像机控制器。 @@ -82,21 +76,18 @@ namespace CameraControl /// 当前聚焦的维度对象。 private void Init(Dimension obj) { - Debug.Log($"[CameraControl] Init(Dimension obj) called. Focused Dimension: {(obj != null ? obj.name : "null")}"); - dimensionList = Program.Instance.Dimensions; - Debug.Log($"[CameraControl] Dimension list updated. Count: {(dimensionList != null ? dimensionList.Length.ToString() : "null")}"); - + if (!CurrentCamera) // 如果摄像机仍未找到,记录错误并返回 { - Debug.LogError("[CameraControl] No camera found! CameraControl functionality will not be fully initialized."); - return; + Debug.LogError("[CameraControl] 未找到摄像机!摄像机控制功能将无法完全初始化。"); + return; } // 处理 obj 为 null 的情况 - 此时 dimensionList 已更新 - if (!obj) + if (!obj) { - Debug.LogWarning("[CameraControl] Init method called with a null focused dimension. Dimension list updated, but camera state not set based on a specific dimension."); + Debug.LogWarning("[CameraControl] Init方法在聚焦维度为空时调用。维度列表已更新,但摄像机状态未基于特定维度设置。"); return; } @@ -105,11 +96,10 @@ namespace CameraControl if (focusedIndex != -1) { dimensionId = focusedIndex; - Debug.Log($"[CameraControl] Focused dimension '{obj.name}' found at index {dimensionId}."); } else { - Debug.LogWarning($"[CameraControl] Focused dimension '{obj.name}' not found in the dimension list. Falling back to ID 0."); + Debug.LogWarning($"[CameraControl] 聚焦维度 '{obj.name}' 未在维度列表中找到。回退到ID 0。"); dimensionId = 0; // 找不到时,回退到第一个维度,避免数组越界 } @@ -117,11 +107,10 @@ namespace CameraControl if (Program.Instance.FocusedDimension != null) { CurrentCamera.transform.position = Program.Instance.FocusedDimension.cameraPosition; - Debug.Log($"[CameraControl] Camera position set to focused dimension's camera position: {CurrentCamera.transform.position}"); } else { - Debug.LogWarning("[CameraControl] Focused dimension is null when trying to set camera position. Camera position not explicitly updated based on a dimension."); + Debug.LogWarning("[CameraControl] 尝试设置摄像机位置时,聚焦维度为空。摄像机位置未明确根据维度更新。"); } } @@ -130,51 +119,49 @@ namespace CameraControl /// private void Init() { - Debug.Log("[CameraControl] Init() called (no parameters). Calling Init(Program.Instance.FocusedDimension)."); Init(Program.Instance.FocusedDimension); } - + /// /// 切换到下一个维度。 /// 会保存当前摄像机位置到当前维度,然后加载下一个维度的摄像机位置。 /// public void NextDimension() { - Debug.Log("[CameraControl] NextDimension called."); - if (!CurrentCamera) { - Debug.LogWarning("[CameraControl] Camera reference is null, cannot switch dimensions."); + Debug.LogWarning("[CameraControl] 摄像机引用为空,无法切换维度。"); return; } + if (dimensionList == null || dimensionList.Length == 0) { - Debug.LogWarning("[CameraControl] Dimension list is empty or uninitialized, cannot switch dimensions."); + Debug.LogWarning("[CameraControl] 维度列表为空或未初始化,无法切换维度。"); return; } // 1. 保存当前摄像机的实际位置到当前维度 - if (dimensionId >= 0 && dimensionId < dimensionList.Length) + if (dimensionId >= 0 && dimensionId < dimensionList.Length) { - var currentDimension = Program.Instance.GetDimension(dimensionList[dimensionId]); - if (currentDimension != null) + var currentDimension = Program.Instance.GetDimension(dimensionList[dimensionId]); + if (currentDimension != null) { currentDimension.cameraPosition = CurrentCamera.transform.position; - Debug.Log($"[CameraControl] Saved current camera position {CurrentCamera.transform.position} to dimension '{dimensionList[dimensionId]}'."); } else { - Debug.LogWarning($"[CameraControl] Could not find Dimension object for ID {dimensionId} ({dimensionList[dimensionId]}), cannot save camera position."); + Debug.LogWarning( + $"[CameraControl] 无法找到ID为 {dimensionId} ({dimensionList[dimensionId]}) 的维度对象,无法保存摄像机位置。"); } } else { - Debug.LogWarning($"[CameraControl] Current dimension ID ({dimensionId}) is out of range, cannot save camera position (Dimension list length: {dimensionList.Length})."); + Debug.LogWarning( + $"[CameraControl] 当前维度ID ({dimensionId}) 超出范围,无法保存摄像机位置(维度列表长度: {dimensionList.Length})。"); } // 2. 更新 dimensionId, 形成循环切换 dimensionId = (dimensionId + 1) % dimensionList.Length; - Debug.Log($"[CameraControl] Updated dimensionId to {dimensionId}."); // 3. 更新聚焦维度,摄像机位置的更新将由事件触发的 Init 方法处理 SetFocusedDimensionById(dimensionId); @@ -187,20 +174,19 @@ namespace CameraControl /// 要设置的维度ID。 private void SetFocusedDimensionById(int id) { - Debug.Log($"[CameraControl] SetFocusedDimensionById called with ID: {id}."); - if (!CurrentCamera) { - Debug.LogWarning("[CameraControl] Camera reference is null, cannot set camera position."); + Debug.LogWarning("[CameraControl] 摄像机引用为空,无法设置摄像机位置。"); return; } + if (dimensionList == null || id < 0 || id >= dimensionList.Length) { - Debug.LogWarning($"[CameraControl] Dimension ID {id} is out of range or dimension list is empty, cannot set focused dimension."); + Debug.LogWarning($"[CameraControl] 维度ID {id} 超出范围或维度列表为空,无法设置聚焦维度。"); return; } + Program.Instance.SetFocusedDimension(dimensionList[id]); - Debug.Log($"[CameraControl] Program.Instance.SetFocusedDimension called for '{dimensionList[id]}'."); } /// @@ -211,12 +197,12 @@ namespace CameraControl if (!CurrentCamera) return; // 确保相机存在 // 当没有拖拽且存在聚焦实体时,摄像机跟随聚焦实体 - if (!_isDragging && Program.Instance.FocusedEntity) + if (!_isDragging && Program.Instance.FocusedEntity) { var targetPosition = new Vector3( Program.Instance.FocusedEntity.Position.x, Program.Instance.FocusedEntity.Position.y, - CurrentCamera.transform.position.z); + CurrentCamera.transform.position.z); // 使用 deltaTime 进行平滑的摄像机跟随 CurrentCamera.transform.position = Vector3.Lerp( @@ -224,26 +210,16 @@ namespace CameraControl targetPosition, Time.deltaTime * _focusLerpSpeed); } - + HandleMiddleMouseDrag(); // 处理鼠标中键拖拽 + // HandleMouseZoom(); // 处理鼠标滚轮缩放 // 按下 Tab 键时切换到下一个维度 if (Input.GetKeyDown(KeyCode.Tab)) { - Debug.Log("[CameraControl] Tab key pressed. Calling NextDimension()."); NextDimension(); } + } - /// - /// 在UI更新循环中每帧调用,用于处理用户界面的摄像机操作,如鼠标拖拽和缩放。 - /// - public void TickUI() - { - if (!CurrentCamera) // 确保相机存在 - return; - - HandleMiddleMouseDrag(); // 处理鼠标中键拖拽 - HandleMouseZoom(); // 处理鼠标滚轮缩放 - } /// /// 设置摄像机的世界坐标位置。 @@ -254,11 +230,10 @@ namespace CameraControl if (CurrentCamera) { CurrentCamera.transform.position = position; - Debug.Log($"[CameraControl] Camera position explicitly set to: {position}."); } else { - Debug.LogWarning("[CameraControl] Camera reference is null, cannot set position."); + Debug.LogWarning("[CameraControl] 摄像机引用为空,无法设置位置。"); } } @@ -274,11 +249,9 @@ namespace CameraControl { _dragOrigin = CurrentCamera.ScreenToWorldPoint(Input.mousePosition); _isDragging = true; - Debug.Log($"[CameraControl] Mouse middle button down. Starting drag from world point: {_dragOrigin}."); // 如果有聚焦实体,则在开始拖拽时取消聚焦,暂停跟随 - if (Program.Instance.FocusedEntity) + if (Program.Instance.FocusedEntity) { - Debug.Log("[CameraControl] Focused entity detected. Setting FocusedEntity to null to pause follow."); Program.Instance.SetFocusedEntity(null); } } @@ -287,14 +260,13 @@ namespace CameraControl if (Input.GetMouseButton(2) && _isDragging) { var difference = _dragOrigin - CurrentCamera.ScreenToWorldPoint(Input.mousePosition); - CurrentCamera.transform.position += difference; + CurrentCamera.transform.position += difference; } // 结束拖拽:检测鼠标中键抬起 if (Input.GetMouseButtonUp(2)) { _isDragging = false; - Debug.Log("[CameraControl] Mouse middle button up. Dragging ended."); } } @@ -307,11 +279,10 @@ namespace CameraControl var scroll = Input.GetAxis("Mouse ScrollWheel"); if (scroll == 0) return; // 没有滚轮滚动,则返回 - + // 根据滚轮输入调整正交摄像机的尺寸,并限制在最小和最大缩放之间 - var newSize = CurrentCamera.orthographicSize - scroll * _zoomSpeed; - CurrentCamera.orthographicSize = Mathf.Clamp(newSize, _minZoom, _maxZoom); - // Debug.Log($"[CameraControl] Mouse scroll wheel. New orthographicSize: {CurrentCamera.orthographicSize}"); // Too frequent for general logging + var newSize = CurrentCamera.orthographicSize - scroll * _zoomSpeed; + CurrentCamera.orthographicSize = Mathf.Clamp(newSize, _minZoom, _maxZoom); } } -} +} \ No newline at end of file diff --git a/Client/Assets/Scripts/Configs/ConfigProcessor.cs b/Client/Assets/Scripts/Configs/ConfigProcessor.cs index 3825e27..c5bfd9f 100644 --- a/Client/Assets/Scripts/Configs/ConfigProcessor.cs +++ b/Client/Assets/Scripts/Configs/ConfigProcessor.cs @@ -2,156 +2,22 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Threading; using System.Threading.Tasks; +// 未在提供代码段中使用,但保留以便与原始上下文保持一致 +// 未在提供代码段中使用,但保留以便与原始上下文保持一致 using System.Xml.Linq; -using Newtonsoft.Json; +// 未在提供代码段中使用,但保留以便与原始上下文保持一致 using UnityEngine; using UnityEngine.Networking; -using Formatting = Newtonsoft.Json.Formatting; + +// 未在提供代码段中使用,但保留以便与原始上下文保持一致 + +// 未在提供代码段中使用,但保留以便与原始上下文保持一致 namespace Configs { public static class ConfigProcessor { - // 保存文件的默认文件夹路径 - private const string FolderPath = "save"; - - /// - /// 初始化文件夹 - /// - static ConfigProcessor() - { - if (!Directory.Exists(FolderPath)) Directory.CreateDirectory(FolderPath); - } - - /// - /// 保存单个类为 JSON 文件 - /// - /// 要保存的类对象 - /// 可选的文件名(不包括扩展名) - public static bool SaveFile(T obj, string fileName = null) - { - try - { - // 如果未提供文件名,则使用类名作为文件名 - if (string.IsNullOrEmpty(fileName)) fileName = typeof(T).Name; - - // 构建完整的文件路径 - var filePath = Path.Combine(FolderPath, fileName + ".json"); - - // 将对象序列化为 JSON 字符串 - var jsonContent = JsonConvert.SerializeObject(obj, Formatting.Indented); - - // 写入文件 - File.WriteAllText(filePath, jsonContent); - - Debug.Log($"Saved file: {filePath}"); - - return true; - } - catch (Exception ex) - { - Debug.LogError($"Failed to save file: {ex.Message}"); - return false; - } - } - - /// - /// 从 JSON 文件读取单个类 - /// - /// 文件名(不包括扩展名) - public static T LoadFile(string fileName) - { - try - { - // 构建完整的文件路径 - var filePath = Path.Combine(FolderPath, fileName + ".json"); - - // 检查文件是否存在 - if (!File.Exists(filePath)) - { - Debug.LogError($"File not found: {filePath}"); - return default; - } - - // 读取文件内容 - var jsonContent = File.ReadAllText(filePath); - - // 反序列化为指定类型的对象 - var obj = JsonConvert.DeserializeObject(jsonContent); - - Debug.Log($"Loaded file: {filePath}"); - return obj; - } - catch (Exception ex) - { - Debug.LogError($"Failed to load file: {ex.Message}"); - return default; - } - } - - /// - /// 打包多个类并保存为一个存档文件 - /// - /// 存档文件名(不包括扩展名) - /// 要保存的类对象字典 - public static bool SaveArchive(string archiveName, Dictionary objects) - { - try - { - // 构建完整的文件路径 - var filePath = Path.Combine(FolderPath, archiveName + ".archive.json"); - - // 将字典中的对象序列化为 JSON - var jsonContent = JsonConvert.SerializeObject(objects, Formatting.Indented); - - // 写入文件 - File.WriteAllText(filePath, jsonContent); - - Debug.Log($"Saved archive: {filePath}"); - return true; - } - catch (Exception ex) - { - Debug.LogError($"Failed to save archive: {ex.Message}"); - return false; - } - } - - /// - /// 从存档文件中读取多个类 - /// - /// 存档文件名(不包括扩展名) - public static Dictionary LoadArchive(string archiveName) - { - try - { - // 构建完整的文件路径 - var filePath = Path.Combine(FolderPath, archiveName + ".archive.json"); - - // 检查文件是否存在 - if (!File.Exists(filePath)) - { - Debug.LogError($"Archive not found: {filePath}"); - return null; - } - - // 读取文件内容 - var jsonContent = File.ReadAllText(filePath); - - // 反序列化为字典 - var objects = JsonConvert.DeserializeObject>(jsonContent); - - Debug.Log($"Loaded archive: {filePath}"); - return objects; - } - catch (Exception ex) - { - Debug.LogError($"Failed to load archive: {ex.Message}"); - return null; - } - } /// /// 获取指定路径下的所有xml文件 @@ -164,29 +30,25 @@ namespace Configs foreach (var path in paths) { - try { // 检查目录是否存在 if (!Directory.Exists(path)) { + // **逻辑修改 1.1**: 将静默跳过改为记录警告,提供更友好的提示。 + Debug.LogWarning($"警告: 指定路径不存在或无法访问 - {path}"); continue; } - // 获取目录下的所有子文件夹 - var subDirectories = Directory.GetDirectories(path); - - // 遍历并收集每个子文件夹中的 XML 文件 - foreach (var dir in subDirectories) - { - var xmlFiles = Directory.GetFiles(dir, "*.xml", SearchOption.AllDirectories); - xmlFilePaths.AddRange(xmlFiles); - } - + // **逻辑修改 1.2**: 原始逻辑仅遍历子文件夹,导致根路径下的XML文件被忽略。 + // 现修改为直接获取当前路径及其所有子目录中的 XML 文件。 + var xmlFiles = Directory.GetFiles(path, "*.xml", SearchOption.AllDirectories); + xmlFilePaths.AddRange(xmlFiles); } catch (Exception ex) { - Debug.LogError($"加载文件时发生错误: {ex.Message}"); + // 优化错误信息,明确是哪个路径出现问题 + Debug.LogError($"获取路径 '{path}' 下的XML文件时发生错误: {ex.Message}"); } } @@ -303,20 +165,19 @@ namespace Configs } // 检查文件是否存在 - if (!System.IO.File.Exists(filePath)) + if (!File.Exists(filePath)) { Debug.LogError($"文件不存在: {filePath}"); return null; } - byte[] bytes = null; + byte[] bytes; try { // 使用 using 自动管理 FileStream 的生命周期 using (var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read)) { - fs.Seek(0, SeekOrigin.Begin); // 将游标移动到文件开头(可选) bytes = new byte[fs.Length]; // 创建一个字节数组来存储文件内容 fs.Read(bytes, 0, bytes.Length); // 读取文件内容到字节数组 } @@ -386,11 +247,93 @@ namespace Configs } catch (UnauthorizedAccessException ex) { - // 可选:记录日志或忽略无权限目录 - Console.WriteLine($"访问被拒绝: {ex.Message}"); + // **逻辑修改 3**: 在 Unity 环境中,应使用 Debug.LogError 而非 Console.WriteLine 记录错误。 + Debug.LogError($"访问目录 '{directoryPath}' 被拒绝: {ex.Message}"); } return result; } + /// + /// 根据文件路径推断音频类型。 + /// + /// 音频文件的完整路径。 + /// 推断出的 AudioType,如果无法推断则返回 AudioType.UNKNOWN。 + private static AudioType GetAudioTypeFromFilePath(string filePath) + { + string ext = Path.GetExtension(filePath)?.ToLower(); // 获取扩展名并转小写 + switch (ext) + { + case ".wav": return AudioType.WAV; + case ".mp3": return AudioType.MPEG; + case ".ogg": return AudioType.OGGVORBIS; + case ".aiff": return AudioType.AIFF; + // 可以根据需要添加更多常见的音频类型 + default: + Debug.LogWarning($"警告: 未知音频文件类型 '{ext}' for file '{filePath}'. 尝试使用 AudioType.UNKNOWN."); + return AudioType.UNKNOWN; // 让Unity尝试自动检测,但成功率可能不如指定类型高 + } + } + /// + /// 从外部指定文件中异步加载音频。 + /// 注意:此方法是异步的,需要使用 await 调用。在Unity主线程中调用以避免潜在的线程问题。 + /// + /// 音频文件的完整路径。 + /// Task,其结果为加载成功的 AudioClip 对象,或加载失败时返回 null。 + public static async Task LoadAudioByIO(string filePath) + { + if (string.IsNullOrEmpty(filePath)) + { + Debug.LogError("音频文件路径为空,请检查输入!"); + return null; + } + + if (!File.Exists(filePath)) + { + Debug.LogError($"音频文件不存在: {filePath}"); + return null; + } + + // UnityWebRequest需要使用file://协议来访问本地文件 + string uri = "file://" + Path.GetFullPath(filePath); + // 根据文件扩展名推断音频类型 + AudioType audioType = GetAudioTypeFromFilePath(filePath); + + using (UnityWebRequest www = UnityWebRequestMultimedia.GetAudioClip(uri, audioType)) + { + try + { + // 发送请求并等待完成 + // await www.SendWebRequest(); 是核心,它会异步执行网络请求。 + // 如果在Unity Editor中遇到“You are trying to load data from a www stream that had a non-OK status”等错误, + // 可以检查文件路径和文件权限。 + await www.SendWebRequest(); + + // 检查请求结果 + // www.isNetworkError 或 www.isHttpError 在较新的Unity版本中已弃用, + // 推荐使用 www.result。 + if (www.result == UnityWebRequest.Result.ConnectionError || www.result == UnityWebRequest.Result.ProtocolError) + { + Debug.LogError($"加载音频文件 '{filePath}' 失败: {www.error}"); + return null; + } + else + { + // 获取下载的AudioClip + AudioClip clip = DownloadHandlerAudioClip.GetContent(www); + if (clip == null) + { + Debug.LogError($"未能从文件 '{filePath}' 获取 AudioClip 内容,请检查文件是否损坏或格式是否支持 ({audioType} 类型)."); + } + return clip; + } + } + catch (Exception e) + { + Debug.LogError($"处理音频文件 '{filePath}' 时发生异常: {e.Message}"); + return null; + } + } + } + } -} \ No newline at end of file +} diff --git a/Client/Assets/Scripts/Managers/AudioManager.cs b/Client/Assets/Scripts/Managers/AudioManager.cs new file mode 100644 index 0000000..c6e9626 --- /dev/null +++ b/Client/Assets/Scripts/Managers/AudioManager.cs @@ -0,0 +1,232 @@ + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Data; +using UnityEngine; + +namespace Managers +{ + /// + /// 音频管理器,负责加载、管理和提供从定义数据中解析出的音频剪辑。 + /// + /// + /// 该管理器是一个单例,并在启动过程中实现 ILaunchManager 接口, + /// 用于处理游戏或应用启动时音频资源的加载和初始化。 + /// + public class AudioManager : Utils.Singleton, ILaunchManager + { + /// + /// 默认音频剪辑,在找不到对应音频时返回。 + /// + public AudioClip defaultAudioClip; + + /// + /// 存储所有已加载的音频剪辑,按音频名称(全局唯一的DefName)索引。 + /// + public Dictionary audioClips = new(); + + /// + /// 获取当前启动步骤的描述。 + /// + public string StepDescription { get; private set; } = "音频管理器正在准备中..."; + + /// + /// 初始化音频管理器,加载默认音频剪辑并处理所有 AudioDef 定义。 + /// + public void Init() + { + if (audioClips.Count > 0) + { + // 如果已经有数据,则跳过初始化,防止重复加载。 + return; + } + defaultAudioClip = Resources.Load("Default/DefaultAudio"); // 假设存在一个默认音频路径 + if (defaultAudioClip == null) + { + Debug.LogWarning("AudioManager: 无法加载默认音频 'Resources/Default/DefaultAudio'。请确保文件存在。"); + } + InitAudioDef(); + } + + /// + /// 根据 AudioDef 定义初始化并加载所有音频剪辑。 + /// + public void InitAudioDef() + { + // 缓存已加载的物理文件路径对应的 AudioClip,避免重复加载 + var audioCache = new Dictionary(); + var audioDefs = Managers.DefineManager.Instance.QueryDefinesByType(); + + if (audioDefs == null || !audioDefs.Any()) + { + Debug.Log($"AudioManager: 在定义管理器中未找到任何音频定义。({nameof(AudioDef)})"); + return; + } + + foreach (var audioDef in audioDefs) + { + if (string.IsNullOrEmpty(audioDef.path) || string.IsNullOrEmpty(audioDef.defName)) + { + Debug.LogWarning($"AudioManager: 跳过音频定义 (DefName: '{audioDef?.defName ?? "未知"}'),因为它包含空路径或DefName。(路径: '{audioDef?.path ?? ""}')"); + continue; + } + + try + { + string cacheKey; + AudioClip audioClip = null; + + if (audioDef.path.StartsWith("res:")) + { + // 处理 Unity Resources 路径 + var resPath = audioDef.path.Substring(4).Replace('\\', '/').TrimStart('/'); + cacheKey = "res://" + resPath.ToLower(); // 缓存键使用小写路径 + + // 检查音频缓存 + if (!audioCache.TryGetValue(cacheKey, out audioClip)) + { + var cleanPath = Path.ChangeExtension(resPath, null); // 去掉扩展名 + audioClip = Resources.Load(cleanPath); + if (audioClip) + audioCache[cacheKey] = audioClip; + } + } + else if (audioDef.path.Contains(':')) // 包含 PackageID:Path 或其他自定义前缀 + { + var splitIndex = audioDef.path.IndexOf(':'); + var packageID = audioDef.path.Substring(0, splitIndex); + var relativePath = audioDef.path.Substring(splitIndex + 1); + + var packageRoot = Managers.DefineManager.Instance.GetPackagePath(packageID); + if (string.IsNullOrEmpty(packageRoot)) + { + Debug.LogWarning($"AudioManager: 音频定义 '{audioDef.defName}' (路径: '{audioDef.path}'): 引用的包ID '{packageID}' 未找到或没有根路径。跳过音频加载。"); + continue; + } + + var fullPath = Path.Combine(packageRoot, relativePath).Replace('\\', '/'); + cacheKey = "file://" + fullPath.ToLower(); // 缓存键使用小写路径 + + // 检查音频缓存 + if (!audioCache.TryGetValue(cacheKey, out audioClip)) + { + // === 重要的实现细节 === + // 在真实的 Unity 项目中,直接从文件系统同步加载 AudioClip 通常不推荐, + // 且 Unity 不提供直接的同步方法。通常会使用 UnityWebRequest.GetAudioClip (异步) + // 或 Asset Bundles。为了与 ImageManager 的 ConfigProcessor.LoadTextureByIO + // 保持一致的同步风格,此处我们假设 Configs.ConfigProcessor 存在一个同步的 + // LoadAudioClipByIO 方法。 + audioClip = Configs.ConfigProcessor.LoadAudioByIO(fullPath).Result; + if (audioClip) + audioCache[cacheKey] = audioClip; + } + } + else + { + // 无前缀:使用当前定义所在包的路径 + var pack = Managers.DefineManager.Instance.GetDefinePackage(audioDef); + if (pack == null) + { + Debug.LogError($"AudioManager: 音频定义 '{audioDef.defName}' (路径: '{audioDef.path}'): 源音频未找到对应的定义包。无法确定完整路径。跳过。"); + continue; + } + var fullPath = Path.Combine(pack.packRootPath, audioDef.path).Replace('\\', '/'); + cacheKey = "file://" + fullPath.ToLower(); // 缓存键使用小写路径 + + // 检查音频缓存 + if (!audioCache.TryGetValue(cacheKey, out audioClip)) + { + audioClip = Configs.ConfigProcessor.LoadAudioByIO(fullPath).Result; + if (audioClip) + audioCache[cacheKey] = audioClip; + } + } + + // 资源加载失败 + if (audioClip == null) + { + Debug.LogError($"AudioManager: 未能加载音频定义关联的音频剪辑: '{audioDef.defName}' (路径: '{audioDef.path}')。请验证路径和文件是否存在。"); + continue; + } + + audioClips[audioDef.defName] = audioClip; // 使用 DefName 作为唯一键存储 + } + catch (Exception ex) + { + Debug.LogError( + $"AudioManager: 处理音频定义时出错: '{audioDef.defName}' (路径: '{audioDef.path}')。异常: {ex.GetType().Name}: {ex.Message}\n堆栈跟踪: {ex.StackTrace}"); + } + } + } + + /// + /// 清理所有已加载的音频剪辑数据。 + /// + /// + /// 此方法会清空 字典,释放对 AudioClip 对象的引用。 + /// 它不会卸载 ,因为它通常通过 Resources.Load 加载, + /// 其生命周期由 Unity 的资源管理系统控制,当不再被引用时会自动卸载。 + /// 对于通过 ConfigProcessor.LoadAudioClipByIO 加载的 AudioClip,如果它们不是通过 Unity API + /// 创建的 Unity.Object 类型,则可能需要额外的内存释放逻辑,但在本示例中,我们假设 + /// ConfigProcessor 会返回一个被 Unity 管理的 AudioClip。 + /// + public void Clear() + { + // 如果需要显式卸载从 Resources.Load 或外部文件加载的 AssetBundle 资源, + // 可能需要 Resources.UnloadAsset(clip) 或 AssetBundle.UnloadAllAssets(true)。 + // 但对于单独的 AudioClip 引用,通常在不再被引用时Unity会自动清理。 + audioClips.Clear(); + StepDescription = "音频管理器数据已清理。"; // 更新状态 + } + + /// + /// 重新加载所有音频数据。 + /// + /// + /// 此方法会首先调用 清理所有数据,然后调用 重新初始化。 + /// + public void Reload() + { + Clear(); + Init(); + } + + /// + /// 根据 对象获取对应的音频剪辑。 + /// + /// 包含音频名称的 对象。 + /// 如果找到对应的音频剪辑,则返回该剪辑;否则返回 + public AudioClip GetAudioClip(AudioDef audioDef) + { + if (audioDef == null) + { + Debug.LogWarning("AudioManager: 请求的 AudioDef 为空。返回默认音频。"); + return defaultAudioClip; + } + return GetAudioClip(audioDef.defName); + } + + /// + /// 根据音频名称(全局唯一的DefName)获取对应的音频剪辑。 + /// + /// 音频剪辑的名称。 + /// 如果找到对应的音频剪辑,则返回该剪辑;否则返回 + public AudioClip GetAudioClip(string name) + { + if (string.IsNullOrEmpty(name)) + { + Debug.LogWarning("AudioManager: 请求的音频名称为空或null。返回默认音频。"); + return defaultAudioClip; + } + + if (audioClips.TryGetValue(name, out var clip)) + return clip; + + // 如果未找到,返回默认音频剪辑 + Debug.LogWarning($"AudioManager: 未找到名称为 '{name}' 的音频剪辑。返回默认音频。"); + return defaultAudioClip; + } + } +} diff --git a/Client/Assets/Scripts/Managers/AudioManager.cs.meta b/Client/Assets/Scripts/Managers/AudioManager.cs.meta new file mode 100644 index 0000000..d23db8b --- /dev/null +++ b/Client/Assets/Scripts/Managers/AudioManager.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 42bb2c7fdd494670bc464905def6952f +timeCreated: 1756549944 \ No newline at end of file diff --git a/Client/Assets/Scripts/Managers/ILaunchManager.cs b/Client/Assets/Scripts/Managers/ILaunchManager.cs index 3b70db6..d47d3ad 100644 --- a/Client/Assets/Scripts/Managers/ILaunchManager.cs +++ b/Client/Assets/Scripts/Managers/ILaunchManager.cs @@ -3,6 +3,7 @@ namespace Managers public interface ILaunchManager { string StepDescription { get; } // 获取当前加载步骤的描述 + void Init(); // 初始化管理器 void Clear(); // 清理管理器,用于重载 } diff --git a/Client/Assets/Scripts/Map/Dimension.cs b/Client/Assets/Scripts/Map/Dimension.cs index 95f0bf8..1555975 100644 --- a/Client/Assets/Scripts/Map/Dimension.cs +++ b/Client/Assets/Scripts/Map/Dimension.cs @@ -43,7 +43,6 @@ namespace Map private void Awake() { - // 1. 确保 DimensionId 已初始化,这会触发 DimensionId 属性的 getter 逻辑 var id = DimensionId; // 2. 创建一个用于存放此维度下所有实体的根GameObject,方便管理 @@ -51,6 +50,11 @@ namespace Map rootObj.transform.SetParent(this.transform); // 将其作为Dimension对象的子对象 DimensionRoot = rootObj.transform; Program.Instance.RegisterDimension(this); + + mapGenerator.Init(); + var size = mapGenerator.baseMap.GetSize(); + cameraPosition = new Vector3(size.x / 2f, size.y / 2f, -10)+transform.position; + // 5. 处理 defaultOpen 逻辑,设置Program的焦点维度 // 确保在自身注册到 Program 之后再设置焦点,这样 Program 内部才能找到它 if (defaultOpen) @@ -59,12 +63,7 @@ namespace Map } } - - private void Start() - { - var size = mapGenerator.baseMap.GetSize(); - cameraPosition = new Vector3(size.x / 2f, size.y / 2f, -10)+transform.position; - } + private void OnDestroy() { diff --git a/Client/Assets/Scripts/Map/MapGenerator.cs b/Client/Assets/Scripts/Map/MapGenerator.cs index 84dca95..edf9255 100644 --- a/Client/Assets/Scripts/Map/MapGenerator.cs +++ b/Client/Assets/Scripts/Map/MapGenerator.cs @@ -7,8 +7,8 @@ namespace Map public class MapGenerator:MonoBehaviour { public DoubleMap baseMap; - - private void Awake() + + public void Init() { Managers.DefineManager.Instance.Init(); Managers.PackagesImageManager.Instance.Init(); @@ -27,7 +27,6 @@ namespace Map } baseMap.RefreshAllTiles(); - CameraControl.CameraControl.Instance.SetPosition(new Vector3(size * 0.5f, size * 0.5f, -10)); } } diff --git a/Client/Assets/Scripts/UI/FullScreenUI.cs b/Client/Assets/Scripts/UI/FullScreenUI.cs index 28bd3f5..d4d8821 100644 --- a/Client/Assets/Scripts/UI/FullScreenUI.cs +++ b/Client/Assets/Scripts/UI/FullScreenUI.cs @@ -14,7 +14,7 @@ namespace UI { if(!IsVisible) return; - if(Input.GetKeyDown(KeyCode.Escape)) + if (Input.GetKeyDown(KeyCode.Escape) || Input.GetKeyDown(actionButton)) UIInputControl.Instance.Hide(this); } } diff --git a/Client/Assets/Scripts/UI/SettingUI.cs b/Client/Assets/Scripts/UI/SettingUI.cs index b04756c..af49694 100644 --- a/Client/Assets/Scripts/UI/SettingUI.cs +++ b/Client/Assets/Scripts/UI/SettingUI.cs @@ -6,7 +6,7 @@ using UnityEngine.UI; namespace UI { - public class SettingUI : UIBase + public class SettingUI : FullScreenUI { [SerializeField] private Scrollbar globalVolume; [SerializeField] private Toggle developerMode;