383 lines
14 KiB
C#
383 lines
14 KiB
C#
|
|
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;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|