using UnityEngine; using System; using System.Collections; using UnityEngine.SceneManagement; using UnityEngine.UI; using GameServer.Client; using Bansonic; #if UNITY_EDITOR using UnityEditor; #endif /// /// Central gameplay pause manager. /// - ESC / top-right Pause button can pause only in gameplay scene. /// - Continue: hide pause panel, replay 3-2-1-Go, then resume chart + music. /// - Replay: reload current gameplay scene (with current selected song/difficulty if available). /// - Exit: return to selectYourSongFirst. /// public class PauseManager : MonoBehaviour { public static PauseManager Instance { get; private set; } [Header("Overlay")] public GameObject overlayRoot; [SerializeField] private float overlayFadeDuration = 0.2f; [SerializeField] private bool preferPausePrefabOverWhiteScreen = true; [SerializeField] private string legacyWhiteScreenRootName = "pauseWhiteScreen"; [Header("Gameplay Scope")] [SerializeField] private bool onlyInGameplayScene = true; [SerializeField] private string gameplaySceneName = "gamePlay_gamePlay"; [SerializeField] private bool requirePlaybackStarted = true; [Header("Buttons")] [SerializeField] private string pauseButtonScenePath = "artworks/NEW UI/zhuangshi (3)/Pause"; [Tooltip("手游暂停按钮:把场景里的暂停 Button 拖到这里,点击效果等同 ESC。\n" + "留空则按 pauseButtonScenePath 或名为 \"Pause\" 的按钮自动查找。")] [SerializeField] private Button pauseTriggerButton; [SerializeField] private Button continueButton; [SerializeField] private Button replayButton; [SerializeField] private Button exitButton; [SerializeField] private Button settingsButton; [Header("Settings Prefab")] [SerializeField] private GameObject settingsPrefab; [Header("Pause Entry Animation")] [SerializeField] private bool playPauseEntryAnimation = true; [SerializeField] private float pauseItemEnterDuration = 0.18f; [SerializeField] private float pauseItemEnterStagger = 0.016f; public bool IsPaused { get; private set; } = false; public event Action OnPauseStateChanged; private CanvasGroup overlayCanvasGroup; private RectTransform overlayPanelRoot; private CanvasGroup overlayPanelCanvasGroup; private Vector3 panelShownScale = Vector3.zero; private static readonly Vector3 PanelHiddenScale = Vector3.zero; private Coroutine overlayFadeCoroutine; private Coroutine continueCoroutine; private bool buttonsBound = false; private float nextBindRetryTime = 0f; private const string ExitSceneName = "selectYourSongFirst"; void Awake() { if (Instance == null) Instance = this; else { Destroy(gameObject); return; } } void Start() { ResolveOverlayRootForPausePrefab(); EnsureOverlayCanvasGroup(); EnsureOverlayPanelRoot(); // Ensure CanvasGroup is initialized to 0 alpha, but keep interactable/blocksRaycasts true if (overlayCanvasGroup != null) { overlayCanvasGroup.alpha = 0f; overlayCanvasGroup.interactable = true; overlayCanvasGroup.blocksRaycasts = true; } // Initialize buttons but DO NOT call SetOverlayVisibleImmediate(false) here // because it would reset the flags we just set. // Instead, we manually ensure the object is active if that's the desired initial state, // or just rely on the alpha=0 to hide it. // Assuming user wants the object active but transparent: if (overlayRoot != null) overlayRoot.SetActive(true); TryBindButtons(force: true); } void Update() { if (!buttonsBound && Time.unscaledTime >= nextBindRetryTime) { TryBindButtons(force: false); } if (Input.GetKeyDown(KeyCode.Delete)) { #if UNITY_EDITOR EditorApplication.isPlaying = false; #else Application.Quit(); #endif return; } if (Input.GetKeyDown(KeyCode.Escape)) { // User requested ESC as pause trigger in gameplay only. if (CanPauseFromInput()) { if (!IsPaused) { Pause(true); } } } // Keep existing Backspace quick-exit while paused. if (IsPaused && Input.GetKeyDown(KeyCode.Backspace)) { ResumeImmediately(true); StartCoroutine(LoadSceneAsync("Main_main")); } } public void Pause(bool pause) { if (pause) { if (IsPaused) return; IsPaused = true; Time.timeScale = 0f; GameplayClock.Pause(); OnPauseStateChanged?.Invoke(true); // Startup flows call Pause(true) before real gameplay starts. // In that phase we keep time paused but do not show pause UI. // Calculate allowOverlay WITHOUT checking IsPaused (since we just set it to true). bool allowOverlay = true; if (onlyInGameplayScene && !IsGameplaySceneActive()) allowOverlay = false; else if (requirePlaybackStarted) { var gm = SceneObjectLookupCache.FindAny(); // If GM missing or playback not started, don't show overlay yet if (gm != null && !gm.PlaybackStarted) allowOverlay = false; } if (allowOverlay) { ShowOverlayAnimated(true); } else { // Startup Pause State: // Keep object active and blocking raycasts (per user request), but invisible. if (overlayRoot != null) overlayRoot.SetActive(true); if (overlayCanvasGroup != null) { overlayCanvasGroup.alpha = 0f; overlayCanvasGroup.interactable = true; overlayCanvasGroup.blocksRaycasts = true; } if (overlayPanelCanvasGroup != null) { overlayPanelCanvasGroup.alpha = 0f; overlayPanelCanvasGroup.interactable = true; overlayPanelCanvasGroup.blocksRaycasts = true; } if (overlayPanelRoot != null) overlayPanelRoot.localScale = PanelHiddenScale; } } else { if (!IsPaused) return; IsPaused = false; Time.timeScale = 1f; GameplayClock.Resume(); // Re-anchor clock to actual music progress to correct any drift during pause var gm = SceneObjectLookupCache.FindAny(); if (gm != null && gm.musicSource != null && gm.musicSource.isPlaying) { GameplayClock.ReanchorToMusicTime(gm.musicSource.time); } OnPauseStateChanged?.Invoke(false); ShowOverlayAnimated(false); } } private void ResumeImmediately(bool hideOverlay) { if (!IsPaused) { if (hideOverlay) { SetOverlayVisibleImmediate(false); } return; } IsPaused = false; Time.timeScale = 1f; GameplayClock.Resume(); // Re-anchor clock to actual music progress to correct any drift during pause var gm = SceneObjectLookupCache.FindAny(); if (gm != null && gm.musicSource != null && gm.musicSource.isPlaying) { GameplayClock.ReanchorToMusicTime(gm.musicSource.time); } OnPauseStateChanged?.Invoke(false); if (hideOverlay) { SetOverlayVisibleImmediate(false); } else { ShowOverlayAnimated(false); } } public void TogglePause() { if (!CanPauseFromInput()) return; if (!IsPaused) Pause(true); } public void ContinueFromPause() { if (!IsPaused) return; if (continueCoroutine != null) return; continueCoroutine = StartCoroutine(ContinueFromPauseCoroutine()); } public void ReplayFromPause() { if (!IsPaused) return; if (continueCoroutine != null) { StopCoroutine(continueCoroutine); continueCoroutine = null; } StartCoroutine(ReplayFromPauseCoroutine()); } public void ExitFromPause() { if (!IsPaused) return; if (continueCoroutine != null) { StopCoroutine(continueCoroutine); continueCoroutine = null; } StartCoroutine(ExitFromPauseCoroutine()); } private bool IsGameplaySceneActive() { string sceneName = SceneManager.GetActiveScene().name ?? string.Empty; if (string.Equals(sceneName, gameplaySceneName, StringComparison.OrdinalIgnoreCase)) return true; if (sceneName.IndexOf("gameplay", StringComparison.OrdinalIgnoreCase) >= 0) return true; return false; } private bool CanPauseFromInput() { if (onlyInGameplayScene && !IsGameplaySceneActive()) return false; if (!requirePlaybackStarted) return true; var gm = SceneObjectLookupCache.FindAny(); if (gm == null) return false; // If already paused, allow input to unpause regardless of PlaybackStarted state if (IsPaused) return true; return gm.PlaybackStarted; } private void EnsureOverlayCanvasGroup() { if (overlayRoot == null) return; // Priority: Use existing CanvasGroup if available (Index first) if (!overlayRoot.TryGetComponent(out overlayCanvasGroup)) { // Fallback: Create only if not found overlayCanvasGroup = overlayRoot.AddComponent(); } } private void ResolveOverlayRootForPausePrefab() { // Priority: If manually assigned in Inspector, respect it. if (overlayRoot != null) return; if (!preferPausePrefabOverWhiteScreen) return; // Prefer the real pause prefab root (contains Continue/Replay/Exit). GameObject pauseRoot = FindPauseMenuRootInScene(); if (pauseRoot != null) { overlayRoot = pauseRoot; return; } if (overlayRoot == null) return; // Legacy setup fallback: PauseManager points to pauseWhiteScreen. if (!string.Equals(overlayRoot.name, legacyWhiteScreenRootName, StringComparison.OrdinalIgnoreCase)) return; GameObject candidate = FindPauseOverlayCandidate(overlayRoot.transform); if (candidate == null) candidate = FindPauseOverlayCandidate(null); if (candidate != null && candidate != overlayRoot) overlayRoot = candidate; } private GameObject FindPauseOverlayCandidate(Transform searchRoot) { Canvas[] canvases = searchRoot != null ? searchRoot.GetComponentsInChildren(true) : FindObjectsByType(FindObjectsInactive.Include, FindObjectsSortMode.None); for (int i = 0; i < canvases.Length; i++) { Canvas canvas = canvases[i]; if (canvas == null) continue; GameObject go = canvas.gameObject; if (go == null || go == overlayRoot) continue; if (!HasPauseButtons(go)) continue; return go; } return null; } private GameObject FindPauseMenuRootInScene() { var all = Resources.FindObjectsOfTypeAll(); for (int i = 0; i < all.Length; i++) { var t = all[i]; if (t == null) continue; if (!t.gameObject.scene.IsValid()) continue; if (!string.Equals(t.name, "pause", StringComparison.OrdinalIgnoreCase)) continue; GameObject go = t.gameObject; if (HasPauseButtons(go)) return go; } // Fallback: any object that contains all three pause action buttons. for (int i = 0; i < all.Length; i++) { var t = all[i]; if (t == null) continue; if (!t.gameObject.scene.IsValid()) continue; GameObject go = t.gameObject; if (HasPauseButtons(go)) return go; } return null; } private static bool HasPauseButtons(GameObject root) { if (root == null) return false; Button[] buttons = root.GetComponentsInChildren