From 410023216262a1aea45c59ce31a2894fe4317dd4 Mon Sep 17 00:00:00 2001 From: Ding Yuntian <1491671119@qq.com> Date: Thu, 16 Jul 2026 17:56:33 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=85=A8=E5=B1=80?= =?UTF-8?q?=E5=B1=8F=E5=B9=95=E8=BD=AC=E5=9C=BA=E9=9D=A2=E6=9D=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 ScreenTransitionPanel,支持 fade_in/out、fade_in_white、flash_white、 wipe_in/out、shutter_in/out 等多种转场效果 - 新增 Screen Transition Panel Prefab 并在 Persistence 场景挂载 - UIManager 启动时自动显示该面板 - BaseYarnCommand 新增对应的 Yarn 全局转场指令 --- .../Prefabs/Screen Transition Panel.prefab | 395 ++++++++++++++++ .../Screen Transition Panel.prefab.meta | 7 + Assets/Scenes/Persistence.unity | 19 + .../Scripts/Dialog System/BaseYarnCommand.cs | 44 +- .../Scripts/UI/Panel/ScreenTransitionPanel.cs | 442 ++++++++++++++++++ .../UI/Panel/ScreenTransitionPanel.cs.meta | 11 + Assets/Scripts/UI/UIManager.cs | 1 + 7 files changed, 918 insertions(+), 1 deletion(-) create mode 100644 Assets/GameContent/Feature_MainUI/Prefabs/Screen Transition Panel.prefab create mode 100644 Assets/GameContent/Feature_MainUI/Prefabs/Screen Transition Panel.prefab.meta create mode 100644 Assets/Scripts/UI/Panel/ScreenTransitionPanel.cs create mode 100644 Assets/Scripts/UI/Panel/ScreenTransitionPanel.cs.meta diff --git a/Assets/GameContent/Feature_MainUI/Prefabs/Screen Transition Panel.prefab b/Assets/GameContent/Feature_MainUI/Prefabs/Screen Transition Panel.prefab new file mode 100644 index 000000000..60f49686b --- /dev/null +++ b/Assets/GameContent/Feature_MainUI/Prefabs/Screen Transition Panel.prefab @@ -0,0 +1,395 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1 &100100 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 100101} + - component: {fileID: 100102} + m_Layer: 5 + m_Name: Screen Transition Panel + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &100101 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 100100} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 200101} + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &100102 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 100100} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 6b517be3c5ca46bdad6d13b94cb1b179, type: 3} + m_Name: + m_EditorClassIdentifier: + transitionVisualRoot: {fileID: 200100} + overlayImage: {fileID: 300103} + shutterRoot: {fileID: 400101} + shutterTop: {fileID: 500103} + shutterBottom: {fileID: 600103} +--- !u!1 &200100 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 200101} + - component: {fileID: 200102} + - component: {fileID: 200103} + m_Layer: 5 + m_Name: Transition Visual Root + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 0 +--- !u!224 &200101 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 200100} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 300101} + - {fileID: 400101} + m_Father: {fileID: 100101} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &200102 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 200100} + m_CullTransparentMesh: 1 +--- !u!114 &200103 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 200100} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0, g: 0, b: 0, a: 0} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &300100 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 300101} + - component: {fileID: 300102} + - component: {fileID: 300103} + m_Layer: 5 + m_Name: Overlay Image + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &300101 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 300100} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 200101} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &300102 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 300100} + m_CullTransparentMesh: 1 +--- !u!114 &300103 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 300100} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0, g: 0, b: 0, a: 0} + m_RaycastTarget: 0 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &400100 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 400101} + m_Layer: 5 + m_Name: Shutter Root + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 0 +--- !u!224 &400101 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 400100} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 500101} + - {fileID: 600101} + m_Father: {fileID: 200101} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &500100 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 500101} + - component: {fileID: 500102} + - component: {fileID: 500103} + m_Layer: 5 + m_Name: Shutter Top + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &500101 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 500100} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 400101} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0.5} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 1} + m_Pivot: {x: 0.5, y: 1} +--- !u!222 &500102 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 500100} + m_CullTransparentMesh: 1 +--- !u!114 &500103 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 500100} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0, g: 0, b: 0, a: 1} + m_RaycastTarget: 0 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &600100 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 600101} + - component: {fileID: 600102} + - component: {fileID: 600103} + m_Layer: 5 + m_Name: Shutter Bottom + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &600101 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 600100} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 400101} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 0.5} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 1} + m_Pivot: {x: 0.5, y: 0} +--- !u!222 &600102 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 600100} + m_CullTransparentMesh: 1 +--- !u!114 &600103 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 600100} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0, g: 0, b: 0, a: 1} + m_RaycastTarget: 0 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 diff --git a/Assets/GameContent/Feature_MainUI/Prefabs/Screen Transition Panel.prefab.meta b/Assets/GameContent/Feature_MainUI/Prefabs/Screen Transition Panel.prefab.meta new file mode 100644 index 000000000..1815e9974 --- /dev/null +++ b/Assets/GameContent/Feature_MainUI/Prefabs/Screen Transition Panel.prefab.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 3923593479c745bdb809777da8333e7e +PrefabImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scenes/Persistence.unity b/Assets/Scenes/Persistence.unity index f0d0ca633..0870a6863 100644 --- a/Assets/Scenes/Persistence.unity +++ b/Assets/Scenes/Persistence.unity @@ -8385,6 +8385,7 @@ RectTransform: - {fileID: 923998313} - {fileID: 1867622714} - {fileID: 3100100002} + - {fileID: 3100101002} m_Father: {fileID: 1110722291} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0, y: 0} @@ -9687,6 +9688,24 @@ PrefabInstance: m_AddedGameObjects: [] m_AddedComponents: [] m_SourcePrefab: {fileID: 100100000, guid: 0bd336c33b9dc77498582232b543e604, type: 3} +--- !u!1001 &3100101000 +PrefabInstance: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Modification: + serializedVersion: 3 + m_TransformParent: {fileID: 2091451641} + m_Modifications: [] + m_RemovedComponents: [] + m_RemovedGameObjects: [] + m_AddedGameObjects: [] + m_AddedComponents: [] + m_SourcePrefab: {fileID: 100100000, guid: 3923593479c745bdb809777da8333e7e, type: 3} +--- !u!224 &3100101002 stripped +RectTransform: + m_CorrespondingSourceObject: {fileID: 100101, guid: 3923593479c745bdb809777da8333e7e, type: 3} + m_PrefabInstance: {fileID: 3100101000} + m_PrefabAsset: {fileID: 0} --- !u!1660057539 &9223372036854775807 SceneRoots: m_ObjectHideFlags: 0 diff --git a/Assets/Scripts/Dialog System/BaseYarnCommand.cs b/Assets/Scripts/Dialog System/BaseYarnCommand.cs index e8cbf2ba4..521c79348 100644 --- a/Assets/Scripts/Dialog System/BaseYarnCommand.cs +++ b/Assets/Scripts/Dialog System/BaseYarnCommand.cs @@ -57,7 +57,7 @@ namespace AibisDream var panel = UIManager.Instance.GetPanel(); return panel.FadeOutAsync(duration); } - + [YarnCommand("fade_in_sync")] public static void FadeInSync(float duration = 1) { @@ -72,6 +72,48 @@ namespace AibisDream panel.FadeOutSync(duration); } + [YarnCommand("fade_in_white")] + public static IEnumerator FadeInWhite(float duration = 1) + { + var panel = UIManager.Instance.GetPanel(); + return panel.FadeInWhiteAsync(duration); + } + + [YarnCommand("flash_white")] + public static IEnumerator FlashWhite(float duration = 0.2f) + { + var panel = UIManager.Instance.GetPanel(); + return panel.FlashWhite(duration); + } + + [YarnCommand("wipe_in")] + public static IEnumerator WipeIn(float duration = 0.6f) + { + var panel = UIManager.Instance.GetPanel(); + return panel.WipeInAsync(duration); + } + + [YarnCommand("wipe_out")] + public static IEnumerator WipeOut(float duration = 0.6f) + { + var panel = UIManager.Instance.GetPanel(); + return panel.WipeOutAsync(duration); + } + + [YarnCommand("shutter_in")] + public static IEnumerator ShutterIn(float duration = 0.6f) + { + var panel = UIManager.Instance.GetPanel(); + return panel.ShutterInAsync(duration); + } + + [YarnCommand("shutter_out")] + public static IEnumerator ShutterOut(float duration = 0.6f) + { + var panel = UIManager.Instance.GetPanel(); + return panel.ShutterOutAsync(duration); + } + [YarnCommand("show_obj")] public static IEnumerator ShowObj(string picName, float duration = 1) { diff --git a/Assets/Scripts/UI/Panel/ScreenTransitionPanel.cs b/Assets/Scripts/UI/Panel/ScreenTransitionPanel.cs new file mode 100644 index 000000000..1db91f226 --- /dev/null +++ b/Assets/Scripts/UI/Panel/ScreenTransitionPanel.cs @@ -0,0 +1,442 @@ +using System.Collections; +using DG.Tweening; +using UnityEngine; +using UnityEngine.UI; + +namespace AibisDream.UI +{ + /// + /// 全局屏幕转场面板。In 决定遮罩颜色和盖入方式,Out 决定揭出方式。 + /// + 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; + } + } +} diff --git a/Assets/Scripts/UI/Panel/ScreenTransitionPanel.cs.meta b/Assets/Scripts/UI/Panel/ScreenTransitionPanel.cs.meta new file mode 100644 index 000000000..df8887856 --- /dev/null +++ b/Assets/Scripts/UI/Panel/ScreenTransitionPanel.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6b517be3c5ca46bdad6d13b94cb1b179 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/UI/UIManager.cs b/Assets/Scripts/UI/UIManager.cs index febd4aee9..62f05d44b 100644 --- a/Assets/Scripts/UI/UIManager.cs +++ b/Assets/Scripts/UI/UIManager.cs @@ -95,6 +95,7 @@ namespace AibisDream // 游戏打开 ShowPanel(); ShowPanel(); + ShowPanel(); HidePanel(); HidePanel();