UI 系统
This commit is contained in:
@@ -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);
|
||||
// 复用正式新游戏会话入口:建档并应用 → ISaveable(PlayerStats 等)广播初始数值 →
|
||||
// 粘性事件频道(_replayLastValueToNewSubscribers)留存 → 随后激活的 HUD 订阅时回放,血条/灵珠等正确填充。
|
||||
// 此处玩家已在场景中且已注册 ISaveable,CreateSlot 会立即对其应用空档并广播(见 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()
|
||||
|
||||
@@ -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");
|
||||
|
||||
46
Assets/_Game/Scripts/Core/GameSessionService.cs
Normal file
46
Assets/_Game/Scripts/Core/GameSessionService.cs
Normal 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>
|
||||
/// 不负责场景加载与状态机转换——调用方各自决定(正常流程发 SceneLoadRequest;dev 直连原地切状态)。
|
||||
/// </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);
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Core/GameSessionService.cs.meta
Normal file
11
Assets/_Game/Scripts/Core/GameSessionService.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2b80eda21bea91c448dc1fdece4bc89d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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} 个早注册场景中的 ISaveable(init 时序兜底)。");
|
||||
#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)
|
||||
|
||||
Reference in New Issue
Block a user