- 新增 ScreenTransitionPanel,支持 fade_in/out、fade_in_white、flash_white、 wipe_in/out、shutter_in/out 等多种转场效果 - 新增 Screen Transition Panel Prefab 并在 Persistence 场景挂载 - UIManager 启动时自动显示该面板 - BaseYarnCommand 新增对应的 Yarn 全局转场指令
443 lines
13 KiB
C#
443 lines
13 KiB
C#
using System.Collections;
|
|
using DG.Tweening;
|
|
using UnityEngine;
|
|
using UnityEngine.UI;
|
|
|
|
namespace AibisDream.UI
|
|
{
|
|
/// <summary>
|
|
/// 全局屏幕转场面板。In 决定遮罩颜色和盖入方式,Out 决定揭出方式。
|
|
/// </summary>
|
|
public class ScreenTransitionPanel : MonoBehaviour, IUIPanel
|
|
{
|
|
private enum TransitionVisual
|
|
{
|
|
None,
|
|
Overlay,
|
|
Shutter
|
|
}
|
|
|
|
private const float DefaultWipeDuration = 0.6f;
|
|
private const float DefaultShutterDuration = 0.6f;
|
|
|
|
[SerializeField] private GameObject transitionVisualRoot;
|
|
[SerializeField] private Image overlayImage;
|
|
[SerializeField] private RectTransform shutterRoot;
|
|
[SerializeField] private Image shutterTop;
|
|
[SerializeField] private Image shutterBottom;
|
|
|
|
private Color _coverColor = Color.black;
|
|
private TransitionVisual _currentVisual = TransitionVisual.None;
|
|
private bool _isCovered;
|
|
private int _transitionVersion;
|
|
|
|
public bool IsOpen => gameObject.activeSelf;
|
|
public bool IsCloseable => false;
|
|
|
|
private RectTransform OverlayRect => overlayImage.rectTransform;
|
|
|
|
private void Awake()
|
|
{
|
|
ResetVisualsImmediate();
|
|
}
|
|
|
|
private void OnDisable()
|
|
{
|
|
CancelActiveTransition();
|
|
}
|
|
|
|
public IEnumerator FadeInAsync(float duration = 1f, bool useUnscaledTime = false)
|
|
{
|
|
return FadeInInternal(Color.black, duration, useUnscaledTime);
|
|
}
|
|
|
|
public IEnumerator FadeInWhiteAsync(float duration = 1f, bool useUnscaledTime = false)
|
|
{
|
|
return FadeInInternal(Color.white, duration, useUnscaledTime);
|
|
}
|
|
|
|
public IEnumerator FadeOutAsync(float duration = 1f, bool useUnscaledTime = false)
|
|
{
|
|
var version = BeginTransition();
|
|
PrepareOverlayForFadeOut();
|
|
|
|
duration = Mathf.Max(0f, duration);
|
|
if (duration <= 0f)
|
|
{
|
|
if (version == _transitionVersion)
|
|
{
|
|
ResetVisualsImmediate();
|
|
}
|
|
|
|
yield break;
|
|
}
|
|
|
|
var tween = overlayImage.DOFade(0f, duration)
|
|
.SetUpdate(useUnscaledTime)
|
|
.SetTarget(this);
|
|
yield return tween.WaitForCompletion();
|
|
|
|
if (version == _transitionVersion)
|
|
{
|
|
ResetVisualsImmediate();
|
|
}
|
|
}
|
|
|
|
public void FadeInSync(float duration = 1f)
|
|
{
|
|
StartCoroutine(FadeInAsync(duration));
|
|
}
|
|
|
|
public void FadeOutSync(float duration = 1f)
|
|
{
|
|
StartCoroutine(FadeOutAsync(duration));
|
|
}
|
|
|
|
public IEnumerator FlashWhite(float duration = 0.2f, bool useUnscaledTime = false)
|
|
{
|
|
var version = BeginTransition();
|
|
PrepareVisibleOverlay(Color.white, 0f);
|
|
_isCovered = false;
|
|
|
|
duration = Mathf.Max(0f, duration);
|
|
if (duration <= 0f)
|
|
{
|
|
if (version == _transitionVersion)
|
|
{
|
|
ResetVisualsImmediate();
|
|
}
|
|
|
|
yield break;
|
|
}
|
|
|
|
var halfDuration = duration * 0.5f;
|
|
var sequence = DOTween.Sequence()
|
|
.Append(overlayImage.DOFade(1f, halfDuration))
|
|
.Append(overlayImage.DOFade(0f, halfDuration))
|
|
.SetUpdate(useUnscaledTime)
|
|
.SetTarget(this);
|
|
yield return sequence.WaitForCompletion();
|
|
|
|
if (version == _transitionVersion)
|
|
{
|
|
ResetVisualsImmediate();
|
|
}
|
|
}
|
|
|
|
public IEnumerator WipeInAsync(float duration = DefaultWipeDuration, bool useUnscaledTime = false)
|
|
{
|
|
var version = BeginTransition();
|
|
PrepareVisibleOverlay(Color.black, 1f);
|
|
_isCovered = false;
|
|
|
|
var width = GetTransitionWidth();
|
|
OverlayRect.anchoredPosition = new Vector2(-width, 0f);
|
|
duration = Mathf.Max(0f, duration);
|
|
if (duration <= 0f)
|
|
{
|
|
OverlayRect.anchoredPosition = Vector2.zero;
|
|
CompleteCover(version);
|
|
yield break;
|
|
}
|
|
|
|
var tween = OverlayRect.DOAnchorPosX(0f, duration)
|
|
.SetEase(Ease.InOutQuad)
|
|
.SetUpdate(useUnscaledTime)
|
|
.SetTarget(this);
|
|
yield return tween.WaitForCompletion();
|
|
CompleteCover(version);
|
|
}
|
|
|
|
public IEnumerator WipeOutAsync(float duration = DefaultWipeDuration, bool useUnscaledTime = false)
|
|
{
|
|
var version = BeginTransition();
|
|
PrepareOverlayCovered();
|
|
|
|
var width = GetTransitionWidth();
|
|
duration = Mathf.Max(0f, duration);
|
|
if (duration <= 0f)
|
|
{
|
|
OverlayRect.anchoredPosition = new Vector2(width, 0f);
|
|
if (version == _transitionVersion)
|
|
{
|
|
ResetVisualsImmediate();
|
|
}
|
|
|
|
yield break;
|
|
}
|
|
|
|
var tween = OverlayRect.DOAnchorPosX(width, duration)
|
|
.SetEase(Ease.InOutQuad)
|
|
.SetUpdate(useUnscaledTime)
|
|
.SetTarget(this);
|
|
yield return tween.WaitForCompletion();
|
|
|
|
if (version == _transitionVersion)
|
|
{
|
|
ResetVisualsImmediate();
|
|
}
|
|
}
|
|
|
|
public IEnumerator ShutterInAsync(float duration = DefaultShutterDuration, bool useUnscaledTime = false)
|
|
{
|
|
var version = BeginTransition();
|
|
_coverColor = Color.black;
|
|
PrepareShutter(_coverColor, 0f);
|
|
_isCovered = false;
|
|
|
|
duration = Mathf.Max(0f, duration);
|
|
if (duration <= 0f)
|
|
{
|
|
SetShutterScale(1f);
|
|
CompleteCover(version);
|
|
yield break;
|
|
}
|
|
|
|
var sequence = DOTween.Sequence()
|
|
.Join(shutterTop.rectTransform.DOScaleY(1f, duration))
|
|
.Join(shutterBottom.rectTransform.DOScaleY(1f, duration))
|
|
.SetEase(Ease.InOutQuad)
|
|
.SetUpdate(useUnscaledTime)
|
|
.SetTarget(this);
|
|
yield return sequence.WaitForCompletion();
|
|
CompleteCover(version);
|
|
}
|
|
|
|
public IEnumerator ShutterOutAsync(float duration = DefaultShutterDuration, bool useUnscaledTime = false)
|
|
{
|
|
var version = BeginTransition();
|
|
if (!transitionVisualRoot.activeSelf)
|
|
{
|
|
_coverColor = Color.black;
|
|
}
|
|
|
|
PrepareShutter(_coverColor, 1f);
|
|
duration = Mathf.Max(0f, duration);
|
|
if (duration <= 0f)
|
|
{
|
|
SetShutterScale(0f);
|
|
if (version == _transitionVersion)
|
|
{
|
|
ResetVisualsImmediate();
|
|
}
|
|
|
|
yield break;
|
|
}
|
|
|
|
var sequence = DOTween.Sequence()
|
|
.Join(shutterTop.rectTransform.DOScaleY(0f, duration))
|
|
.Join(shutterBottom.rectTransform.DOScaleY(0f, duration))
|
|
.SetEase(Ease.InOutQuad)
|
|
.SetUpdate(useUnscaledTime)
|
|
.SetTarget(this);
|
|
yield return sequence.WaitForCompletion();
|
|
|
|
if (version == _transitionVersion)
|
|
{
|
|
ResetVisualsImmediate();
|
|
}
|
|
}
|
|
|
|
public bool CaptureScreenCovered()
|
|
{
|
|
return _isCovered;
|
|
}
|
|
|
|
public void ApplyScreenCovered(bool isCovered)
|
|
{
|
|
BeginTransition();
|
|
if (!isCovered)
|
|
{
|
|
ResetVisualsImmediate();
|
|
return;
|
|
}
|
|
|
|
PrepareVisibleOverlay(Color.black, 1f);
|
|
_isCovered = true;
|
|
}
|
|
|
|
public void Show()
|
|
{
|
|
gameObject.SetActive(true);
|
|
}
|
|
|
|
public void Hide()
|
|
{
|
|
BeginTransition();
|
|
ResetVisualsImmediate();
|
|
gameObject.SetActive(false);
|
|
}
|
|
|
|
private IEnumerator FadeInInternal(Color color, float duration, bool useUnscaledTime)
|
|
{
|
|
var version = BeginTransition();
|
|
var startAlpha = transitionVisualRoot.activeSelf && _currentVisual == TransitionVisual.Overlay
|
|
? overlayImage.color.a
|
|
: 0f;
|
|
PrepareVisibleOverlay(color, startAlpha);
|
|
_isCovered = false;
|
|
|
|
duration = Mathf.Max(0f, duration);
|
|
if (duration <= 0f)
|
|
{
|
|
SetImageAlpha(overlayImage, 1f);
|
|
CompleteCover(version);
|
|
yield break;
|
|
}
|
|
|
|
var tween = overlayImage.DOFade(1f, duration)
|
|
.SetUpdate(useUnscaledTime)
|
|
.SetTarget(this);
|
|
yield return tween.WaitForCompletion();
|
|
CompleteCover(version);
|
|
}
|
|
|
|
private int BeginTransition()
|
|
{
|
|
_transitionVersion++;
|
|
CancelActiveTransition();
|
|
return _transitionVersion;
|
|
}
|
|
|
|
private void CancelActiveTransition()
|
|
{
|
|
DOTween.Kill(this, false);
|
|
overlayImage?.DOKill(false);
|
|
if (overlayImage != null)
|
|
{
|
|
overlayImage.rectTransform.DOKill(false);
|
|
}
|
|
|
|
shutterTop?.rectTransform.DOKill(false);
|
|
shutterBottom?.rectTransform.DOKill(false);
|
|
}
|
|
|
|
private void PrepareVisibleOverlay(Color color, float alpha)
|
|
{
|
|
transitionVisualRoot.SetActive(true);
|
|
_coverColor = Opaque(color);
|
|
_currentVisual = TransitionVisual.Overlay;
|
|
|
|
shutterRoot.gameObject.SetActive(false);
|
|
overlayImage.gameObject.SetActive(true);
|
|
OverlayRect.anchoredPosition = Vector2.zero;
|
|
overlayImage.color = WithAlpha(_coverColor, alpha);
|
|
}
|
|
|
|
private void PrepareOverlayForFadeOut()
|
|
{
|
|
if (!transitionVisualRoot.activeSelf)
|
|
{
|
|
PrepareVisibleOverlay(Color.black, 1f);
|
|
return;
|
|
}
|
|
|
|
if (_currentVisual == TransitionVisual.Shutter)
|
|
{
|
|
PrepareVisibleOverlay(_coverColor, 1f);
|
|
return;
|
|
}
|
|
|
|
var alpha = overlayImage.gameObject.activeSelf ? overlayImage.color.a : 1f;
|
|
PrepareVisibleOverlay(_coverColor, alpha);
|
|
}
|
|
|
|
private void PrepareOverlayCovered()
|
|
{
|
|
if (!transitionVisualRoot.activeSelf)
|
|
{
|
|
_coverColor = Color.black;
|
|
}
|
|
|
|
PrepareVisibleOverlay(_coverColor, 1f);
|
|
}
|
|
|
|
private void PrepareShutter(Color color, float scaleY)
|
|
{
|
|
transitionVisualRoot.SetActive(true);
|
|
_coverColor = Opaque(color);
|
|
_currentVisual = TransitionVisual.Shutter;
|
|
|
|
shutterRoot.gameObject.SetActive(true);
|
|
shutterTop.gameObject.SetActive(true);
|
|
shutterBottom.gameObject.SetActive(true);
|
|
shutterTop.color = _coverColor;
|
|
shutterBottom.color = _coverColor;
|
|
SetShutterScale(scaleY);
|
|
|
|
// 完全覆盖帧先准备目标节点,再关闭来源节点,避免跨模式切换时漏出背景。
|
|
overlayImage.gameObject.SetActive(false);
|
|
}
|
|
|
|
private void CompleteCover(int version)
|
|
{
|
|
if (version != _transitionVersion)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_isCovered = true;
|
|
}
|
|
|
|
private float GetTransitionWidth()
|
|
{
|
|
Canvas.ForceUpdateCanvases();
|
|
var rectTransform = transitionVisualRoot.transform as RectTransform;
|
|
return Mathf.Max(1f, rectTransform != null ? rectTransform.rect.width : Screen.width);
|
|
}
|
|
|
|
private void SetShutterScale(float scaleY)
|
|
{
|
|
var topScale = shutterTop.rectTransform.localScale;
|
|
topScale.y = scaleY;
|
|
shutterTop.rectTransform.localScale = topScale;
|
|
|
|
var bottomScale = shutterBottom.rectTransform.localScale;
|
|
bottomScale.y = scaleY;
|
|
shutterBottom.rectTransform.localScale = bottomScale;
|
|
}
|
|
|
|
private void ResetVisualsImmediate()
|
|
{
|
|
CancelActiveTransition();
|
|
|
|
if (overlayImage != null)
|
|
{
|
|
overlayImage.gameObject.SetActive(true);
|
|
OverlayRect.anchoredPosition = Vector2.zero;
|
|
overlayImage.color = Color.clear;
|
|
}
|
|
|
|
if (shutterRoot != null)
|
|
{
|
|
SetShutterScale(0f);
|
|
shutterRoot.gameObject.SetActive(false);
|
|
}
|
|
|
|
_coverColor = Color.black;
|
|
_currentVisual = TransitionVisual.None;
|
|
_isCovered = false;
|
|
transitionVisualRoot?.SetActive(false);
|
|
}
|
|
|
|
private static Color Opaque(Color color)
|
|
{
|
|
color.a = 1f;
|
|
return color;
|
|
}
|
|
|
|
private static Color WithAlpha(Color color, float alpha)
|
|
{
|
|
color.a = alpha;
|
|
return color;
|
|
}
|
|
|
|
private static void SetImageAlpha(Graphic graphic, float alpha)
|
|
{
|
|
var color = graphic.color;
|
|
color.a = alpha;
|
|
graphic.color = color;
|
|
}
|
|
}
|
|
}
|