2025-06-03 02:42:28 -04:00
|
|
|
|
using System.Collections;
|
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
|
using AK.Wwise;
|
2025-08-11 14:04:06 -04:00
|
|
|
|
using Ichni.Menu;
|
2026-07-18 16:51:18 -04:00
|
|
|
|
using Ichni.Story;
|
2025-06-14 14:42:49 -04:00
|
|
|
|
using Ichni.Story.UI;
|
2025-06-06 10:14:55 -04:00
|
|
|
|
using Ichni.UI;
|
2025-06-03 02:42:28 -04:00
|
|
|
|
using Sirenix.OdinInspector;
|
2026-03-14 03:13:10 -04:00
|
|
|
|
using SLSUtilities.WwiseAssistance;
|
2025-06-03 02:42:28 -04:00
|
|
|
|
using UnityEngine;
|
|
|
|
|
|
using UnityEngine.SceneManagement;
|
2025-06-06 10:14:55 -04:00
|
|
|
|
using UnityEngine.Serialization;
|
2025-06-03 02:42:28 -04:00
|
|
|
|
|
2025-06-06 10:14:55 -04:00
|
|
|
|
namespace Ichni
|
2025-06-03 02:42:28 -04:00
|
|
|
|
{
|
|
|
|
|
|
public partial class MenuManager : MonoBehaviour
|
2025-06-06 10:14:55 -04:00
|
|
|
|
{
|
|
|
|
|
|
public static MenuManager instance;
|
|
|
|
|
|
|
2025-06-14 14:42:49 -04:00
|
|
|
|
public StartUIPage startUIPage;
|
2026-06-15 14:54:30 +08:00
|
|
|
|
public LoginPage loginPage;
|
2025-06-14 14:42:49 -04:00
|
|
|
|
public ChapterSelectionUIPage chapterSelectionUIPage;
|
|
|
|
|
|
public StoryUIPage storyUIPage;
|
2026-07-05 16:08:23 -04:00
|
|
|
|
// dialogUIPage 已随旧对话系统移除;阶段 3 将接入新的 VN 对话页面引用
|
2025-06-14 14:42:49 -04:00
|
|
|
|
public SongSelectionUIPage songSelectionUIPage;
|
2025-08-22 14:54:40 -04:00
|
|
|
|
public TransitionUIPage transitionUIPage;
|
2025-08-11 14:04:06 -04:00
|
|
|
|
public SettingsUIPage settingsUIPage;
|
2025-08-22 14:54:40 -04:00
|
|
|
|
|
|
|
|
|
|
public List<string> languageList;
|
|
|
|
|
|
public List<string> displayLanguageList;
|
2025-06-06 10:14:55 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public partial class MenuManager
|
2025-06-03 02:42:28 -04:00
|
|
|
|
{
|
2026-07-24 03:43:11 -04:00
|
|
|
|
private const string GameSceneName = "GameScene";
|
|
|
|
|
|
private const float DefaultTransitionDelay = 0.6f;
|
|
|
|
|
|
|
|
|
|
|
|
private Coroutine gameSceneLoadCoroutine;
|
|
|
|
|
|
private AsyncOperation gameSceneLoadOperation;
|
2026-07-18 16:51:18 -04:00
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2026-07-24 03:43:11 -04:00
|
|
|
|
/// 是否已经接受了某个进入游戏的请求。
|
|
|
|
|
|
/// 从确认进入开始便保持为 true,用于阻止按钮连点和多个入口同时发起场景切换。
|
2026-07-18 16:51:18 -04:00
|
|
|
|
/// </summary>
|
2026-07-24 03:43:11 -04:00
|
|
|
|
public bool isEnteringGame { get; private set; }
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 当前是否可以接受新的 GameScene 进入请求。
|
|
|
|
|
|
/// GameScene 改为玩家确认后按需加载,因此不再依赖菜单启动阶段的预加载句柄。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public bool CanBeginGame =>
|
|
|
|
|
|
!isEnteringGame &&
|
|
|
|
|
|
gameSceneLoadCoroutine == null &&
|
|
|
|
|
|
gameSceneLoadOperation == null;
|
2025-06-03 02:42:28 -04:00
|
|
|
|
|
|
|
|
|
|
private void Awake()
|
|
|
|
|
|
{
|
2025-06-06 10:14:55 -04:00
|
|
|
|
instance = this;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void Start()
|
|
|
|
|
|
{
|
2025-08-27 21:45:18 -04:00
|
|
|
|
isEnteringGame = false;
|
|
|
|
|
|
|
2026-07-18 16:51:18 -04:00
|
|
|
|
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)
|
2025-08-27 21:45:18 -04:00
|
|
|
|
{
|
|
|
|
|
|
startUIPage.mainCanvasGroup.gameObject.SetActive(false);
|
|
|
|
|
|
ChapterSelectionManager.instance.currentChapter =
|
2026-07-18 16:51:18 -04:00
|
|
|
|
information.chapter;
|
2026-03-14 03:13:10 -04:00
|
|
|
|
AudioManager.SetSwitch(ChapterSelectionManager.instance.currentChapter.chapterSwitch);
|
2026-07-18 16:51:18 -04:00
|
|
|
|
MenuInformationRecorder.instance.songSelectionRecords[
|
|
|
|
|
|
ChapterSelectionManager.instance.currentChapter] =
|
|
|
|
|
|
new SongSelectionRecord(information.song, information.difficulty);
|
2025-08-27 21:45:18 -04:00
|
|
|
|
songSelectionUIPage.FadeIn();
|
|
|
|
|
|
}
|
2026-07-18 16:51:18 -04:00
|
|
|
|
else if (returnDestination == MenuReturnDestination.Story &&
|
|
|
|
|
|
information != null && information.chapter != null)
|
2025-09-06 21:58:48 -04:00
|
|
|
|
{
|
|
|
|
|
|
startUIPage.mainCanvasGroup.gameObject.SetActive(false);
|
2026-07-18 16:51:18 -04:00
|
|
|
|
ChapterSelectionManager.instance.currentChapter = information.chapter;
|
2026-03-14 03:13:10 -04:00
|
|
|
|
AudioManager.SetSwitch(ChapterSelectionManager.instance.currentChapter.chapterSwitch);
|
2026-07-18 16:51:18 -04:00
|
|
|
|
|
|
|
|
|
|
// MenuScene 会在教程游玩后重新创建,必须显式重建章节故事树,
|
|
|
|
|
|
// 不能依赖旧场景中残留的 Block 实例。
|
|
|
|
|
|
StoryManager.instance?.OpenChapter(ChapterSelectionManager.instance.currentChapter.chapterIndex);
|
2025-09-06 21:58:48 -04:00
|
|
|
|
storyUIPage.FadeIn();
|
2026-07-21 15:24:42 -04:00
|
|
|
|
StoryManager.instance?.RestoreStoryAfterGameReturn();
|
2025-09-06 21:58:48 -04:00
|
|
|
|
}
|
2026-07-18 16:51:18 -04:00
|
|
|
|
|
|
|
|
|
|
// 返回目标是一次性运行态信息。无论是否能恢复页面,都不能让它在下次重进菜单时重复生效。
|
|
|
|
|
|
information?.ClearMenuReturnDestination();
|
2025-08-27 21:45:18 -04:00
|
|
|
|
|
2026-07-19 16:44:58 -04:00
|
|
|
|
Application.targetFrameRate = SettingsManager.instance.settingsSaveData.targetFrame;
|
2025-06-03 02:42:28 -04:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-06 10:14:55 -04:00
|
|
|
|
public partial class MenuManager
|
2025-06-03 02:42:28 -04:00
|
|
|
|
{
|
2025-09-06 21:58:48 -04:00
|
|
|
|
public void EnterGame()
|
|
|
|
|
|
{
|
2026-07-24 03:43:11 -04:00
|
|
|
|
ScheduleGameSceneLoad(ResolveCurrentReturnDestination(), 0f);
|
2026-07-18 16:51:18 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2026-07-24 03:43:11 -04:00
|
|
|
|
/// 按需加载 GameScene,并写入唯一的菜单返回目标。
|
2026-07-18 16:51:18 -04:00
|
|
|
|
/// 普通选曲使用 <see cref="MenuReturnDestination.SongSelection"/>,教程使用
|
|
|
|
|
|
/// <see cref="MenuReturnDestination.Story"/>。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public void EnterGame(MenuReturnDestination returnDestination)
|
|
|
|
|
|
{
|
2026-07-24 03:43:11 -04:00
|
|
|
|
ScheduleGameSceneLoad(returnDestination, 0f);
|
2025-09-06 21:58:48 -04:00
|
|
|
|
}
|
2026-07-18 16:51:18 -04:00
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 为 TutorialBlock 等非选曲入口准备上下文并播放通用转场。
|
|
|
|
|
|
/// 运行成功后会返回 true;返回 false 时不会修改剧情变量或存档,调用方应保留原状。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public bool TryBeginGame(ChapterSelectionUnit chapter, SongItemData song,
|
|
|
|
|
|
DifficultyData difficulty, MenuReturnDestination returnDestination, UIPageBase sourcePage)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!CanBeginGame)
|
|
|
|
|
|
{
|
2026-07-24 03:43:11 -04:00
|
|
|
|
Debug.LogWarning("[MenuManager] 菜单已经接受其它进入请求,忽略本次 GameScene 启动请求。");
|
2026-07-18 16:51:18 -04:00
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-24 03:43:11 -04:00
|
|
|
|
if (InformationTransistor.instance == null || chapter == null || song == null || difficulty == null)
|
2026-07-18 16:51:18 -04:00
|
|
|
|
{
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-24 03:43:11 -04:00
|
|
|
|
if (!TryReserveGameEntry())
|
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
|
|
if (!InformationTransistor.instance.PrepareGameLaunch(chapter, song, difficulty, returnDestination))
|
|
|
|
|
|
{
|
|
|
|
|
|
CancelGameEntryReservation();
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-18 16:51:18 -04:00
|
|
|
|
sourcePage?.FadeOut();
|
|
|
|
|
|
transitionUIPage?.FadeIn();
|
|
|
|
|
|
AudioManager.Post(AK.EVENTS.ENTERTOGAME);
|
|
|
|
|
|
|
2026-07-24 03:43:11 -04:00
|
|
|
|
return ScheduleGameSceneLoad(returnDestination, DefaultTransitionDelay);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 在进入动画开始前预占本次场景切换。
|
|
|
|
|
|
/// 标准 Play、快速进入和 TutorialBlock 都必须通过这里防止同一帧的重复点击。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public bool TryReserveGameEntry()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!CanBeginGame)
|
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
|
|
isEnteringGame = true;
|
2026-07-18 16:51:18 -04:00
|
|
|
|
return true;
|
|
|
|
|
|
}
|
2026-07-24 03:43:11 -04:00
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 为已经完成自定义入场动画的入口安排 GameScene 加载。
|
|
|
|
|
|
/// 延迟使用 realtime,确保暂停或修改 timeScale 时仍能正常进入游戏。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public bool ScheduleCurrentGameSceneLoad(float delay = DefaultTransitionDelay)
|
2025-06-03 02:42:28 -04:00
|
|
|
|
{
|
2026-07-24 03:43:11 -04:00
|
|
|
|
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)
|
2026-07-18 16:51:18 -04:00
|
|
|
|
{
|
2026-07-24 03:43:11 -04:00
|
|
|
|
Debug.LogWarning("[MenuManager] GameScene 加载已经安排或正在执行,忽略重复请求。");
|
|
|
|
|
|
return false;
|
2026-07-18 16:51:18 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-24 03:43:11 -04:00
|
|
|
|
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);
|
|
|
|
|
|
|
2026-07-18 16:51:18 -04:00
|
|
|
|
MenuInputManager.instance?.gameInput?.Menu.Disable();
|
2026-07-24 03:43:11 -04:00
|
|
|
|
|
|
|
|
|
|
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;
|
2025-06-03 02:42:28 -04:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-07-18 16:51:18 -04:00
|
|
|
|
}
|