using System.Collections; using System.Collections.Generic; using AK.Wwise; using Ichni.Menu; using Ichni.Story; using Ichni.Story.UI; using Ichni.UI; using Sirenix.OdinInspector; using SLSUtilities.WwiseAssistance; using UnityEngine; using UnityEngine.SceneManagement; using UnityEngine.Serialization; namespace Ichni { public partial class MenuManager : MonoBehaviour { public static MenuManager instance; public StartUIPage startUIPage; public LoginPage loginPage; public ChapterSelectionUIPage chapterSelectionUIPage; public StoryUIPage storyUIPage; // dialogUIPage 已随旧对话系统移除;阶段 3 将接入新的 VN 对话页面引用 public SongSelectionUIPage songSelectionUIPage; public TransitionUIPage transitionUIPage; public SettingsUIPage settingsUIPage; public List languageList; public List displayLanguageList; } public partial class MenuManager { private const string GameSceneName = "GameScene"; private const float DefaultTransitionDelay = 0.6f; private Coroutine gameSceneLoadCoroutine; private AsyncOperation gameSceneLoadOperation; /// /// 是否已经接受了某个进入游戏的请求。 /// 从确认进入开始便保持为 true,用于阻止按钮连点和多个入口同时发起场景切换。 /// public bool isEnteringGame { get; private set; } /// /// 当前是否可以接受新的 GameScene 进入请求。 /// GameScene 改为玩家确认后按需加载,因此不再依赖菜单启动阶段的预加载句柄。 /// public bool CanBeginGame => !isEnteringGame && gameSceneLoadCoroutine == null && gameSceneLoadOperation == null; private void Awake() { instance = this; } private void Start() { isEnteringGame = false; InformationTransistor information = InformationTransistor.instance; MenuReturnDestination returnDestination = information != null ? information.menuReturnDestination : MenuReturnDestination.None; if (returnDestination == MenuReturnDestination.SongSelection && information != null && information.chapter != null && information.song != null && information.difficulty != null) { startUIPage.mainCanvasGroup.gameObject.SetActive(false); ChapterSelectionManager.instance.currentChapter = information.chapter; AudioManager.SetSwitch(ChapterSelectionManager.instance.currentChapter.chapterSwitch); MenuInformationRecorder.instance.songSelectionRecords[ ChapterSelectionManager.instance.currentChapter] = new SongSelectionRecord(information.song, information.difficulty); songSelectionUIPage.FadeIn(); } else if (returnDestination == MenuReturnDestination.Story && information != null && information.chapter != null) { startUIPage.mainCanvasGroup.gameObject.SetActive(false); ChapterSelectionManager.instance.currentChapter = information.chapter; AudioManager.SetSwitch(ChapterSelectionManager.instance.currentChapter.chapterSwitch); // MenuScene 会在教程游玩后重新创建,必须显式重建章节故事树, // 不能依赖旧场景中残留的 Block 实例。 StoryManager.instance?.OpenChapter(ChapterSelectionManager.instance.currentChapter.chapterIndex); storyUIPage.FadeIn(); StoryManager.instance?.RestoreStoryAfterGameReturn(); } // 返回目标是一次性运行态信息。无论是否能恢复页面,都不能让它在下次重进菜单时重复生效。 information?.ClearMenuReturnDestination(); Application.targetFrameRate = SettingsManager.instance.settingsSaveData.targetFrame; } } public partial class MenuManager { public void EnterGame() { ScheduleGameSceneLoad(ResolveCurrentReturnDestination(), 0f); } /// /// 按需加载 GameScene,并写入唯一的菜单返回目标。 /// 普通选曲使用 ,教程使用 /// 。 /// public void EnterGame(MenuReturnDestination returnDestination) { ScheduleGameSceneLoad(returnDestination, 0f); } /// /// 为 TutorialBlock 等非选曲入口准备上下文并播放通用转场。 /// 运行成功后会返回 true;返回 false 时不会修改剧情变量或存档,调用方应保留原状。 /// public bool TryBeginGame(ChapterSelectionUnit chapter, SongItemData song, DifficultyData difficulty, MenuReturnDestination returnDestination, UIPageBase sourcePage) { if (!CanBeginGame) { Debug.LogWarning("[MenuManager] 菜单已经接受其它进入请求,忽略本次 GameScene 启动请求。"); return false; } if (InformationTransistor.instance == null || chapter == null || song == null || difficulty == null) { return false; } if (!TryReserveGameEntry()) return false; if (!InformationTransistor.instance.PrepareGameLaunch(chapter, song, difficulty, returnDestination)) { CancelGameEntryReservation(); return false; } sourcePage?.FadeOut(); transitionUIPage?.FadeIn(); AudioManager.Post(AK.EVENTS.ENTERTOGAME); return ScheduleGameSceneLoad(returnDestination, DefaultTransitionDelay); } /// /// 在进入动画开始前预占本次场景切换。 /// 标准 Play、快速进入和 TutorialBlock 都必须通过这里防止同一帧的重复点击。 /// public bool TryReserveGameEntry() { if (!CanBeginGame) return false; isEnteringGame = true; return true; } /// /// 为已经完成自定义入场动画的入口安排 GameScene 加载。 /// 延迟使用 realtime,确保暂停或修改 timeScale 时仍能正常进入游戏。 /// public bool ScheduleCurrentGameSceneLoad(float delay = DefaultTransitionDelay) { return ScheduleGameSceneLoad(ResolveCurrentReturnDestination(), delay); } /// /// 在指定转场延迟后启动 GameScene 的异步加载。 /// 这里绝不把 allowSceneActivation 设为 false:场景开始加载后会自然完成并自动激活, /// 避免阻塞 Localization、Addressables 和其它 Unity AsyncOperation。 /// public bool ScheduleGameSceneLoad(MenuReturnDestination returnDestination, float delay) { if (gameSceneLoadCoroutine != null || gameSceneLoadOperation != null) { Debug.LogWarning("[MenuManager] GameScene 加载已经安排或正在执行,忽略重复请求。"); return false; } if (!isEnteringGame && !TryReserveGameEntry()) return false; InformationTransistor.instance?.SetMenuReturnDestination(returnDestination); gameSceneLoadCoroutine = StartCoroutine( LoadGameSceneAfterTransition(Mathf.Max(0f, delay))); return true; } /// /// 保留给既有 UnityEvent 和旧调用点的立即加载入口。 /// 新代码通常应使用 ,把转场延迟也交给 MenuManager。 /// public void EnterGameScene() { ScheduleCurrentGameSceneLoad(0f); } private IEnumerator LoadGameSceneAfterTransition(float delay) { if (delay > 0f) yield return new WaitForSecondsRealtime(delay); MenuInputManager.instance?.gameInput?.Menu.Disable(); try { gameSceneLoadOperation = SceneManager.LoadSceneAsync( GameSceneName, LoadSceneMode.Single); } catch (System.Exception exception) { Debug.LogException(exception, this); HandleGameSceneLoadFailure("创建 GameScene 异步加载操作时发生异常。"); yield break; } if (gameSceneLoadOperation == null) { HandleGameSceneLoadFailure( $"SceneManager.LoadSceneAsync 未能创建场景 '{GameSceneName}' 的加载操作。"); yield break; } // 不设置 allowSceneActivation=false。长时间挂起场景激活会冻结后续 AsyncOperation, // 导致 Addressables String Table、AssetBundle 等资源永久停留在 0%。 while (!gameSceneLoadOperation.isDone) yield return null; } private MenuReturnDestination ResolveCurrentReturnDestination() { // 剧情 SongBlock 仅在玩家确认进入指定目标歌曲后才要求返回剧情; // 普通选曲与从剧情入口改选其它歌曲都继续返回选曲页。 StorySongEntryContext context = InformationTransistor.instance?.storySongEntryContext; return context != null && context.isActive && context.returnToStoryAfterGame ? MenuReturnDestination.Story : MenuReturnDestination.SongSelection; } private void HandleGameSceneLoadFailure(string reason) { Debug.LogError($"[MenuManager] {reason}", this); gameSceneLoadCoroutine = null; gameSceneLoadOperation = null; CancelGameEntryReservation(); MenuInputManager.instance?.gameInput?.Menu.Enable(); } private void CancelGameEntryReservation() { isEnteringGame = false; } } }