Files
ichni_Official/Assets/Scripts/Menu/MenuManager.cs
SoulliesOfficial fe00ecfcc7 微调
2026-07-24 03:43:11 -04:00

265 lines
10 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<string> languageList;
public List<string> displayLanguageList;
}
public partial class MenuManager
{
private const string GameSceneName = "GameScene";
private const float DefaultTransitionDelay = 0.6f;
private Coroutine gameSceneLoadCoroutine;
private AsyncOperation gameSceneLoadOperation;
/// <summary>
/// 是否已经接受了某个进入游戏的请求。
/// 从确认进入开始便保持为 true用于阻止按钮连点和多个入口同时发起场景切换。
/// </summary>
public bool isEnteringGame { get; private set; }
/// <summary>
/// 当前是否可以接受新的 GameScene 进入请求。
/// GameScene 改为玩家确认后按需加载,因此不再依赖菜单启动阶段的预加载句柄。
/// </summary>
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);
}
/// <summary>
/// 按需加载 GameScene并写入唯一的菜单返回目标。
/// 普通选曲使用 <see cref="MenuReturnDestination.SongSelection"/>,教程使用
/// <see cref="MenuReturnDestination.Story"/>。
/// </summary>
public void EnterGame(MenuReturnDestination returnDestination)
{
ScheduleGameSceneLoad(returnDestination, 0f);
}
/// <summary>
/// 为 TutorialBlock 等非选曲入口准备上下文并播放通用转场。
/// 运行成功后会返回 true返回 false 时不会修改剧情变量或存档,调用方应保留原状。
/// </summary>
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);
}
/// <summary>
/// 在进入动画开始前预占本次场景切换。
/// 标准 Play、快速进入和 TutorialBlock 都必须通过这里防止同一帧的重复点击。
/// </summary>
public bool TryReserveGameEntry()
{
if (!CanBeginGame)
return false;
isEnteringGame = true;
return true;
}
/// <summary>
/// 为已经完成自定义入场动画的入口安排 GameScene 加载。
/// 延迟使用 realtime确保暂停或修改 timeScale 时仍能正常进入游戏。
/// </summary>
public bool ScheduleCurrentGameSceneLoad(float delay = DefaultTransitionDelay)
{
return ScheduleGameSceneLoad(ResolveCurrentReturnDestination(), delay);
}
/// <summary>
/// 在指定转场延迟后启动 GameScene 的异步加载。
/// 这里绝不把 allowSceneActivation 设为 false场景开始加载后会自然完成并自动激活
/// 避免阻塞 Localization、Addressables 和其它 Unity AsyncOperation。
/// </summary>
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;
}
/// <summary>
/// 保留给既有 UnityEvent 和旧调用点的立即加载入口。
/// 新代码通常应使用 <see cref="ScheduleCurrentGameSceneLoad"/>,把转场延迟也交给 MenuManager。
/// </summary>
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;
}
}
}