Files
aibis-dream/Assets/Scripts/Game Loop/GameManager.cs
T
dingyuntian 0a1adf1558 fix: 部分QA问题处理
1. 返回主菜单闪烁
2. 对话隐藏
3. 历史记录宽度调整
2026-07-26 16:36:51 +08:00

775 lines
24 KiB
C#

using System;
using System.Collections;
using System.Collections.Generic;
using AibisDream.FixSystem;
using AibisDream.Framework;
using AibisDream.Kit;
using AibisDream.SaveSystem;
using AibisDream.UI;
using UnityEngine;
using UnityEngine.EventSystems;
namespace AibisDream
{
public sealed class SessionFailure
{
public string Command { get; internal set; }
public string Stage { get; internal set; }
public IReadOnlyList<string> Errors { get; internal set; } = Array.Empty<string>();
}
public class GameManager : Singleton<GameManager>
{
private enum SessionCommandKind
{
None,
Start,
Transition,
Restore,
ReturnToMenu
}
private const float TransitionDuration = 0.5f;
private const float DialogueHandoffTimeout = 5f;
[Header("生产环境数据(一般不动)")]
[SerializeField] private TalkSceneSO firstTalkSo;
private GameSession _session;
private TalkSceneGraphIndex _talkSceneIndex;
private SessionCommandKind _activeCommand;
private bool _returnToMenuRequested;
public static GameSession Session =>
Instance != null && Instance._session != null
? Instance._session
: throw new InvalidOperationException(
"GameManager.Session accessed before GameManager initialization.");
public IReadOnlyList<TalkSceneSO> RuntimeChapters => _talkSceneIndex.RuntimeChapters;
public override void OnSingletonInit()
{
_session = new GameSession();
_talkSceneIndex = new TalkSceneGraphIndex(firstTalkSo);
foreach (var error in _talkSceneIndex.ValidationErrors)
{
Debug.LogError($"[GameManager] {error}");
}
}
private void Start()
{
if (DialogController.Instance != null)
{
DialogController.Instance.DialogueActivityChanged += OnDialogueActivityChanged;
}
}
public override void OnSingletonDestroy()
{
if (DialogController.Instance != null)
{
DialogController.Instance.DialogueActivityChanged -= OnDialogueActivityChanged;
}
}
public bool TryStartNewGame()
{
return TryStartChapter(firstTalkSo);
}
public bool TryStartChapter(TalkSceneSO chapter)
{
if (!ValidateChapter(chapter, out var error))
{
Debug.LogError($"[GameManager] Cannot start chapter: {error}");
return false;
}
if (!TryAcquireCommand(SessionCommandKind.Start))
{
return false;
}
StartCoroutine(StartChapterRoutine(chapter));
return true;
}
public bool TryRestoreSlot(int slotIndex, Action<RestoreResult> completed = null)
{
if (!CanBeginRestore() || !TryAcquireCommand(SessionCommandKind.Restore))
{
return false;
}
StartCoroutine(RestoreRoutine(
() => SaveRestoreOrchestrator.PrepareRestoreFromSlot(
slotIndex,
_talkSceneIndex,
RestoreOptions.Default),
completed));
return true;
}
#if UNITY_EDITOR || DEVELOPMENT_BUILD
public bool TryRestoreTestSave(TestSaveEntry entry, Action<RestoreResult> completed = null)
{
if (entry == null
|| !entry.IsValid
|| !CanBeginRestore()
|| !TryAcquireCommand(SessionCommandKind.Restore))
{
return false;
}
StartCoroutine(RestoreRoutine(
() => SaveRestoreOrchestrator.PrepareRestoreFromTestSave(
entry,
_talkSceneIndex,
RestoreOptions.Default),
completed));
return true;
}
#endif
public bool TryPause()
{
if (!_session.CanPause)
{
return false;
}
_session.SetPaused(true);
Time.timeScale = 0f;
EnumEventSystem.Global.Send(GameLifecycleEvent.SessionPaused);
return true;
}
public bool TryResume()
{
if (!_session.CanResume)
{
return false;
}
ResumeSessionInternal(sendEvent: true);
return true;
}
public bool TryReturnToMainMenu()
{
if (_session.Phase is GameSessionPhase.MainMenu or GameSessionPhase.ReturningToMenu)
{
return false;
}
if (_activeCommand != SessionCommandKind.None)
{
_returnToMenuRequested = true;
return true;
}
if (!TryAcquireCommand(SessionCommandKind.ReturnToMenu))
{
return false;
}
StartCoroutine(ReturnToMainMenuRoutine());
return true;
}
public void QuitApplication()
{
Application.Quit();
}
internal bool TryAdvanceChapterFromYarn(string exitName)
{
var target = _session.CurrentTalkScene?.GetNextScene(exitName);
if (!ValidateChapter(target, out var error))
{
Debug.LogError($"[GameManager] Invalid chapter exit '{exitName ?? "default"}': {error}");
return false;
}
if (_session.Phase != GameSessionPhase.Playing
|| !TryAcquireCommand(SessionCommandKind.Transition))
{
Debug.LogWarning("[GameManager] Chapter transition rejected because another command is active.");
return false;
}
StartCoroutine(AdvanceChapterCommandRoutine(target));
return true;
}
private IEnumerator AdvanceChapterCommandRoutine(TalkSceneSO target)
{
try
{
yield return TransitionChapterRoutine(target);
}
finally
{
FinishCommand(SessionCommandKind.Transition);
}
}
internal IEnumerator LoadSceneFromYarn(string sceneName)
{
if (_session.Phase != GameSessionPhase.Playing
|| !TryAcquireCommand(SessionCommandKind.Transition))
{
Debug.LogWarning("[GameManager] Yarn scene load rejected because another command is active.");
yield break;
}
try
{
DialogUIManager.Instance?.HideDialog();
if (!TrySetPhase(GameSessionPhase.Transitioning))
{
yield break;
}
SceneOperationResult sceneResult = null;
yield return SceneLoader.Instance.LoadSceneAsync(sceneName, value => sceneResult = value);
if (!EnsureSceneSucceeded(sceneResult, "Yarn scene load"))
{
yield break;
}
SceneReadinessResult readiness = null;
yield return SceneReadiness.WaitUntilReady(
SceneLoader.Instance.CurrentScene,
IsReturnRequested,
value => readiness = value);
if (!EnsureReadinessSucceeded(readiness, "Yarn scene readiness"))
{
yield break;
}
if (_returnToMenuRequested)
{
yield break;
}
TrySetPhase(GameSessionPhase.Playing);
EnumEventSystem.Global.Send(GameLifecycleEvent.SessionReady);
}
finally
{
FinishCommand(SessionCommandKind.Transition);
}
}
internal IEnumerator UnloadSceneFromYarn()
{
if (_session.Phase != GameSessionPhase.Playing
|| !TryAcquireCommand(SessionCommandKind.Transition))
{
Debug.LogWarning("[GameManager] Yarn scene unload rejected because another command is active.");
yield break;
}
try
{
if (!TrySetPhase(GameSessionPhase.Transitioning))
{
yield break;
}
SceneOperationResult result = null;
yield return SceneLoader.Instance.UnloadSceneAsync(value => result = value);
if (!EnsureSceneSucceeded(result, "Yarn scene unload"))
{
yield break;
}
if (_returnToMenuRequested)
{
yield break;
}
TrySetPhase(GameSessionPhase.Playing);
EnumEventSystem.Global.Send(GameLifecycleEvent.SessionReady);
}
finally
{
FinishCommand(SessionCommandKind.Transition);
}
}
private IEnumerator StartChapterRoutine(TalkSceneSO chapter)
{
try
{
// 设置当前状态
_session.ResetRuntimeState();
if (!TrySetPhase(GameSessionPhase.Starting))
{
yield break;
}
EnumEventSystem.Global.Send(GameLifecycleEvent.SessionStarted);
yield return FadeIn();
if (_returnToMenuRequested)
{
yield break;
}
SceneOperationResult sceneResult = null;
yield return SceneLoader.Instance.LoadSceneAsync(
chapter.firstSceneName,
value => sceneResult = value);
if (!EnsureSceneSucceeded(sceneResult, "Start scene"))
{
yield break;
}
SceneReadinessResult readiness = null;
yield return SceneReadiness.WaitUntilReady(
SceneLoader.Instance.CurrentScene,
IsReturnRequested,
value => readiness = value);
if (!EnsureReadinessSucceeded(readiness, "Start readiness"))
{
yield break;
}
if (_returnToMenuRequested)
{
yield break;
}
CommitChapter(chapter);
DialogController.Instance.StartDialog(chapter.yarnProject);
TrySetPhase(GameSessionPhase.Playing);
EnumEventSystem.Global.Send(GameLifecycleEvent.SessionReady);
// 让Yarn自己处理淡出
// yield return FadeOut();
}
finally
{
FinishCommand(SessionCommandKind.Start);
}
}
private IEnumerator TransitionChapterRoutine(TalkSceneSO target)
{
if (!TrySetPhase(GameSessionPhase.Transitioning))
{
yield break;
}
yield return FadeIn();
if (_returnToMenuRequested)
{
yield break;
}
var sourceDialogueCompleted = false;
yield return WaitForSourceDialogueCompletion(
value => sourceDialogueCompleted = value);
if (_returnToMenuRequested)
{
yield break;
}
if (!sourceDialogueCompleted)
{
SendFailure(
"Transition",
"SourceDialogueCompletion",
new[]
{
$"Source dialogue did not complete within {DialogueHandoffTimeout:0.#} seconds. "
+ "NextYarn must be the final executable command on its branch."
});
_returnToMenuRequested = true;
yield break;
}
SceneOperationResult sceneResult = null;
yield return SceneLoader.Instance.LoadSceneAsync(
target.firstSceneName,
value => sceneResult = value);
if (!EnsureSceneSucceeded(sceneResult, "Chapter scene"))
{
yield break;
}
SceneReadinessResult readiness = null;
yield return SceneReadiness.WaitUntilReady(
SceneLoader.Instance.CurrentScene,
IsReturnRequested,
value => readiness = value);
if (!EnsureReadinessSucceeded(readiness, "Chapter readiness"))
{
yield break;
}
if (_returnToMenuRequested)
{
yield break;
}
CommitChapter(target);
DialogController.Instance.StartDialog(target.yarnProject);
TrySetPhase(GameSessionPhase.Playing);
EnumEventSystem.Global.Send(GameLifecycleEvent.SessionReady);
// 让Yarn自己处理淡出
// yield return FadeOut();
}
private IEnumerator WaitForSourceDialogueCompletion(Action<bool> completed)
{
var runner = DialogController.Instance?.DialogueRunner;
if (runner == null)
{
completed?.Invoke(false);
yield break;
}
var deadline = Time.unscaledTime + DialogueHandoffTimeout;
while (runner.IsDialogueRunning && Time.unscaledTime < deadline)
{
if (_returnToMenuRequested)
{
completed?.Invoke(false);
yield break;
}
yield return null;
}
completed?.Invoke(!runner.IsDialogueRunning);
}
private IEnumerator RestoreRoutine(
Func<RestorePreparation> prepare,
Action<RestoreResult> completed)
{
var originPhase = _session.Phase;
RestoreResult result = null;
try
{
if (!TrySetPhase(GameSessionPhase.Restoring))
{
yield break;
}
var preparation = prepare();
result = preparation.Result;
if (!preparation.Success)
{
SendFailure("Restore", result.FailedPhase, result.Errors);
TrySetPhase(originPhase);
yield break;
}
EnumEventSystem.Global.Send(GameLifecycleEvent.SessionStarted);
ResumeSessionInternal(sendEvent: _session.IsPaused);
yield return FadeIn();
if (_returnToMenuRequested)
{
result.Cancelled = true;
yield break;
}
if (DialogController.Instance != null)
{
yield return DialogController.Instance.StopDialogRoutine();
}
if (_returnToMenuRequested)
{
result.Cancelled = true;
yield break;
}
ResetPresentationEffects();
yield return SaveRestoreOrchestrator.ExecutePreparedRestore(
preparation,
IsReturnRequested);
result = preparation.Result;
if (result.Cancelled || _returnToMenuRequested)
{
_returnToMenuRequested = true;
yield break;
}
if (!result.Success)
{
SendFailure("Restore", result.FailedPhase, result.Errors);
_returnToMenuRequested = true;
yield break;
}
CommitChapter(result.RestoredTalkScene);
TrySetPhase(GameSessionPhase.Playing);
EnumEventSystem.Global.Send(GameLifecycleEvent.SessionReady);
ScreenSnapshotHelper.ApplyDeferredFadeScreenIfNeeded();
// yield return FadeOut();
}
finally
{
completed?.Invoke(result ?? RestoreResult.CreateFailure(
"Restore",
"Restore did not produce a result."));
FinishCommand(SessionCommandKind.Restore);
}
}
private IEnumerator ReturnToMainMenuRoutine()
{
try
{
_returnToMenuRequested = false;
if (!TrySetPhase(GameSessionPhase.ReturningToMenu))
{
yield break;
}
EnumEventSystem.Global.Send(GameLifecycleEvent.SessionEnding);
if (DialogController.Instance != null)
{
yield return DialogController.Instance.StopDialogRoutine();
}
// The in-game terminal remains visible while this transition
// covers the gameplay behind it.
yield return FadeIn();
UIManager.Instance.GetPanel<InGameTerminalPanel>()?.CloseForSessionEnd();
// The showcase belongs to the loaded gameplay scene, so clear it
// while that scene is still alive and hidden behind the fade.
ResetPresentationEffects();
SceneOperationResult unloadResult = null;
const int maxUnloadAttempts = 2;
for (var attempt = 1; attempt <= maxUnloadAttempts; attempt++)
{
yield return SceneLoader.Instance.UnloadSceneAsync(value => unloadResult = value);
if (unloadResult?.Success == true)
{
break;
}
if (attempt < maxUnloadAttempts)
{
yield return null;
}
}
if (unloadResult?.Success != true)
{
SendFailure(
"ReturnToMenu",
"UnloadScene",
new[] { unloadResult?.Error ?? "Scene unload did not return a result." });
yield break;
}
UIManager.Instance.HidePanel<EndPanel>();
yield return LocalizationKit.WaitUntilReady();
ResumeSessionInternal(sendEvent: _session.IsPaused);
_session.ResetRuntimeState();
TrySetPhase(GameSessionPhase.MainMenu);
EnumEventSystem.Global.Send(GameLifecycleEvent.SessionEnded);
yield return LocalizationKit.WaitForUIRefresh();
yield return FadeOut();
EventSystem.current?.SetSelectedGameObject(null);
}
finally
{
FinishCommand(SessionCommandKind.ReturnToMenu);
}
}
private bool CanBeginRestore()
{
return _activeCommand == SessionCommandKind.None
&& _session.Phase is GameSessionPhase.MainMenu or GameSessionPhase.Playing;
}
private bool TryAcquireCommand(SessionCommandKind command)
{
if (_activeCommand != SessionCommandKind.None)
{
return false;
}
_activeCommand = command;
return true;
}
private void FinishCommand(SessionCommandKind command)
{
if (_activeCommand == command)
{
_activeCommand = SessionCommandKind.None;
}
if (!_returnToMenuRequested
|| _activeCommand != SessionCommandKind.None
|| _session.Phase is GameSessionPhase.MainMenu or GameSessionPhase.ReturningToMenu)
{
return;
}
if (TryAcquireCommand(SessionCommandKind.ReturnToMenu))
{
StartCoroutine(ReturnToMainMenuRoutine());
}
}
private bool TrySetPhase(GameSessionPhase phase)
{
if (_session.TryTransitionTo(phase, out var error))
{
return true;
}
Debug.LogError($"[GameManager] {error}");
return false;
}
private void CommitChapter(TalkSceneSO chapter)
{
_session.SetCurrentTalkScene(chapter);
ChapterController.Instance?.UnlockChapter(chapter);
}
private bool EnsureSceneSucceeded(SceneOperationResult result, string stage)
{
if (result?.Success == true)
{
return true;
}
SendFailure("Scene", stage, new[] { result?.Error ?? "Scene operation did not return a result." });
_returnToMenuRequested = true;
return false;
}
private bool EnsureReadinessSucceeded(SceneReadinessResult result, string stage)
{
if (result?.Success == true)
{
return true;
}
if (result?.Cancelled == true)
{
_returnToMenuRequested = true;
return false;
}
SendFailure("SceneReadiness", stage, new[]
{
result?.Error ?? "Scene readiness did not return a result."
});
_returnToMenuRequested = true;
return false;
}
private void SendFailure(string command, string stage, IReadOnlyList<string> errors)
{
var failure = new SessionFailure
{
Command = command,
Stage = stage,
Errors = errors ?? Array.Empty<string>()
};
Debug.LogError($"[GameManager] {command} failed at {stage}: {string.Join("; ", failure.Errors)}");
EnumEventSystem.Global.Send<GameLifecycleEvent, SessionFailure>(
GameLifecycleEvent.SessionFailed,
failure);
}
private void ResumeSessionInternal(bool sendEvent)
{
var wasPaused = _session.IsPaused;
Time.timeScale = 1f;
_session.SetPaused(false);
if (sendEvent && wasPaused)
{
EnumEventSystem.Global.Send(GameLifecycleEvent.SessionResumed);
}
}
private void ResetPresentationEffects()
{
ScreenEffectManager.Instance?.ResetSaturation();
UIManager.Instance.GetPanel<PlayToolPanel>()?.ClearPresentationEffects();
Day2SleepPresentationController.Instance?.ClearPresentationEffects();
SpriteShowcase.Instance?.ClearPresentationEffects();
CameraKit.Instance?.ResetCamera();
}
private IEnumerator FadeIn()
{
var transitionPanel = UIManager.Instance.GetPanel<ScreenTransitionPanel>();
if (transitionPanel != null)
{
yield return transitionPanel.FadeInAsync(TransitionDuration, useUnscaledTime: true);
}
}
private IEnumerator FadeOut()
{
var transitionPanel = UIManager.Instance.GetPanel<ScreenTransitionPanel>();
if (transitionPanel != null)
{
yield return transitionPanel.FadeOutAsync(TransitionDuration, useUnscaledTime: true);
}
}
private void OnDialogueActivityChanged(bool active)
{
_session.SetDialogueActive(active);
}
private bool IsReturnRequested()
{
return _returnToMenuRequested;
}
private static bool ValidateChapter(TalkSceneSO chapter, out string error)
{
if (chapter == null)
{
error = "TalkSceneSO is null.";
return false;
}
if (string.IsNullOrWhiteSpace(chapter.firstSceneName))
{
error = $"TalkSceneSO {chapter.name} has an empty scene key.";
return false;
}
if (chapter.yarnProject == null)
{
error = $"TalkSceneSO {chapter.name} has no YarnProject.";
return false;
}
error = null;
return true;
}
}
}