Files
aibis-dream/Assets/Scripts/UI/Panel/ScreenTransitionPanel.cs
T
dingyuntian 4100232162 feat: 添加全局屏幕转场面板
- 新增 ScreenTransitionPanel,支持 fade_in/out、fade_in_white、flash_white、
  wipe_in/out、shutter_in/out 等多种转场效果
- 新增 Screen Transition Panel Prefab 并在 Persistence 场景挂载
- UIManager 启动时自动显示该面板
- BaseYarnCommand 新增对应的 Yarn 全局转场指令
2026-07-16 17:56:33 +08:00

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;
}
}
}