using System.Collections; using UnityEngine; using Core.InputLock; #if DOTWEEN using DG.Tweening; #endif namespace EndingSystem { public class EndingSequenceController : MonoBehaviour { [Header("Overlay Safety")] public bool forceScreenObjectsActive = true; public bool bringScreenToFront = true; public bool overrideCanvasSorting = true; public int overlaySortingOrder = 5000; [Header("Sequence Settings")] public float delayBeforeFadeFromBlack = 1f; [Header("UI Fader")] [Tooltip("Assign a CanvasGroup attached to a black Image that covers the whole screen")] public CanvasGroup blackScreenCanvasGroup; public float fadeToBlackDuration = 1.5f; public float fadeFromBlackDuration = 2f; [Tooltip("Assign a CanvasGroup attached to a white Image that covers the whole screen")] public CanvasGroup whiteScreenCanvasGroup; public float fadeToWhiteDuration = 2f; [Tooltip("How long to wait after the door opens before fading to white")] public float delayBeforeFadeToWhite = 2f; [Header("Ending UI")] [Tooltip("The UI Panel to activate at the very end (e.g. Return to Menu / Credits)")] public GameObject endingUIPanel; [Header("Player & Camera References")] [Tooltip("The parent object of the player (the one with CharacterController)")] public Transform playerBody; [Tooltip("The camera pivot (usually the parent of the camera that handles up/down look)")] public Transform playerCameraPivot; public Camera playerCamera; [Header("Target Positions & Angles")] [Tooltip("Where should the player be teleported to when the screen goes black?")] public Transform targetPlayerTransform; [Tooltip("The angle to which the camera will look up (negative values usually mean looking up)")] public float cameraLookUpAngle = -30f; public float cameraLookDuration = 3f; [Tooltip("Target FOV to focus on the door")] public float targetFOV = 30f; public float fovShrinkDuration = 2f; [Header("Scene Object Controllers")] public EndingDoorController doorController; public EndingLightController lightController; #if DOTWEEN [Header("DOTween")] public bool useUnscaledTime = false; public Ease fadeToBlackEase = Ease.InOutSine; public Ease fadeFromBlackEase = Ease.InOutSine; public Ease fadeToWhiteEase = Ease.InOutSine; public Ease lookEase = Ease.InOutSine; public Ease fovEase = Ease.InOutSine; #endif private bool sequenceStarted = false; #if DOTWEEN private Sequence sequence; #endif public void StartEndingSequence() { if (sequenceStarted) return; sequenceStarted = true; #if DOTWEEN PlaySequenceWithDOTween(); #else StartCoroutine(EndingCoroutine()); #endif } #if DOTWEEN private void OnDisable() { if (sequence != null) { sequence.Kill(false); sequence = null; } } private void PlaySequenceWithDOTween() { if (sequence != null) { sequence.Kill(false); sequence = null; } if (PlayerControlLockService.Instance != null) { PlayerControlLockService.Instance.Lock(this); } if (blackScreenCanvasGroup == null) { Debug.LogWarning("[EndingSequenceController] Black Screen CanvasGroup is not assigned!"); return; } if (endingUIPanel != null) { endingUIPanel.SetActive(false); } PrepareOverlay(blackScreenCanvasGroup, setAlpha: true, alpha: 0f); if (whiteScreenCanvasGroup != null) { PrepareOverlay(whiteScreenCanvasGroup, setAlpha: true, alpha: 0f); } blackScreenCanvasGroup.DOKill(); if (playerCameraPivot != null) playerCameraPivot.DOKill(); if (playerCamera != null) DOTween.Kill(playerCamera); sequence = DOTween.Sequence(); sequence.SetUpdate(useUnscaledTime); sequence.Append(blackScreenCanvasGroup.DOFade(1f, Mathf.Max(0.0001f, fadeToBlackDuration)).SetEase(fadeToBlackEase)); sequence.AppendCallback(ApplyBlackScreenPlacementAndReset); if (delayBeforeFadeFromBlack > 0f) { sequence.AppendInterval(delayBeforeFadeFromBlack); } Tween fadeFromTween = blackScreenCanvasGroup.DOFade(0f, Mathf.Max(0.0001f, fadeFromBlackDuration)).SetEase(fadeFromBlackEase); sequence.Append(fadeFromTween); if (playerCameraPivot != null) { Vector3 targetEuler = new Vector3(cameraLookUpAngle, 0f, 0f); Tween lookTween = playerCameraPivot.DOLocalRotate(targetEuler, Mathf.Max(0.0001f, cameraLookDuration), RotateMode.Fast).SetEase(lookEase); lookTween.SetUpdate(useUnscaledTime); sequence.Join(lookTween); } if (playerCamera != null) { float duration = Mathf.Max(0.0001f, fovShrinkDuration); Tween fovTween = DOTween.To(() => playerCamera.fieldOfView, v => playerCamera.fieldOfView = v, targetFOV, duration) .SetEase(fovEase) .SetTarget(playerCamera) .SetUpdate(useUnscaledTime); sequence.Append(fovTween); } sequence.AppendCallback(() => { if (doorController != null) doorController.OpenDoor(useUnscaledTime); if (lightController != null) lightController.TurnOnLight(useUnscaledTime); }); // 等待开门动画和光照完成,再等一个自定义时间 float doorWaitTime = (doorController != null) ? (doorController.openLittleDuration + doorController.pauseDuration + doorController.fullOpenDuration) : 0f; sequence.AppendInterval(doorWaitTime + delayBeforeFadeToWhite); // 淡入白屏 if (whiteScreenCanvasGroup != null) { Tween whiteFadeTween = whiteScreenCanvasGroup.DOFade(1f, Mathf.Max(0.0001f, fadeToWhiteDuration)).SetEase(fadeToWhiteEase); sequence.Append(whiteFadeTween); } // 最后激活UI并解锁鼠标 sequence.AppendCallback(FinishSequence); } #endif private IEnumerator EndingCoroutine() { if (PlayerControlLockService.Instance != null) { PlayerControlLockService.Instance.Lock(this); } if (endingUIPanel != null) { endingUIPanel.SetActive(false); } PrepareOverlay(blackScreenCanvasGroup, setAlpha: true, alpha: 0f); if (whiteScreenCanvasGroup != null) PrepareOverlay(whiteScreenCanvasGroup, setAlpha: true, alpha: 0f); yield return StartCoroutine(FadeScreen(0f, 1f, fadeToBlackDuration, blackScreenCanvasGroup)); ApplyBlackScreenPlacementAndReset(); yield return new WaitForSeconds(delayBeforeFadeFromBlack); Coroutine fadeRoutine = StartCoroutine(FadeScreen(1f, 0f, fadeFromBlackDuration, blackScreenCanvasGroup)); Coroutine lookRoutine = StartCoroutine(LookUpCoroutine()); yield return fadeRoutine; yield return lookRoutine; if (playerCamera != null) { float startFOV = playerCamera.fieldOfView; float t = 0f; while (t < 1f) { t += Time.deltaTime / fovShrinkDuration; playerCamera.fieldOfView = Mathf.Lerp(startFOV, targetFOV, t); yield return null; } playerCamera.fieldOfView = targetFOV; } if (doorController != null) doorController.OpenDoor(); if (lightController != null) lightController.TurnOnLight(); // 等待开门时间 float doorWaitTime = (doorController != null) ? (doorController.openLittleDuration + doorController.pauseDuration + doorController.fullOpenDuration) : 0f; yield return new WaitForSeconds(doorWaitTime + delayBeforeFadeToWhite); if (whiteScreenCanvasGroup != null) { yield return StartCoroutine(FadeScreen(0f, 1f, fadeToWhiteDuration, whiteScreenCanvasGroup)); } FinishSequence(); } private void FinishSequence() { if (endingUIPanel != null) { endingUIPanel.SetActive(true); BringEndingUIToFront(); } // 解锁玩家输入,以显示鼠标点击 UI,但是玩家在结局时应该只看UI,通常我们可以保持移动锁定 // 但你的要求是"解锁鼠标",我们可以解锁 LockService,由于游戏已经结束,你可能需要在UI层面自己拦截按键 // 另外一种做法是调用 Unlock,然后让 UI 自己去 Lock(this),这取决于你的 UI 设计 if (PlayerControlLockService.Instance != null) { PlayerControlLockService.Instance.Unlock(this); } // 强制显示鼠标 Cursor.lockState = CursorLockMode.None; Cursor.visible = true; } private void BringEndingUIToFront() { if (endingUIPanel == null) { return; } endingUIPanel.transform.SetAsLastSibling(); Canvas uiCanvas = endingUIPanel.GetComponentInParent(true); if (uiCanvas == null) { return; } int maxOverlayOrder = -1; Canvas overlayCanvas = null; if (blackScreenCanvasGroup != null) overlayCanvas = blackScreenCanvasGroup.GetComponentInParent(true); if (overlayCanvas == null && whiteScreenCanvasGroup != null) overlayCanvas = whiteScreenCanvasGroup.GetComponentInParent(true); if (overlayCanvas != null) maxOverlayOrder = overlayCanvas.sortingOrder; if (uiCanvas == overlayCanvas) { if (blackScreenCanvasGroup != null) blackScreenCanvasGroup.transform.SetSiblingIndex(0); if (whiteScreenCanvasGroup != null) whiteScreenCanvasGroup.transform.SetSiblingIndex(0); endingUIPanel.transform.SetAsLastSibling(); return; } uiCanvas.overrideSorting = true; uiCanvas.sortingOrder = Mathf.Max(uiCanvas.sortingOrder, maxOverlayOrder + 1, overlaySortingOrder + 1); } private void ApplyBlackScreenPlacementAndReset() { if (playerBody != null && targetPlayerTransform != null) { var cc = playerBody.GetComponent(); if (cc != null) cc.enabled = false; playerBody.position = targetPlayerTransform.position; playerBody.rotation = targetPlayerTransform.rotation; if (cc != null) cc.enabled = true; } if (playerCameraPivot != null) { playerCameraPivot.localRotation = Quaternion.identity; } } private IEnumerator LookUpCoroutine() { if (playerCameraPivot == null) yield break; Quaternion startRot = playerCameraPivot.localRotation; Quaternion endRot = Quaternion.Euler(cameraLookUpAngle, 0f, 0f); float t = 0f; while (t < 1f) { t += Time.deltaTime / cameraLookDuration; playerCameraPivot.localRotation = Quaternion.Lerp(startRot, endRot, t); yield return null; } playerCameraPivot.localRotation = endRot; } private IEnumerator FadeScreen(float startAlpha, float endAlpha, float duration, CanvasGroup cg) { if (cg == null) { Debug.LogWarning("[EndingSequenceController] A CanvasGroup is not assigned for fade!"); yield break; } float t = 0f; while (t < 1f) { t += Time.deltaTime / duration; cg.alpha = Mathf.Lerp(startAlpha, endAlpha, t); yield return null; } cg.alpha = endAlpha; } private void PrepareOverlay(CanvasGroup cg, bool setAlpha, float alpha) { if (cg == null) { return; } if (forceScreenObjectsActive) { if (!cg.gameObject.activeSelf) cg.gameObject.SetActive(true); if (cg.transform.parent != null && !cg.transform.parent.gameObject.activeSelf) cg.transform.parent.gameObject.SetActive(true); } var animator = cg.GetComponent(); if (animator != null) animator.enabled = false; var reachCanvasGroupAnimator = cg.GetComponent(); if (reachCanvasGroupAnimator != null) reachCanvasGroupAnimator.enabled = false; var reachImageFading = cg.GetComponent(); if (reachImageFading != null) reachImageFading.enabled = false; #if DOTWEEN cg.DOKill(); #endif cg.interactable = false; cg.blocksRaycasts = false; if (setAlpha) cg.alpha = alpha; if (bringScreenToFront) { cg.transform.SetAsLastSibling(); } if (overrideCanvasSorting) { Canvas canvas = cg.GetComponentInParent(true); if (canvas != null) { canvas.overrideSorting = true; canvas.sortingOrder = overlaySortingOrder; } } } } }