Initial Unity project commit
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
#if DOTWEEN
|
||||
using DG.Tweening;
|
||||
#endif
|
||||
|
||||
namespace EndingSystem
|
||||
{
|
||||
public class EndingDoorController : MonoBehaviour
|
||||
{
|
||||
[Header("Door References")]
|
||||
public Transform leftDoor;
|
||||
public Transform rightDoor;
|
||||
|
||||
[Header("Animation Settings")]
|
||||
[Tooltip("The initial small angle to open the door (e.g., crack it open slightly)")]
|
||||
public float openLittleAngle = 10f;
|
||||
public float openLittleDuration = 1.5f;
|
||||
|
||||
[Tooltip("How long to pause before fully opening the door")]
|
||||
public float pauseDuration = 0.5f;
|
||||
|
||||
[Tooltip("The final angle to fully open the door")]
|
||||
public float fullOpenAngle = 90f;
|
||||
public float fullOpenDuration = 3f;
|
||||
|
||||
[Tooltip("The axis of rotation for the doors (usually Vector3.up)")]
|
||||
public Vector3 rotationAxis = Vector3.up;
|
||||
|
||||
[Tooltip("If true, the right door rotates in the opposite direction of the left door")]
|
||||
public bool mirrorRightDoor = true;
|
||||
|
||||
#if DOTWEEN
|
||||
[Header("DOTween")]
|
||||
public Ease openLittleEase = Ease.InOutSine;
|
||||
public Ease fullOpenEase = Ease.InOutSine;
|
||||
#endif
|
||||
|
||||
private Quaternion leftClosedRotation;
|
||||
private Quaternion rightClosedRotation;
|
||||
#if DOTWEEN
|
||||
private Sequence currentSequence;
|
||||
#endif
|
||||
|
||||
public void OpenDoor(bool useUnscaledTime = false)
|
||||
{
|
||||
#if DOTWEEN
|
||||
PlayOpenDoor(useUnscaledTime);
|
||||
#else
|
||||
StartCoroutine(DoorAnimationCoroutine());
|
||||
#endif
|
||||
}
|
||||
|
||||
#if DOTWEEN
|
||||
private void OnDisable()
|
||||
{
|
||||
if (currentSequence != null)
|
||||
{
|
||||
currentSequence.Kill(false);
|
||||
currentSequence = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void CacheClosedRotations()
|
||||
{
|
||||
if (leftDoor != null) leftClosedRotation = leftDoor.localRotation;
|
||||
if (rightDoor != null) rightClosedRotation = rightDoor.localRotation;
|
||||
}
|
||||
|
||||
private void PlayOpenDoor(bool useUnscaledTime)
|
||||
{
|
||||
CacheClosedRotations();
|
||||
|
||||
if (leftDoor != null) leftDoor.DOKill();
|
||||
if (rightDoor != null) rightDoor.DOKill();
|
||||
|
||||
if (currentSequence != null)
|
||||
{
|
||||
currentSequence.Kill(false);
|
||||
currentSequence = null;
|
||||
}
|
||||
|
||||
currentSequence = DOTween.Sequence();
|
||||
currentSequence.SetUpdate(useUnscaledTime);
|
||||
|
||||
currentSequence.Append(CreateRotateTween(openLittleAngle, Mathf.Max(0.0001f, openLittleDuration), openLittleEase, useUnscaledTime));
|
||||
if (pauseDuration > 0f) currentSequence.AppendInterval(pauseDuration);
|
||||
currentSequence.Append(CreateRotateTween(fullOpenAngle, Mathf.Max(0.0001f, fullOpenDuration), fullOpenEase, useUnscaledTime));
|
||||
}
|
||||
|
||||
private Tween CreateRotateTween(float angle, float duration, Ease ease, bool useUnscaledTime)
|
||||
{
|
||||
Sequence seq = DOTween.Sequence();
|
||||
seq.SetUpdate(useUnscaledTime);
|
||||
|
||||
Vector3 axis = rotationAxis.sqrMagnitude > 0f ? rotationAxis.normalized : Vector3.up;
|
||||
|
||||
if (leftDoor != null)
|
||||
{
|
||||
Quaternion target = leftClosedRotation * Quaternion.AngleAxis(angle, axis);
|
||||
Tween t = leftDoor.DOLocalRotateQuaternion(target, duration).SetEase(ease).SetUpdate(useUnscaledTime);
|
||||
seq.Join(t);
|
||||
}
|
||||
|
||||
if (rightDoor != null)
|
||||
{
|
||||
float rightAngle = mirrorRightDoor ? -angle : angle;
|
||||
Quaternion target = rightClosedRotation * Quaternion.AngleAxis(rightAngle, axis);
|
||||
Tween t = rightDoor.DOLocalRotateQuaternion(target, duration).SetEase(ease).SetUpdate(useUnscaledTime);
|
||||
seq.Join(t);
|
||||
}
|
||||
|
||||
return seq;
|
||||
}
|
||||
#endif
|
||||
|
||||
private IEnumerator DoorAnimationCoroutine()
|
||||
{
|
||||
yield return StartCoroutine(RotateDoors(0f, openLittleAngle, openLittleDuration));
|
||||
|
||||
yield return new WaitForSeconds(pauseDuration);
|
||||
|
||||
yield return StartCoroutine(RotateDoors(openLittleAngle, fullOpenAngle, fullOpenDuration));
|
||||
}
|
||||
|
||||
private IEnumerator RotateDoors(float startAngle, float endAngle, float duration)
|
||||
{
|
||||
float t = 0f;
|
||||
while (t < 1f)
|
||||
{
|
||||
t += Time.deltaTime / duration;
|
||||
float currentAngle = Mathf.Lerp(startAngle, endAngle, t);
|
||||
|
||||
if (leftDoor != null)
|
||||
{
|
||||
leftDoor.localRotation = Quaternion.AngleAxis(currentAngle, rotationAxis);
|
||||
}
|
||||
|
||||
if (rightDoor != null)
|
||||
{
|
||||
float rightAngle = mirrorRightDoor ? -currentAngle : currentAngle;
|
||||
rightDoor.localRotation = Quaternion.AngleAxis(rightAngle, rotationAxis);
|
||||
}
|
||||
|
||||
yield return null;
|
||||
}
|
||||
|
||||
if (leftDoor != null)
|
||||
leftDoor.localRotation = Quaternion.AngleAxis(endAngle, rotationAxis);
|
||||
|
||||
if (rightDoor != null)
|
||||
{
|
||||
float rightAngleFinal = mirrorRightDoor ? -endAngle : endAngle;
|
||||
rightDoor.localRotation = Quaternion.AngleAxis(rightAngleFinal, rotationAxis);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4b39d9923eb612140b7809b1cad268e3
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,88 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
#if DOTWEEN
|
||||
using DG.Tweening;
|
||||
#endif
|
||||
|
||||
namespace EndingSystem
|
||||
{
|
||||
public class EndingLightController : MonoBehaviour
|
||||
{
|
||||
[Header("Light Reference")]
|
||||
public Light targetLight;
|
||||
|
||||
[Header("Animation Settings")]
|
||||
[Tooltip("The final intensity of the light when fully turned on")]
|
||||
public float targetIntensity = 5f;
|
||||
[Tooltip("How long it takes for the light to reach target intensity")]
|
||||
public float fadeDuration = 3f;
|
||||
|
||||
#if DOTWEEN
|
||||
[Header("DOTween")]
|
||||
public Ease fadeEase = Ease.InOutSine;
|
||||
private Tween currentTween;
|
||||
#endif
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (targetLight != null)
|
||||
{
|
||||
targetLight.intensity = 0f;
|
||||
targetLight.enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void TurnOnLight(bool useUnscaledTime = false)
|
||||
{
|
||||
if (targetLight != null)
|
||||
{
|
||||
#if DOTWEEN
|
||||
PlayFade(useUnscaledTime);
|
||||
#else
|
||||
StartCoroutine(FadeLightCoroutine());
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
#if DOTWEEN
|
||||
private void OnDisable()
|
||||
{
|
||||
if (currentTween != null)
|
||||
{
|
||||
currentTween.Kill(false);
|
||||
currentTween = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void PlayFade(bool useUnscaledTime)
|
||||
{
|
||||
if (currentTween != null)
|
||||
{
|
||||
currentTween.Kill(false);
|
||||
currentTween = null;
|
||||
}
|
||||
|
||||
float duration = Mathf.Max(0.0001f, fadeDuration);
|
||||
currentTween = DOTween.To(() => targetLight.intensity, v => targetLight.intensity = v, targetIntensity, duration)
|
||||
.SetEase(fadeEase)
|
||||
.SetTarget(targetLight)
|
||||
.SetUpdate(useUnscaledTime);
|
||||
}
|
||||
#endif
|
||||
|
||||
private IEnumerator FadeLightCoroutine()
|
||||
{
|
||||
float t = 0f;
|
||||
float startIntensity = targetLight.intensity;
|
||||
|
||||
while (t < 1f)
|
||||
{
|
||||
t += Time.deltaTime / fadeDuration;
|
||||
targetLight.intensity = Mathf.Lerp(startIntensity, targetIntensity, t);
|
||||
yield return null;
|
||||
}
|
||||
|
||||
targetLight.intensity = targetIntensity;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0cdac34e0f9cd2f4cbd68a80a309d726
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,382 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9e117aa7f36336b4cbb9ac8c6bf7afc8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,121 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace EndingSystem
|
||||
{
|
||||
[RequireComponent(typeof(BoxCollider))]
|
||||
public class EndingTrigger : MonoBehaviour
|
||||
{
|
||||
[Header("Controller Reference")]
|
||||
public EndingSequenceController sequenceController;
|
||||
|
||||
[Header("Trigger Settings")]
|
||||
[Tooltip("If true, the sequence will start when the player enters the trigger. If false, you can call TriggerSequence() manually (e.g., from a UI Button).")]
|
||||
public bool triggerOnCollision = true;
|
||||
[Tooltip("Tag of the player object to detect collision")]
|
||||
public string playerTag = "Player";
|
||||
|
||||
[Header("Win Condition")]
|
||||
[Tooltip("If true, collision trigger requires EndingWinConditionService.CanTriggerEnding == true")]
|
||||
public bool requireWinConditionToTrigger = true;
|
||||
[Tooltip("If true, TriggerSequence() will ignore win condition (useful for debug buttons)")]
|
||||
public bool allowManualTriggerWhenLocked = true;
|
||||
|
||||
private bool hasTriggered = false;
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
enabled = true;
|
||||
triggerOnCollision = true;
|
||||
requireWinConditionToTrigger = false;
|
||||
hasTriggered = false;
|
||||
|
||||
var coll = GetComponent<BoxCollider>();
|
||||
if (coll != null) coll.isTrigger = true;
|
||||
}
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
enabled = true;
|
||||
triggerOnCollision = true;
|
||||
requireWinConditionToTrigger = false;
|
||||
|
||||
var coll = GetComponent<BoxCollider>();
|
||||
coll.isTrigger = true;
|
||||
|
||||
if (sequenceController == null)
|
||||
{
|
||||
sequenceController = GetComponentInParent<EndingSequenceController>(true);
|
||||
if (sequenceController == null)
|
||||
{
|
||||
sequenceController = FindObjectOfType<EndingSequenceController>(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnValidate()
|
||||
{
|
||||
enabled = true;
|
||||
triggerOnCollision = true;
|
||||
requireWinConditionToTrigger = false;
|
||||
|
||||
var coll = GetComponent<BoxCollider>();
|
||||
if (coll != null) coll.isTrigger = true;
|
||||
|
||||
if (sequenceController == null)
|
||||
{
|
||||
sequenceController = GetComponentInParent<EndingSequenceController>(true);
|
||||
if (sequenceController == null)
|
||||
{
|
||||
sequenceController = FindObjectOfType<EndingSequenceController>(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTriggerEnter(Collider other)
|
||||
{
|
||||
if (hasTriggered) return;
|
||||
|
||||
if (other.CompareTag(playerTag))
|
||||
{
|
||||
TriggerSequence();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 提供给外部调用的接口,比如 Button OnClick 事件,方便在开发阶段不检查碰撞直接触发
|
||||
/// </summary>
|
||||
public void TriggerSequence()
|
||||
{
|
||||
if (hasTriggered) return;
|
||||
hasTriggered = true;
|
||||
|
||||
if (!allowManualTriggerWhenLocked && requireWinConditionToTrigger)
|
||||
{
|
||||
var wc = EndingWinConditionService.EnsureInstance();
|
||||
if (wc != null && !wc.CanTriggerEnding)
|
||||
{
|
||||
hasTriggered = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (sequenceController == null)
|
||||
{
|
||||
sequenceController = GetComponentInParent<EndingSequenceController>(true);
|
||||
if (sequenceController == null)
|
||||
{
|
||||
sequenceController = FindObjectOfType<EndingSequenceController>(true);
|
||||
}
|
||||
}
|
||||
|
||||
if (sequenceController != null)
|
||||
{
|
||||
sequenceController.StartEndingSequence();
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("[EndingTrigger] EndingSequenceController is not assigned!");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2c05e5ca4a67e0d48b7efd0317624066
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,18 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace EndingSystem
|
||||
{
|
||||
public class EndingWinConditionDebugButton : MonoBehaviour
|
||||
{
|
||||
public void UnlockEnding()
|
||||
{
|
||||
EndingWinConditionService.EnsureInstance().UnlockEnding();
|
||||
}
|
||||
|
||||
public void LockEnding()
|
||||
{
|
||||
EndingWinConditionService.EnsureInstance().LockEnding();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 966a245d19c45bf408394b84389f4631
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,76 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace EndingSystem
|
||||
{
|
||||
public class EndingWinConditionService : MonoBehaviour
|
||||
{
|
||||
public static EndingWinConditionService Instance { get; private set; }
|
||||
|
||||
[Header("Win Condition")]
|
||||
[SerializeField] private bool canTriggerEnding = false;
|
||||
|
||||
[Header("Lifetime")]
|
||||
[SerializeField] private bool dontDestroyOnLoad = true;
|
||||
|
||||
public event Action<bool> CanTriggerEndingChanged;
|
||||
|
||||
public bool CanTriggerEnding => canTriggerEnding;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (Instance != null && Instance != this)
|
||||
{
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
Instance = this;
|
||||
if (dontDestroyOnLoad) { DontDestroyOnLoad(gameObject); }
|
||||
}
|
||||
|
||||
public void SetCanTriggerEnding(bool value)
|
||||
{
|
||||
if (canTriggerEnding == value) { return; }
|
||||
canTriggerEnding = value;
|
||||
CanTriggerEndingChanged?.Invoke(canTriggerEnding);
|
||||
}
|
||||
|
||||
public void UnlockEnding()
|
||||
{
|
||||
SetCanTriggerEnding(true);
|
||||
}
|
||||
|
||||
public void DebugUnlockEnding()
|
||||
{
|
||||
EnsureInstance().UnlockEnding();
|
||||
}
|
||||
|
||||
public void LockEnding()
|
||||
{
|
||||
SetCanTriggerEnding(false);
|
||||
}
|
||||
|
||||
public void DebugLockEnding()
|
||||
{
|
||||
EnsureInstance().LockEnding();
|
||||
}
|
||||
|
||||
public static EndingWinConditionService EnsureInstance()
|
||||
{
|
||||
if (Instance != null) { return Instance; }
|
||||
|
||||
EndingWinConditionService existing = FindObjectOfType<EndingWinConditionService>(true);
|
||||
if (existing != null)
|
||||
{
|
||||
Instance = existing;
|
||||
return Instance;
|
||||
}
|
||||
|
||||
var go = new GameObject("EndingWinConditionService");
|
||||
var service = go.AddComponent<EndingWinConditionService>();
|
||||
Instance = service;
|
||||
return service;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a419e3badae0d7a4ea5d3b584d74148e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,211 @@
|
||||
using UnityEngine;
|
||||
using Core.InputLock;
|
||||
#if DOTWEEN
|
||||
using DG.Tweening;
|
||||
#endif
|
||||
|
||||
namespace EndingSystem
|
||||
{
|
||||
public class FaintEndingSequenceController : MonoBehaviour
|
||||
{
|
||||
[Header("References")]
|
||||
public Player.PlayerController playerController;
|
||||
public Transform cameraPivot;
|
||||
public Transform cameraTransform;
|
||||
public CanvasGroup blackScreenCanvasGroup;
|
||||
public GameObject endingUIPanel;
|
||||
|
||||
[Header("Input")]
|
||||
public bool keepCursorLockedUntilEnd = true;
|
||||
public bool switchToUIActionMapAtEnd = true;
|
||||
|
||||
#if DOTWEEN
|
||||
[Header("DOTween")]
|
||||
public bool useUnscaledTime = false;
|
||||
public Ease step1Ease = Ease.InOutSine;
|
||||
public Ease step3Ease = Ease.InOutSine;
|
||||
public Ease step4Ease = Ease.InOutSine;
|
||||
public Ease alphaEase = Ease.InOutSine;
|
||||
#endif
|
||||
|
||||
[Header("Step 1")]
|
||||
public float step1Duration = 1.2f;
|
||||
public Vector3 step1CameraLocalOffset = new Vector3(0f, -0.4f, 0.3f);
|
||||
public float step1Pitch = 35f;
|
||||
[Range(0f, 1f)] public float step1Alpha = 0.65f;
|
||||
|
||||
[Header("Step 2")]
|
||||
public float step2HoldDuration = 0.4f;
|
||||
|
||||
[Header("Step 3")]
|
||||
public float step3Duration = 0.4f;
|
||||
public Vector3 step3CameraLocalOffset = new Vector3(0f, -0.32f, 0.24f);
|
||||
public float step3Pitch = 25f;
|
||||
[Range(0f, 1f)] public float step3Alpha = 0.45f;
|
||||
|
||||
[Header("Step 4")]
|
||||
public float step4Duration = 1.1f;
|
||||
public Vector3 step4CameraLocalOffset = new Vector3(0f, -0.9f, 0.45f);
|
||||
public float step4Pitch = 90f;
|
||||
[Range(0f, 1f)] public float step4Alpha = 1f;
|
||||
|
||||
private bool sequenceStarted;
|
||||
|
||||
private Vector3 initialCameraLocalPos;
|
||||
private Quaternion initialPivotLocalRot;
|
||||
private float initialPivotPitch;
|
||||
|
||||
#if DOTWEEN
|
||||
private Sequence sequence;
|
||||
#endif
|
||||
|
||||
public void StartFaintSequence()
|
||||
{
|
||||
if (sequenceStarted) return;
|
||||
sequenceStarted = true;
|
||||
|
||||
if (endingUIPanel != null) endingUIPanel.SetActive(false);
|
||||
|
||||
if (playerController == null) playerController = FindObjectOfType<Player.PlayerController>(true);
|
||||
if (playerController != null) playerController.enabled = false;
|
||||
|
||||
if (cameraTransform == null)
|
||||
{
|
||||
var cam = Camera.main;
|
||||
if (cam != null) cameraTransform = cam.transform;
|
||||
}
|
||||
|
||||
if (cameraPivot == null && cameraTransform != null) cameraPivot = cameraTransform.parent;
|
||||
|
||||
if (cameraTransform != null) initialCameraLocalPos = cameraTransform.localPosition;
|
||||
if (cameraPivot != null)
|
||||
{
|
||||
initialPivotLocalRot = cameraPivot.localRotation;
|
||||
float x = cameraPivot.localEulerAngles.x;
|
||||
if (x > 180f) x -= 360f;
|
||||
initialPivotPitch = x;
|
||||
}
|
||||
|
||||
PrepareOverlay(blackScreenCanvasGroup, setAlpha: true, alpha: 0f);
|
||||
|
||||
#if DOTWEEN
|
||||
PlayWithDOTween();
|
||||
#else
|
||||
StartCoroutine(PlayWithCoroutine());
|
||||
#endif
|
||||
}
|
||||
|
||||
#if DOTWEEN
|
||||
private void OnDisable()
|
||||
{
|
||||
if (sequence != null)
|
||||
{
|
||||
sequence.Kill(false);
|
||||
sequence = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void PlayWithDOTween()
|
||||
{
|
||||
if (sequence != null)
|
||||
{
|
||||
sequence.Kill(false);
|
||||
sequence = null;
|
||||
}
|
||||
|
||||
if (cameraTransform != null) cameraTransform.DOKill();
|
||||
if (cameraPivot != null) cameraPivot.DOKill();
|
||||
|
||||
sequence = DOTween.Sequence();
|
||||
sequence.SetUpdate(useUnscaledTime);
|
||||
|
||||
Vector3 step1Pos = initialCameraLocalPos + step1CameraLocalOffset;
|
||||
Vector3 step3Pos = initialCameraLocalPos + step3CameraLocalOffset;
|
||||
Vector3 step4Pos = initialCameraLocalPos + step4CameraLocalOffset;
|
||||
|
||||
if (blackScreenCanvasGroup != null)
|
||||
sequence.Join(blackScreenCanvasGroup.DOFade(step1Alpha, Mathf.Max(0.0001f, step1Duration)).SetEase(alphaEase));
|
||||
|
||||
if (cameraTransform != null)
|
||||
sequence.Join(cameraTransform.DOLocalMove(step1Pos, Mathf.Max(0.0001f, step1Duration)).SetEase(step1Ease).SetUpdate(useUnscaledTime));
|
||||
|
||||
if (cameraPivot != null)
|
||||
sequence.Join(cameraPivot.DOLocalRotate(new Vector3(initialPivotPitch + step1Pitch, 0f, 0f), Mathf.Max(0.0001f, step1Duration), RotateMode.Fast).SetEase(step1Ease).SetUpdate(useUnscaledTime));
|
||||
|
||||
if (step2HoldDuration > 0f) sequence.AppendInterval(step2HoldDuration);
|
||||
|
||||
if (blackScreenCanvasGroup != null)
|
||||
sequence.Append(blackScreenCanvasGroup.DOFade(step3Alpha, Mathf.Max(0.0001f, step3Duration)).SetEase(alphaEase));
|
||||
|
||||
if (cameraTransform != null)
|
||||
sequence.Join(cameraTransform.DOLocalMove(step3Pos, Mathf.Max(0.0001f, step3Duration)).SetEase(step3Ease).SetUpdate(useUnscaledTime));
|
||||
|
||||
if (cameraPivot != null)
|
||||
sequence.Join(cameraPivot.DOLocalRotate(new Vector3(initialPivotPitch + step3Pitch, 0f, 0f), Mathf.Max(0.0001f, step3Duration), RotateMode.Fast).SetEase(step3Ease).SetUpdate(useUnscaledTime));
|
||||
|
||||
if (blackScreenCanvasGroup != null)
|
||||
sequence.Append(blackScreenCanvasGroup.DOFade(step4Alpha, Mathf.Max(0.0001f, step4Duration)).SetEase(alphaEase));
|
||||
|
||||
if (cameraTransform != null)
|
||||
sequence.Join(cameraTransform.DOLocalMove(step4Pos, Mathf.Max(0.0001f, step4Duration)).SetEase(step4Ease).SetUpdate(useUnscaledTime));
|
||||
|
||||
if (cameraPivot != null)
|
||||
sequence.Join(cameraPivot.DOLocalRotate(new Vector3(initialPivotPitch + step4Pitch, 0f, 0f), Mathf.Max(0.0001f, step4Duration), RotateMode.Fast).SetEase(step4Ease).SetUpdate(useUnscaledTime));
|
||||
|
||||
sequence.AppendCallback(FinishSequence);
|
||||
}
|
||||
#else
|
||||
private System.Collections.IEnumerator PlayWithCoroutine()
|
||||
{
|
||||
yield return null;
|
||||
}
|
||||
#endif
|
||||
|
||||
private void FinishSequence()
|
||||
{
|
||||
Cursor.lockState = CursorLockMode.None;
|
||||
Cursor.visible = true;
|
||||
|
||||
if (switchToUIActionMapAtEnd && PlayerControlLockService.Instance != null)
|
||||
{
|
||||
PlayerControlLockService.Instance.Lock(this);
|
||||
}
|
||||
|
||||
if (endingUIPanel != null) endingUIPanel.SetActive(true);
|
||||
}
|
||||
|
||||
private void PrepareOverlay(CanvasGroup cg, bool setAlpha, float alpha)
|
||||
{
|
||||
if (cg == null) { return; }
|
||||
|
||||
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;
|
||||
cg.transform.SetAsLastSibling();
|
||||
|
||||
Canvas canvas = cg.GetComponentInParent<Canvas>(true);
|
||||
if (canvas != null)
|
||||
{
|
||||
canvas.overrideSorting = true;
|
||||
canvas.sortingOrder = 5000;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 837e4b6269a003744b0bfbddb6bed35e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user