This commit is contained in:
SoulliesOfficial
2026-07-24 03:43:11 -04:00
parent 48e7364981
commit fe00ecfcc7
90 changed files with 9610 additions and 461 deletions

View File

@@ -1,4 +1,3 @@
using System;
using System.Collections;
using System.Collections.Generic;
using AK.Wwise;
@@ -8,7 +7,6 @@ using Ichni.Story.UI;
using Ichni.UI;
using Sirenix.OdinInspector;
using SLSUtilities.WwiseAssistance;
using UniRx;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.Serialization;
@@ -34,14 +32,26 @@ namespace Ichni
public partial class MenuManager
{
AsyncOperation asyncOperation;
public bool isEnteringGame;
private const string GameSceneName = "GameScene";
private const float DefaultTransitionDelay = 0.6f;
private Coroutine gameSceneLoadCoroutine;
private AsyncOperation gameSceneLoadOperation;
/// <summary>
/// 当前 MenuScene 是否已经完成 GameScene 预加载且尚未处于切场中
/// TutorialFlowController 在写入“已处理”剧情变量前使用此状态进行最后一次启动前检查
/// 是否已经接受了某个进入游戏的请求
/// 从确认进入开始便保持为 true用于阻止按钮连点和多个入口同时发起场景切换
/// </summary>
public bool CanBeginGame => !isEnteringGame && asyncOperation != null;
public bool isEnteringGame { get; private set; }
/// <summary>
/// 当前是否可以接受新的 GameScene 进入请求。
/// GameScene 改为玩家确认后按需加载,因此不再依赖菜单启动阶段的预加载句柄。
/// </summary>
public bool CanBeginGame =>
!isEnteringGame &&
gameSceneLoadCoroutine == null &&
gameSceneLoadOperation == null;
private void Awake()
{
@@ -88,8 +98,6 @@ namespace Ichni
information?.ClearMenuReturnDestination();
Application.targetFrameRate = SettingsManager.instance.settingsSaveData.targetFrame;
asyncOperation = SceneManager.LoadSceneAsync("GameScene");
asyncOperation.allowSceneActivation = false;
}
}
@@ -97,25 +105,17 @@ namespace Ichni
{
public void EnterGame()
{
// 剧情 SongBlock 仅在玩家确认进入指定目标歌曲后才要求返回剧情;
// 普通选曲与从剧情入口改选其它歌曲都继续返回选曲页。
StorySongEntryContext context = InformationTransistor.instance?.storySongEntryContext;
MenuReturnDestination destination = context != null && context.isActive &&
context.returnToStoryAfterGame
? MenuReturnDestination.Story
: MenuReturnDestination.SongSelection;
EnterGame(destination);
ScheduleGameSceneLoad(ResolveCurrentReturnDestination(), 0f);
}
/// <summary>
/// 激活已预加载 GameScene并写入唯一的菜单返回目标。
/// 按需加载 GameScene并写入唯一的菜单返回目标。
/// 普通选曲使用 <see cref="MenuReturnDestination.SongSelection"/>,教程使用
/// <see cref="MenuReturnDestination.Story"/>。
/// </summary>
public void EnterGame(MenuReturnDestination returnDestination)
{
InformationTransistor.instance?.SetMenuReturnDestination(returnDestination);
EnterGameScene();
ScheduleGameSceneLoad(returnDestination, 0f);
}
/// <summary>
@@ -127,36 +127,138 @@ namespace Ichni
{
if (!CanBeginGame)
{
Debug.LogWarning("[MenuManager] GameScene 未完成预加载或菜单已在切换,忽略启动请求。");
Debug.LogWarning("[MenuManager] 菜单已经接受其它进入请求,忽略本次 GameScene 启动请求。");
return false;
}
if (InformationTransistor.instance == null ||
!InformationTransistor.instance.PrepareGameLaunch(chapter, song, difficulty, returnDestination))
if (InformationTransistor.instance == null || chapter == null || song == null || difficulty == null)
{
return false;
}
isEnteringGame = true;
if (!TryReserveGameEntry())
return false;
if (!InformationTransistor.instance.PrepareGameLaunch(chapter, song, difficulty, returnDestination))
{
CancelGameEntryReservation();
return false;
}
sourcePage?.FadeOut();
transitionUIPage?.FadeIn();
AudioManager.Post(AK.EVENTS.ENTERTOGAME);
Observable.Timer(TimeSpan.FromSeconds(0.6f)).Subscribe(_ => EnterGameScene());
return ScheduleGameSceneLoad(returnDestination, DefaultTransitionDelay);
}
/// <summary>
/// 在进入动画开始前预占本次场景切换。
/// 标准 Play、快速进入和 TutorialBlock 都必须通过这里防止同一帧的重复点击。
/// </summary>
public bool TryReserveGameEntry()
{
if (!CanBeginGame)
return false;
isEnteringGame = true;
return true;
}
public void EnterGameScene()
/// <summary>
/// 为已经完成自定义入场动画的入口安排 GameScene 加载。
/// 延迟使用 realtime确保暂停或修改 timeScale 时仍能正常进入游戏。
/// </summary>
public bool ScheduleCurrentGameSceneLoad(float delay = DefaultTransitionDelay)
{
if (asyncOperation == null)
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.LogError("[MenuManager] GameScene 尚未完成预加载,无法进入游戏。");
isEnteringGame = false;
return;
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();
asyncOperation.allowSceneActivation = true;
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;
}
}
}