UI 系统

This commit is contained in:
2026-06-09 10:41:43 +08:00
parent 247de307c6
commit e09bee31ec
31 changed files with 221439 additions and 997 deletions

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 377 KiB

View File

@@ -0,0 +1,114 @@
fileFormatVersion: 2
guid: fc8f99186f75506439df2656662fc155
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 0
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 1
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 1
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 8
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 3
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
physicsShape: []
bones: []
spriteID: 5e97eb03825dee720800000000000000
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

After

Width:  |  Height:  |  Size: 382 KiB

View File

@@ -0,0 +1,114 @@
fileFormatVersion: 2
guid: 091ff8f5921cd674ab1d2ebe25d4948b
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 0
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 1
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 1
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 8
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 3
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
physicsShape: []
bones: []
spriteID: 5e97eb03825dee720800000000000000
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

After

Width:  |  Height:  |  Size: 376 KiB

View File

@@ -0,0 +1,114 @@
fileFormatVersion: 2
guid: 86f9837013dcd7f4abd3e7280445e2d9
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 0
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 1
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 1
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 8
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 3
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
physicsShape: []
bones: []
spriteID: 5e97eb03825dee720800000000000000
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

After

Width:  |  Height:  |  Size: 359 KiB

View File

@@ -0,0 +1,114 @@
fileFormatVersion: 2
guid: 4b6727a600ac7a54cb43f68bb78d0e82
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 0
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 1
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 1
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 8
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 3
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
physicsShape: []
bones: []
spriteID: 5e97eb03825dee720800000000000000
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

After

Width:  |  Height:  |  Size: 375 KiB

View File

@@ -0,0 +1,114 @@
fileFormatVersion: 2
guid: 68837cb144e444549b41a8857bda4119
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 0
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 1
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 1
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 8
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 3
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
physicsShape: []
bones: []
spriteID: 5e97eb03825dee720800000000000000
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

After

Width:  |  Height:  |  Size: 377 KiB

View File

@@ -0,0 +1,114 @@
fileFormatVersion: 2
guid: 14e41296f68fd8a49836d6bea5415f7e
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 0
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 1
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 1
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 8
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 3
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
physicsShape: []
bones: []
spriteID: 5e97eb03825dee720800000000000000
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -27653,7 +27653,7 @@ BoxCollider:
m_ProvidesContacts: 0
m_Enabled: 1
serializedVersion: 3
m_Size: {x: 26.091469, y: 0, z: 1}
m_Size: {x: 20.912247, y: 0, z: 1}
m_Center: {x: 3.5, y: 3, z: -64}
--- !u!4 &1456787688
Transform:
@@ -215862,7 +215862,7 @@ BoxCollider:
m_ProvidesContacts: 0
m_Enabled: 1
serializedVersion: 3
m_Size: {x: 6.091469, y: 0, z: 1}
m_Size: {x: 0.9122467, y: 0, z: 1}
m_Center: {x: -1, y: 0.5, z: -64}
--- !u!4 &2043137492
Transform:

View File

@@ -124,10 +124,11 @@ namespace BaseGames.Core
// 等一帧:让 Persistent 内所有组件的 Start() 跑完(尤其 InputReaderBootstrap 的 EnableUIInput 兜底)。
yield return null;
// 复刻新游戏初始状态:确保槽 0 存在内存存档,供存档点 / 世界状态等系统使用。
var save = ServiceLocator.GetOrDefault<ISaveService>();
if (save != null && !save.HasSave(0))
save.CreateSlot(0, false);
// 复用正式新游戏会话入口:建档并应用 → ISaveablePlayerStats 等)广播初始数值 →
// 粘性事件频道_replayLastValueToNewSubscribers留存 → 随后激活的 HUD 订阅时回放,血条/灵珠等正确填充。
// 此处玩家已在场景中且已注册 ISaveableCreateSlot 会立即对其应用空档并广播(见 GameSaveManager.CreateSlot
// dev 与正常新游戏走同一 IGameSessionService.BeginNewGame不再手搓建档/广播,杜绝与正式流程分叉。
ServiceLocator.GetOrDefault<IGameSessionService>()?.BeginNewGame(0, DifficultyLevel.Normal);
// 驱动状态机走完整合法链路Initializing → MainMenu → LoadingScene → Gameplay。
// 期间不加载任何场景,仅切换全局状态;末态 Gameplay 使 HUD 显示、暂停可用、输入切到 Gameplay。
@@ -139,23 +140,32 @@ namespace BaseGames.Core
}
/// <summary>
/// 返回当前已加载的、非系统场景(即游玩场景)的名称;没有则返回 null
/// 返回当前“活动场景”的名称——仅当它是游玩场景(非系统场景)时;否则返回 null回退正常启动
/// <para>
/// 用活动场景而非遍历:按 Play 时你在编辑器里打开的那个场景即活动场景Persistent 是 Additive 加载(非活动)。
/// 这样从 MainMenu / Persistent 按 Play 时一定回退正常流程,不会被直连劫持。
/// 仅按场景名约定判断,避免 Core 反向依赖 World 程序集的 RoomController 类型。
/// </para>
/// </summary>
private static string FindOpenGameplaySceneName()
{
for (int i = 0; i < SceneManager.sceneCount; i++)
{
var scene = SceneManager.GetSceneAt(i);
if (!scene.isLoaded) continue;
string n = scene.name;
if (string.IsNullOrEmpty(n)) continue;
if (n == AddressKeys.ScenePersistentName || n == AddressKeys.ScenePersistent) continue;
if (n == AddressKeys.SceneMainMenu) continue;
return n;
}
return null;
var active = SceneManager.GetActiveScene();
if (!active.isLoaded) return null;
string n = active.name;
if (string.IsNullOrEmpty(n) || IsSystemScene(n)) return null;
return n;
}
/// <summary>
/// 是否为启动链路中的系统场景Persistent / MainMenu。从这些场景按 Play 应走正常启动流程,不触发直连。
/// 同时匹配 Unity 场景名(文件名,如 "MainMenu" / "Persistent")与 Addressable key"Scene_MainMenu" 等),
/// 因为两者并不一致(文件 MainMenu.unity → 场景名 "MainMenu"Addressable key 为 "Scene_MainMenu")。
/// </summary>
private static bool IsSystemScene(string n)
=> n == AddressKeys.ScenePersistentName // "Persistent"(场景名)
|| n == AddressKeys.ScenePersistent // "Scene_Persistent"Addressable key
|| n == AddressKeys.SceneMainMenu // "Scene_MainMenu"Addressable key
|| n == "MainMenu"; // 实际场景文件名 MainMenu.unity → 场景名
#endif
private void OnEnable()

View File

@@ -78,6 +78,9 @@ namespace BaseGames.Core
else
Debug.LogWarning("[GameServiceRegistrar] ⚠ _checkpointService 未绑定ICheckpointService 未注册。", this);
// 会话入口服务(无状态,调用时解析依赖;统一新游戏/继续/dev 直连的会话初始化)
ServiceLocator.Register<IGameSessionService>(new GameSessionService());
#if UNITY_EDITOR
var sb = new System.Text.StringBuilder("[GameServiceRegistrar] ✅ 服务注册完成:");
if (_deathRespawnService) sb.Append(" IDeathRespawnService");

View File

@@ -0,0 +1,46 @@
using UnityEngine;
using BaseGames.Core.Events; // DifficultyLevel
namespace BaseGames.Core
{
/// <summary>
/// 游戏会话入口服务:统一"开始一局新游戏"的初始化步骤,供主菜单新游戏、开发直连、调试入口共用,
/// 避免会话初始化逻辑在各入口分叉(历史上 dev 直连曾手搓建档 + 广播,导致与正式流程不一致的 bug
/// </summary>
public interface IGameSessionService
{
/// <summary>
/// 开始一局新游戏会话:
/// <list type="number">
/// <item>建立内存存档槽(<see cref="ISaveService.CreateSlot"/>)——其内部会立即对已注册的
/// ISaveable 应用空档并广播初始数值HP/MaxHP/货币等),配合粘性事件频道使 HUD 正确填充。</item>
/// <item>应用难度档位(<see cref="IDifficultyService.BeginNewGame"/>)。</item>
/// </list>
/// 不负责场景加载与状态机转换——调用方各自决定(正常流程发 SceneLoadRequestdev 直连原地切状态)。
/// </summary>
void BeginNewGame(int slotIndex, DifficultyLevel level);
}
/// <summary>
/// <see cref="IGameSessionService"/> 的默认实现。无状态,经 ServiceLocator 在调用时解析所需服务,
/// 因此不依赖服务注册顺序。由 GameServiceRegistrar 注册。
/// </summary>
public sealed class GameSessionService : IGameSessionService
{
public void BeginNewGame(int slotIndex, DifficultyLevel level)
{
var save = ServiceLocator.GetOrDefault<ISaveService>();
if (save == null)
{
Debug.LogError("[GameSessionService] ISaveService 未注册,无法开始新游戏会话。");
return;
}
// 建档CreateSlot 内部已对已注册 ISaveable 应用空档并广播初始值(见 GameSaveManager.CreateSlot
save.CreateSlot(slotIndex, level == DifficultyLevel.SteelSoul);
// 应用难度(可缺省,难度服务未注册时跳过)。
ServiceLocator.GetOrDefault<IDifficultyService>()?.BeginNewGame(level);
}
}
}

View File

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

View File

@@ -43,6 +43,29 @@ namespace BaseGames.Core.Save
ServiceLocator.Register<ISaveableRegistry>(this);
}
private void Start()
{
// 注册兜底:补登记"早于本注册表初始化就已存在于已加载场景中的 ISaveable"。
// 正常流程下游玩场景晚于 Persistent 加载saveable 在 OnEnable 即可取到 ISaveableRegistry 完成注册;
// 但开发直连(游玩场景作为首场景、与 Persistent 服务同帧初始化)时,这些 saveable 的 OnEnable
// 可能早于本注册表注册 → ServiceLocator.GetOrDefault<ISaveableRegistry>() 取到 null → 漏注册。
// 启动时全量扫描一次兜底确保不漏Register 幂等Add 去重),不会重复登记。
ReconcileSaveablesInLoadedScenes();
}
/// <summary>扫描所有已加载场景中的 ISaveable MonoBehaviour 并补登记(幂等)。仅启动时调用一次。</summary>
private void ReconcileSaveablesInLoadedScenes()
{
var all = FindObjectsOfType<MonoBehaviour>(true);
int added = 0;
foreach (var mb in all)
if (mb is ISaveable s && !_saveables.Contains(s)) { Register(s); added++; }
#if UNITY_EDITOR
if (added > 0)
Debug.Log($"[SaveManager] 启动兜底补登记 {added} 个早注册场景中的 ISaveableinit 时序兜底)。");
#endif
}
/// <summary>
/// 注入存储后端。由 GameServiceRegistrar.Awake 在注册服务后立即调用。
/// 允许在测试环境替换为 InMemoryStorage 等替代实现。
@@ -261,6 +284,13 @@ namespace BaseGames.Core.Save
_current = new SaveData();
_current.Meta.SlotIndex = slotIndex;
_current.Meta.IsSteelSoul = steelSoul;
// 与 LoadAsync 对称:立即对已注册的 ISaveable 应用新档(空档 → 各系统恢复为初始值并广播)。
// 这使"先注册、后建档"(如 dev 直连:玩家已在场景中、再建槽)与"先建档、后注册"(正常新游戏:
// 主菜单建槽、玩家随场景加载后注册由 Register 补发 OnLoad表现一致——
// 初始数值经事件频道广播配合粘性频道_replayLastValueToNewSubscribers让晚激活的 HUD 也能拿到。
var snapshot = _saveables.ToList();
foreach (var s in snapshot) s.OnLoad(_current);
}
public async Task DeleteSlotAsync(int slotIndex)

View File

@@ -30,9 +30,8 @@ namespace BaseGames.Editor.Debugging
return;
}
var save = ServiceLocator.GetOrDefault<ISaveService>();
if (save != null && !save.HasSave(0))
save.CreateSlot(0, false); // 建立内存存档,确保新游戏初始状态
// 统一会话入口建档内部应用空档并广播初始值HUD 正确填充)+ 应用难度。与正式新游戏共用。
ServiceLocator.GetOrDefault<IGameSessionService>()?.BeginNewGame(0, DifficultyLevel.Normal);
// 必须经事件频道 EVT_SceneLoadRequest 发请求SceneService 据此加载场景,
// GameManager 据此驱动状态机 LoadingScene → GameplayHUD/小地图随之显示)。

View File

@@ -29,6 +29,7 @@ namespace BaseGames.Editor.UI
// ── HUDRoot ───────────────────────────────────────────────────────
GameObject hudRootGo = GetOrCreateChild(canvasGo.transform, "HUDRoot").gameObject;
StretchFull(hudRootGo.transform); // 铺满 Canvas使子元件的四角锚定映射到真实屏幕角
HUDController hudCtrl = GetOrAddComponent<HUDController>(hudRootGo);
// 若场景中已存在 UIManager自动将 _hudRoot 指向本 HUDRoot
@@ -72,11 +73,18 @@ namespace BaseGames.Editor.UI
// ── 形态图标Form Icons × 4────────────────────────────────────
const int kFormIconCount = 4;
GameObject formIconsRoot = GetOrCreateChild(hudRootGo.transform, "FormIcons").gameObject;
var formLayout = GetOrAddComponent<HorizontalLayoutGroup>(formIconsRoot);
formLayout.childForceExpandWidth = false;
formLayout.childForceExpandHeight = false;
formLayout.spacing = 6f;
Image[] formImages = new Image[kFormIconCount];
for (int i = 0; i < kFormIconCount; i++)
{
GameObject iconGo = GetOrCreateChild(formIconsRoot.transform, $"FormIcon_{i}").gameObject;
formImages[i] = GetOrAddComponent<Image>(iconGo);
var le = GetOrAddComponent<LayoutElement>(iconGo);
le.preferredWidth = 40f;
le.preferredHeight = 40f;
}
// ── 交互提示InteractPrompt─────────────────────────────────────
@@ -242,6 +250,25 @@ namespace BaseGames.Editor.UI
AssignAsset(toolHUD, "_onToolUsed", report, false, "EVT_ToolUsed");
// ── 布局摆位(点锚定到屏幕四角,避免元件堆叠在中心;尺寸/留白为占位,美术可再精修)──
// 左上角:战斗信息簇(血条 / 魄元 / 灵力 / 灵泉 / 形态 / 灵铢 / 状态效果,自上而下)
SetRect(hpContainerGo.transform, TopLeft, TopLeft, new Vector2(40f, -40f), new Vector2(500f, 50f));
SetRect(soulGaugeGo.transform, TopLeft, TopLeft, new Vector2(40f, -100f), new Vector2(240f, 22f));
SetRect(spiritGaugeGo.transform, TopLeft, TopLeft, new Vector2(40f, -128f), new Vector2(240f, 22f));
SetRect(springContainerGo.transform, TopLeft, TopLeft, new Vector2(40f, -160f), new Vector2(240f, 32f));
SetRect(formIconsRoot.transform, TopLeft, TopLeft, new Vector2(40f, -200f), new Vector2(240f, 44f));
SetRect(lingZhuGo.transform, TopLeft, TopLeft, new Vector2(40f, -252f), new Vector2(200f, 40f));
SetRect(statusHUDGo.transform, TopLeft, TopLeft, new Vector2(40f, -300f), new Vector2(320f, 44f));
// 顶部居中Boss 血条(默认隐藏)
SetRect(bossBarGo.transform, TopCenter, TopCenter, new Vector2(0f, -40f), new Vector2(720f, 60f));
// 右上角:任务追踪
SetRect(questTrackerGo.transform, TopRight, TopRight, new Vector2(-40f, -40f), new Vector2(320f, 220f));
// 右下角:法术槽 + 工具栏
SetRect(spellSlotGo.transform, BottomRight, BottomRight, new Vector2(-40f, 40f), new Vector2(90f, 90f));
SetRect(toolHUDGo.transform, BottomRight, BottomRight, new Vector2(-150f, 40f), new Vector2(180f, 90f));
// 底部居中:交互提示
SetRect(interactPromptGo.transform, BottomCenter, BottomCenter, new Vector2(0f, 140f), new Vector2(320f, 64f));
// ── 手工步骤说明 ──────────────────────────────────────────────────
// _hpCellPrefab / _springIconPrefab 已自动创建并绑定(占位红/青方块,美术可替换)。
report.Add("BossHPBar._phaseMarkerPrefab请将阶段标记点 Prefab 赋给该字段。");
@@ -352,12 +379,47 @@ namespace BaseGames.Editor.UI
Transform child = parent.Find(name);
if (child != null) return child;
GameObject go = new GameObject(name);
// UI 节点必须是 RectTransformnew GameObject 默认创建普通 Transform
// 仅当随后挂上 Graphic/LayoutGroup 时才会被隐式转换——纯逻辑容器FormIcons / SpellSlot 等)
// 会停留为普通 Transform 而无法锚定/定位。显式带 RectTransform 创建,杜绝该缺陷。
GameObject go = new GameObject(name, typeof(RectTransform));
Undo.RegisterCreatedObjectUndo(go, $"Create {name}");
go.transform.SetParent(parent, false);
return go.transform;
}
// ── 锚定 / 布局 ───────────────────────────────────────────────────────
/// <summary>设置 RectTransform 的锚点 / 轴心 / 锚定坐标 / 尺寸(统一布局入口)。</summary>
private static void SetRect(Transform t, Vector2 anchor, Vector2 pivot, Vector2 anchoredPos, Vector2 size)
{
var rt = t as RectTransform;
if (rt == null) return;
rt.anchorMin = anchor;
rt.anchorMax = anchor;
rt.pivot = pivot;
rt.sizeDelta = size;
rt.anchoredPosition = anchoredPos;
}
/// <summary>将 RectTransform 设为全屏拉伸铺满父节点offset 全 0。用于 HUDRoot 等容器根。</summary>
private static void StretchFull(Transform t)
{
var rt = t as RectTransform;
if (rt == null) return;
rt.anchorMin = Vector2.zero;
rt.anchorMax = Vector2.one;
rt.pivot = new Vector2(0.5f, 0.5f);
rt.offsetMin = Vector2.zero;
rt.offsetMax = Vector2.zero;
}
// 常用锚/轴心常量(点锚定,非拉伸)
private static readonly Vector2 TopLeft = new(0f, 1f);
private static readonly Vector2 TopCenter = new(0.5f, 1f);
private static readonly Vector2 TopRight = new(1f, 1f);
private static readonly Vector2 BottomRight = new(1f, 0f);
private static readonly Vector2 BottomCenter= new(0.5f, 0f);
private static T GetOrAddComponent<T>(GameObject go) where T : Component
{
T c = go.GetComponent<T>();

View File

@@ -232,6 +232,8 @@ namespace BaseGames.Input
BindStarted(_ui, "InventoryTabPrev", () => _onInventoryTabPrev?.Raise());
// 快速直达UI Map 同名 ActionHub 已开时按 M 直接跳地图 Tab。共用 Gameplay 同一频道。
BindStarted(_ui, "QuickMap", () => _onQuickMap?.Raise());
// 背包键UI Map 同名 ActionTabHub 已开时按 Tab 关闭(与 Gameplay 同频道UIManager 做 toggle
BindStarted(_ui, "Inventory", () => _onInventoryOpen?.Raise());
}
_isBound = true;

View File

@@ -129,6 +129,12 @@ namespace BaseGames.UI.Inventory
if (_tabs[i].content != null && _tabs[i].content.name == contentName) { SelectTab(i); return; }
}
/// <summary>当前选中 Tab 的内容根名(如 "Content_Map");无有效 Tab 时为 null。供"同键 toggle"判断当前是否已在目标 Tab。</summary>
public string CurrentContentName
=> _tabs != null && _currentIndex >= 0 && _currentIndex < _tabs.Length && _tabs[_currentIndex].content != null
? _tabs[_currentIndex].content.name
: null;
private void SelectTab(int index, bool raise, bool animateEntry)
{
if (_tabs == null || _tabs.Length == 0) return;

View File

@@ -149,9 +149,8 @@ namespace BaseGames.UI.Menus
if (svc.HasSave(slotIndex))
await svc.DeleteSlotAsync(slotIndex);
bool steel = level == DifficultyLevel.SteelSoul;
svc.CreateSlot(slotIndex, steel);
ServiceLocator.GetOrDefault<IDifficultyService>()?.BeginNewGame(level);
// 统一会话入口:建档(内部应用空档并广播初始值)+ 应用难度。与 dev 直连共用,避免分叉。
ServiceLocator.GetOrDefault<IGameSessionService>()?.BeginNewGame(slotIndex, level);
_onSlotConfirmed?.Raise(slotIndex);
}

View File

@@ -161,8 +161,12 @@ namespace BaseGames.UI
}
// ── 状态响应 ──────────────────────────────────────────────────────────
/// <summary>最近一次的游戏状态。用于背包/地图同键 toggle 时判断"是否在游戏内",避免在主菜单等 UI 上下文误开 Hub。</summary>
private GameStateId _currentState;
private void HandleGameStateChanged(GameStateId state)
{
_currentState = state;
bool showHud = state == GameStates.Gameplay || state == GameStates.BossFight;
if (_hudRoot != null) _hudRoot.SetActive(showHud);
@@ -284,19 +288,20 @@ namespace BaseGames.UI
private void OpenCharmPanel() => OpenPanel(PanelId.CharmPanel);
private void OpenSpellSelect() => OpenPanel(PanelId.SpellSelect);
/// <summary>背包键 toggle若统一背包屏正位于栈顶则关闭否则打开同键开/关。</summary>
/// <summary>背包键 toggle若统一背包屏正位于栈顶则关闭否则(仅游戏内)打开同键开/关。</summary>
private void OpenInventory()
{
if (_panelRegistry.TryGetValue(PanelId.Inventory, out var inv)
&& Navigator?.Top != null && Navigator.Top.gameObject == inv)
Navigator.Pop();
else
OpenPanel(PanelId.Inventory);
if (!_panelRegistry.TryGetValue(PanelId.Inventory, out var inv) || inv == null) return;
if (HubIsTop(inv)) { Navigator.Pop(); return; } // 已开 → 关闭
if (!InGameplay) return; // 仅游戏内允许打开(避免主菜单等误开)
OpenPanel(PanelId.Inventory);
}
/// <summary>
/// 快速直达:确保统一背包屏打开,并定位到指定内容根名对应的 Tab如 "Content_Map")。
/// 已打开则只切 Tab不关闭未打开则打开后切。按名定位与 Tab 顺序解耦
/// 快速直达:定位到指定内容根名对应的 Tab如 "Content_Map")。同键 toggle
/// 已在该 Tab → 关闭 Hub在其他 Tab → 切到该 Tab未打开 → (仅游戏内)打开并切到该 Tab
/// 按名定位,与 Tab 顺序解耦。
/// </summary>
private void OpenInventoryAt(string contentName)
{
@@ -305,11 +310,29 @@ namespace BaseGames.UI
Debug.LogWarning("[UIManager] 统一背包屏PanelId.Inventory未注册快速直达失败。", this);
return;
}
bool hubTop = Navigator?.Top != null && Navigator.Top.gameObject == inv;
if (!hubTop) OpenPanel(PanelId.Inventory); // 激活时 OnEnable 同步运行
inv.GetComponent<Inventory.InventoryHubPanel>()?.SelectTabByContentName(contentName);
var hub = inv.GetComponent<Inventory.InventoryHubPanel>();
if (HubIsTop(inv))
{
// 已显示目标 Tab → 同键关闭;否则切到目标 Tab不关闭
if (hub != null && hub.CurrentContentName == contentName) { Navigator.Pop(); return; }
hub?.SelectTabByContentName(contentName);
return;
}
if (!InGameplay) return; // 仅游戏内允许打开
OpenPanel(PanelId.Inventory); // 激活时 OnEnable 同步运行
hub?.SelectTabByContentName(contentName);
}
/// <summary>统一背包屏当前是否位于导航栈顶(即正在显示)。</summary>
private bool HubIsTop(GameObject inv)
=> Navigator?.Top != null && Navigator.Top.gameObject == inv;
/// <summary>当前是否处于游戏内Gameplay / BossFight——决定是否允许用快捷键打开 Hub。</summary>
private bool InGameplay
=> _currentState == GameStates.Gameplay || _currentState == GameStates.BossFight;
// ── 编辑器工具 ────────────────────────────────────────────────────────
[ContextMenu("验证面板注册表")]
private void EditorValidateRegistry()

View File

@@ -768,6 +768,15 @@ public partial class @PlayerInputActions: IInputActionCollection2, IDisposable
""processors"": """",
""interactions"": """",
""initialStateCheck"": false
},
{
""name"": ""Inventory"",
""type"": ""Button"",
""id"": ""a1b2c3d4-e5f6-0001-0002-000000000201"",
""expectedControlType"": """",
""processors"": """",
""interactions"": """",
""initialStateCheck"": false
}
],
""bindings"": [
@@ -1298,6 +1307,28 @@ public partial class @PlayerInputActions: IInputActionCollection2, IDisposable
""action"": ""QuickMap"",
""isComposite"": false,
""isPartOfComposite"": false
},
{
""name"": """",
""id"": ""a1b2c3d4-e5f6-0001-0002-000000000202"",
""path"": ""<Keyboard>/tab"",
""interactions"": """",
""processors"": """",
""groups"": ""Keyboard&Mouse"",
""action"": ""Inventory"",
""isComposite"": false,
""isPartOfComposite"": false
},
{
""name"": """",
""id"": ""a1b2c3d4-e5f6-0001-0002-000000000203"",
""path"": ""<Gamepad>/select"",
""interactions"": """",
""processors"": """",
""groups"": ""Gamepad"",
""action"": ""Inventory"",
""isComposite"": false,
""isPartOfComposite"": false
}
]
}
@@ -1370,6 +1401,7 @@ public partial class @PlayerInputActions: IInputActionCollection2, IDisposable
m_UI_InventoryTabNext = m_UI.FindAction("InventoryTabNext", throwIfNotFound: true);
m_UI_InventoryTabPrev = m_UI.FindAction("InventoryTabPrev", throwIfNotFound: true);
m_UI_QuickMap = m_UI.FindAction("QuickMap", throwIfNotFound: true);
m_UI_Inventory = m_UI.FindAction("Inventory", throwIfNotFound: true);
}
~@PlayerInputActions()
@@ -1760,6 +1792,7 @@ public partial class @PlayerInputActions: IInputActionCollection2, IDisposable
private readonly InputAction m_UI_InventoryTabNext;
private readonly InputAction m_UI_InventoryTabPrev;
private readonly InputAction m_UI_QuickMap;
private readonly InputAction m_UI_Inventory;
/// <summary>
/// Provides access to input actions defined in input action map "UI".
/// </summary>
@@ -1832,6 +1865,10 @@ public partial class @PlayerInputActions: IInputActionCollection2, IDisposable
/// </summary>
public InputAction @QuickMap => m_Wrapper.m_UI_QuickMap;
/// <summary>
/// Provides access to the underlying input action "UI/Inventory".
/// </summary>
public InputAction @Inventory => m_Wrapper.m_UI_Inventory;
/// <summary>
/// Provides access to the underlying input action map instance.
/// </summary>
public InputActionMap Get() { return m_Wrapper.m_UI; }
@@ -1902,6 +1939,9 @@ public partial class @PlayerInputActions: IInputActionCollection2, IDisposable
@QuickMap.started += instance.OnQuickMap;
@QuickMap.performed += instance.OnQuickMap;
@QuickMap.canceled += instance.OnQuickMap;
@Inventory.started += instance.OnInventory;
@Inventory.performed += instance.OnInventory;
@Inventory.canceled += instance.OnInventory;
}
/// <summary>
@@ -1958,6 +1998,9 @@ public partial class @PlayerInputActions: IInputActionCollection2, IDisposable
@QuickMap.started -= instance.OnQuickMap;
@QuickMap.performed -= instance.OnQuickMap;
@QuickMap.canceled -= instance.OnQuickMap;
@Inventory.started -= instance.OnInventory;
@Inventory.performed -= instance.OnInventory;
@Inventory.canceled -= instance.OnInventory;
}
/// <summary>
@@ -2270,5 +2313,12 @@ public partial class @PlayerInputActions: IInputActionCollection2, IDisposable
/// <seealso cref="UnityEngine.InputSystem.InputAction.performed" />
/// <seealso cref="UnityEngine.InputSystem.InputAction.canceled" />
void OnQuickMap(InputAction.CallbackContext context);
/// <summary>
/// Method invoked when associated input action "Inventory" is either <see cref="UnityEngine.InputSystem.InputAction.started" />, <see cref="UnityEngine.InputSystem.InputAction.performed" /> or <see cref="UnityEngine.InputSystem.InputAction.canceled" />.
/// </summary>
/// <seealso cref="UnityEngine.InputSystem.InputAction.started" />
/// <seealso cref="UnityEngine.InputSystem.InputAction.performed" />
/// <seealso cref="UnityEngine.InputSystem.InputAction.canceled" />
void OnInventory(InputAction.CallbackContext context);
}
}

View File

@@ -682,6 +682,15 @@
"processors": "",
"interactions": "",
"initialStateCheck": false
},
{
"name": "Inventory",
"type": "Button",
"id": "a1b2c3d4-e5f6-0001-0002-000000000201",
"expectedControlType": "",
"processors": "",
"interactions": "",
"initialStateCheck": false
}
],
"bindings": [
@@ -1212,6 +1221,28 @@
"action": "QuickMap",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "",
"id": "a1b2c3d4-e5f6-0001-0002-000000000202",
"path": "<Keyboard>/tab",
"interactions": "",
"processors": "",
"groups": "Keyboard&Mouse",
"action": "Inventory",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "",
"id": "a1b2c3d4-e5f6-0001-0002-000000000203",
"path": "<Gamepad>/select",
"interactions": "",
"processors": "",
"groups": "Gamepad",
"action": "Inventory",
"isComposite": false,
"isPartOfComposite": false
}
]
}