Files

383 lines
14 KiB
C#
Raw Permalink Normal View History

2026-07-08 19:53:15 +08:00
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<Canvas>(true);
if (uiCanvas == null) { return; }
int maxOverlayOrder = -1;
Canvas overlayCanvas = null;
if (blackScreenCanvasGroup != null) overlayCanvas = blackScreenCanvasGroup.GetComponentInParent<Canvas>(true);
if (overlayCanvas == null && whiteScreenCanvasGroup != null) overlayCanvas = whiteScreenCanvasGroup.GetComponentInParent<Canvas>(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<CharacterController>();
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<Animator>();
if (animator != null) animator.enabled = false;
var reachCanvasGroupAnimator = cg.GetComponent<Michsky.UI.Reach.CanvasGroupAnimator>();
if (reachCanvasGroupAnimator != null) reachCanvasGroupAnimator.enabled = false;
var reachImageFading = cg.GetComponent<Michsky.UI.Reach.ImageFading>();
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<Canvas>(true);
if (canvas != null)
{
canvas.overrideSorting = true;
canvas.sortingOrder = overlaySortingOrder;
}
}
}
}
}