From 166b0ac4af7cde88f11a52f67d08bc80f790e1d9 Mon Sep 17 00:00:00 2001 From: bottlefish <781230111@qq.com> Date: Sat, 25 Jul 2026 21:24:56 +0800 Subject: [PATCH] =?UTF-8?q?fix(dreamdoor):=20=E8=BF=91=E6=99=AF=E9=97=A8?= =?UTF-8?q?=E6=A1=86=E6=94=B9=E5=A4=A7=E5=9B=BE=E5=B9=B6=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=20closeup=20=E6=AE=8B=E7=95=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- .../SceneManagement/DreamDoorSystem.cs | 197 +++++++++++++----- .../SceneManagement/DreamDoorYarnCommand.cs | 3 +- .../Scripts/SceneManagement/SpriteShowcase.cs | 12 ++ .../Yarn/FP/FP_Day2_sleep/FP_Day2_sleep.yarn | 11 +- 4 files changed, 162 insertions(+), 61 deletions(-) diff --git a/Assets/Scripts/SceneManagement/DreamDoorSystem.cs b/Assets/Scripts/SceneManagement/DreamDoorSystem.cs index 1af15f640..feb3c7f0a 100644 --- a/Assets/Scripts/SceneManagement/DreamDoorSystem.cs +++ b/Assets/Scripts/SceneManagement/DreamDoorSystem.cs @@ -7,18 +7,29 @@ using UnityEngine; namespace AibisDream { + /// + /// Day2 门演出。可见门框与门后内容分层如下,勿混用: + /// + /// 可见门框(近景):只用 SpriteShowcase 大图(白色门框/金色门框)。 + /// 缩放/透明度走 set_door_closeup / tween_door_closeup,不要再 show_large_sprite「门框特写」叠一层。 + /// Door/background:HDR 白底或门后内容,从大图透明洞透出。 + /// Door/Frame:场景里保持 inactive,缩放与近景大图不同;仅作 terminalShapeReference / 贴图同步,不要 SetActive 当第二层门框。 + /// 远景整门立绘:hide_dream_door + show_large_sprite「D2S门」,不走本系统 Frame。 + /// + /// public class DreamDoorSystem : Singleton { [Header("Door Root")] - [Tooltip("Door 根节点。默认仅 background 和 Frame 显示,整个 Door 初始隐藏。show_door 命令激活后显示。")] + [Tooltip("Door 根节点。show_door / listen_view 时激活;内含 background、海浪等,不含「可见近景门框」。")] [SerializeField] private Transform doorRoot; [Header("Door Sprites")] [SerializeField] private SpriteRenderer glassSea; [SerializeField] private SpriteRenderer codeSea; [SerializeField] private SpriteRenderer flashOverlay; - [Tooltip("仅放门的静态 background,不要把 glassSea/codeSea/overlay 拖进来。")] + [Tooltip("门后 HDR 白底等。不要把 glassSea/codeSea/overlay 拖进来。")] [SerializeField] private Renderer[] backgroundRenderers; + [Tooltip("场景 Frame:默认 inactive,勿当作近景可见门框。可与 terminalShapeReference 共用。")] [SerializeField] private Renderer[] frameRenderers; [SerializeField] private Sprite whiteDoorFrameSprite; [SerializeField] private Sprite seaDoorFrameSprite; @@ -48,12 +59,13 @@ namespace AibisDream [SerializeField] private SpriteRenderer terminalShapeReference; [SerializeField] private Vector3 seaCollapseScale = new(0.9f, 0.84f, 1f); [SerializeField] private Vector3 doorSnapScale = new(0.95f, 1.06f, 1f); - [SerializeField] private Ease morphEase = Ease.InOutCubic; + [SerializeField] private Ease morphEase = Ease.OutBack; [SerializeField] private FrameHidePhase frameHidePhase = FrameHidePhase.OnTerminal; [SerializeField] private float doorSnapDuration = 0.2f; [SerializeField] private float backgroundCutDelay = 0.04f; - [SerializeField] private float preMorphHoldDuration = 0.16f; - [SerializeField] private float postMorphBootDelay = 0.12f; + [Tooltip("变形前停顿,稍长一点更有「蓄力→啪」的顿挫。")] + [SerializeField] private float preMorphHoldDuration = 0.22f; + [SerializeField] private float postMorphBootDelay = 0.06f; [SerializeField] private float referenceSnapMorphProgress = 0.08f; [SerializeField] private Vector2 referenceSnapPadding = new(0.18f, 0.18f); [Tooltip("终端变形时 sceneBlackOverlay 淡入目标透明度(背景压暗)")] @@ -63,6 +75,12 @@ namespace AibisDream [SerializeField] private float blackoutFadeOutDuration = 0.12f; [Tooltip("变形为终端开始时并行播放的 Timeline(TimelineCenter 名称,如 姐姐/头痛;留空则不播)")] [SerializeField] private string morphStartTimelineName = "姐姐/Glitch"; + [Tooltip("门框→终端变形时长。偏短配合 OutBack 更容易做出顿挫。")] + [SerializeField, Min(0.05f)] private float morphDuration = 0.45f; + + [Header("Ambient")] + [Tooltip("变门/终端时立刻关掉的场景背景层(如星空)。须在 HideLargeFrame 之前关掉,否则大图一收会穿帮。")] + [SerializeField] private GameObject[] hideOnTerminal; private Transform _codeSeaTransform; private Vector3 _codeSeaOriginalLocalPosition; @@ -78,6 +96,7 @@ namespace AibisDream private string _glassSeaCurrentStateName; private MaterialPropertyBlock _codeSeaPropertyBlock; private bool _codeSeaPlaybackActive; + private Coroutine _closeupCoroutine; private static readonly int WaveStartTimeId = Shader.PropertyToID("_WaveStartTime"); private static readonly int WavePhaseId = Shader.PropertyToID("_WavePhase"); private static readonly int WaveSpeedId = Shader.PropertyToID("_WaveSpeed"); @@ -120,7 +139,8 @@ namespace AibisDream glassSea.gameObject.SetActive(false); } - SetDoorFrameSprite(hasSea: false); + SyncSceneFrameSprite(hasSea: false); + KeepSceneFrameInactive(); if (codeSea != null) { @@ -173,7 +193,8 @@ namespace AibisDream } /// - /// 进入“仔细听”的门近景。首次是白底门,之后直接显示门后的金色海浪。 + /// 门近景:大图显示白/金色门框 + Door 内 background/海浪。 + /// Yarn 无需再先 show_large_sprite「门框特写」(会被本命令覆盖)。 /// public IEnumerator ShowListenView(bool showSea, float duration = 0f) { @@ -181,8 +202,7 @@ namespace AibisDream EnsureDoorBaseVisible(showSea); ResetCodeSeaImmediate(); ResetTerminalImmediate(); - - Day2SleepPresentationController.Instance?.SetLargePresentationImmediate(1f, 1f); + ShowLargeFrame(hasSea: showSea, scale: 1f, alpha: 1f); if (showSea && glassSea != null) { @@ -261,6 +281,7 @@ namespace AibisDream /// public IEnumerator TweenListenCloseup(float scale, float alpha, float duration) { + StopCloseupCoroutine(); var presentation = Day2SleepPresentationController.Instance; if (presentation == null) yield break; @@ -268,13 +289,43 @@ namespace AibisDream yield return presentation.AnimateLargePresentation(scale, alpha, duration); } + /// 异步近景;再次调用 set/tween/show_framed 时会取消上一次。 + public void TweenListenCloseupAsync(float scale, float alpha, float duration) + { + StopCloseupCoroutine(); + if (Day2SleepPresentationController.Instance == null) + return; + + _closeupCoroutine = StartCoroutine(TweenListenCloseupRoutine(scale, alpha, duration)); + } + public void SetListenCloseup(float scale, float alpha) { + StopCloseupCoroutine(); Day2SleepPresentationController.Instance?.SetLargePresentationImmediate(scale, alpha); } + private IEnumerator TweenListenCloseupRoutine(float scale, float alpha, float duration) + { + var presentation = Day2SleepPresentationController.Instance; + if (presentation == null) + yield break; + yield return presentation.AnimateLargePresentation(scale, alpha, duration); + _closeupCoroutine = null; + } + + private void StopCloseupCoroutine() + { + if (_closeupCoroutine != null) + { + StopCoroutine(_closeupCoroutine); + _closeupCoroutine = null; + } + Day2SleepPresentationController.Instance?.StopLargePresentationAnimation(); + } + /// - /// 全白遮罩下调用的瞬时状态切换:隐藏门底和门框,仅显示全屏金色海浪。 + /// 全白时:隐藏门底与大图门框,仅全屏金色海浪。 /// public void ShowGlassSeaFullscreen() { @@ -282,23 +333,25 @@ namespace AibisDream doorRoot.gameObject.SetActive(true); SetBackgroundVisible(false); - SetFrameVisible(false); + HideLargeFrame(); ResetCodeSeaImmediate(); EnsureGlassSeaVisible(glassSeaStateName); } /// - /// 全白遮罩下调用的瞬时状态切换:恢复门底和门框,金色海浪留在门后。 + /// 全白时:恢复大图门框,金色海浪留在门后。 /// public void ShowGlassSeaFramed() { + StopCloseupCoroutine(); ResetCodeSeaImmediate(); EnsureDoorBaseVisible(hasSea: true); + ShowLargeFrame(hasSea: true, scale: 1f, alpha: 1f); EnsureGlassSeaVisible(glassSeaFramedStateName); } /// - /// 全白时切到无门的全屏 CodeSea;状态职责与 ShowGlassSeaFullscreen 一致。 + /// 全白时:隐藏门底与大图门框,全屏 CodeSea。 /// public void ShowCodeSeaFullscreen() { @@ -308,20 +361,23 @@ namespace AibisDream var restartPlayback = _presentationPhase != DoorPresentationPhase.CodeSea || !_codeSeaPlaybackActive; SetBackgroundVisible(false); - SetFrameVisible(false); + HideLargeFrame(); ResetGlassSeaImmediate(); EnsureCodeSeaVisible(restartPlayback); } /// - /// 全白时恢复门框,CodeSea 保持显示在门内。 + /// 全白时:恢复大图白色门框(scale=1),CodeSea 留在门内。 + /// 会取消进行中的 tween_door_closeup_async,避免停在 4x 放大。 /// public void ShowCodeSeaFramed() { var restartPlayback = _presentationPhase != DoorPresentationPhase.CodeSea || !_codeSeaPlaybackActive; + StopCloseupCoroutine(); ResetGlassSeaImmediate(); EnsureDoorBaseVisible(hasSea: false); + ShowLargeFrame(hasSea: false, scale: 1f, alpha: 1f); EnsureCodeSeaFramedVisible(restartPlayback); } @@ -362,7 +418,7 @@ namespace AibisDream _presentationPhase = DoorPresentationPhase.CodeSea; if (frameHidePhase == FrameHidePhase.OnCodeSea) - SetFrameVisible(false); + HideLargeFrame(); var seq = DOTween.Sequence(); seq.Append(codeSea.DOFade(1f, Mathf.Max(0f, duration)).SetEase(Ease.InOutSine)); @@ -408,6 +464,9 @@ namespace AibisDream { StopAllEffects(); + // 先关星空等远景层,再收大图;否则 D2S门 一隐星空会露出来穿帮。 + HideAmbientForTerminal(); + if (terminalUI != null) { terminalUI.ResetVisualState(); @@ -422,7 +481,7 @@ namespace AibisDream } SetBackgroundVisible(false); - SetFrameVisible(false); + HideLargeFrame(); if (glassSea != null) { @@ -454,11 +513,12 @@ namespace AibisDream if (terminalUI != null) { float morph = 0f; + // OutBack 带轻微过冲,配合变短的 morphDuration,落地更有顿挫。 var morphTween = DOTween.To(() => morph, v => { morph = v; terminalUI.MorphProgress = v; - }, 1f, 0.82f).SetEase(morphEase); + }, 1f, Mathf.Max(0.05f, morphDuration)).SetEase(morphEase); if (!string.IsNullOrEmpty(morphStartTimelineName)) TimelineCenter.Instance.PlayTimeline(morphStartTimelineName); @@ -467,6 +527,7 @@ namespace AibisDream sceneBlackOverlay.DOFade(blackoutAlpha, Mathf.Max(0.01f, blackoutFadeInDuration)); yield return morphTween.WaitForCompletion(); + terminalUI.MorphProgress = 1f; } ResetDoorShellScale(); @@ -507,6 +568,7 @@ namespace AibisDream public void StopAllEffects() { + StopCloseupCoroutine(); DOTween.Kill(glassSea); DOTween.Kill(_codeSeaTransform); DOTween.Kill(codeSea); @@ -527,8 +589,8 @@ namespace AibisDream ResetListenTransitionImmediate(); HideDoor(); SetBackgroundVisible(true); - SetFrameVisible(true); - SetDoorFrameSprite(hasSea: false); + SyncSceneFrameSprite(hasSea: false); + KeepSceneFrameInactive(); ResetDoorShellScale(); if (glassSea != null) @@ -566,7 +628,7 @@ namespace AibisDream if (sceneBlackOverlay != null) sceneBlackOverlay.SetAlpha(0); - Day2SleepPresentationController.Instance?.HideLargePresentationImmediate(); + HideLargeFrame(); _presentationPhase = DoorPresentationPhase.Hidden; } @@ -575,17 +637,77 @@ namespace AibisDream #region Presentation State + /// + /// 打开 Door 根与门后背景;不同时显示场景 Frame(近景门框只走大图)。 + /// private void EnsureDoorBaseVisible(bool hasSea = false) { if (doorRoot != null) doorRoot.gameObject.SetActive(true); + // 梦境「遮罩」是纯黑全屏;不关会从门框透明洞透出黑底,盖住 HDR 白光。 + SpriteShowcase.Instance?.HideBackgroundImmediate(); SetBackgroundVisible(true); - SetFrameVisible(true); - SetDoorFrameSprite(hasSea); + KeepSceneFrameInactive(); + SyncSceneFrameSprite(hasSea); ResetDoorShellScale(); } + /// 可见门框:大图贴白/金色门框并设缩放透明度。 + private void ShowLargeFrame(bool hasSea, float scale, float alpha) + { + var targetSprite = hasSea ? seaDoorFrameSprite : whiteDoorFrameSprite; + SyncSceneFrameSprite(hasSea); + var presentation = Day2SleepPresentationController.Instance; + if (presentation == null) return; + if (targetSprite != null) + presentation.SetLargeSpriteImmediate(targetSprite); + presentation.SetLargePresentationImmediate(scale, alpha); + } + + private void HideLargeFrame() + { + Day2SleepPresentationController.Instance?.HideLargePresentationImmediate(); + } + + /// 只同步场景 Frame 贴图,不激活(供 morph 参考一致)。 + private void SyncSceneFrameSprite(bool hasSea) + { + var targetSprite = hasSea ? seaDoorFrameSprite : whiteDoorFrameSprite; + if (targetSprite == null || frameRenderers == null) return; + + foreach (var frameRenderer in frameRenderers) + { + if (frameRenderer is SpriteRenderer spriteRenderer) + spriteRenderer.sprite = targetSprite; + } + } + + /// + /// 场景 Frame 必须保持 inactive。其 localScale≈0.73,若被激活会叠出一层「缩小门框」。 + /// + private void KeepSceneFrameInactive() + { + if (frameRenderers == null) return; + foreach (var frameRenderer in frameRenderers) + { + if (frameRenderer == null) continue; + if (frameRenderer.gameObject.activeSelf) + frameRenderer.gameObject.SetActive(false); + frameRenderer.enabled = true; + } + } + + private void HideAmbientForTerminal() + { + if (hideOnTerminal == null) return; + foreach (var go in hideOnTerminal) + { + if (go != null && go.activeSelf) + go.SetActive(false); + } + } + private void ResetGlassSeaImmediate() { DOTween.Kill(glassSea); @@ -832,35 +954,6 @@ namespace AibisDream #endregion - private void SetFrameVisible(bool visible) - { - if (frameRenderers == null) return; - - foreach (var frameRenderer in frameRenderers) - { - if (frameRenderer != null) - frameRenderer.enabled = visible; - } - } - - private void SetDoorFrameSprite(bool hasSea) - { - var targetSprite = hasSea ? seaDoorFrameSprite : whiteDoorFrameSprite; - if (targetSprite == null) - return; - - if (frameRenderers != null) - { - foreach (var frameRenderer in frameRenderers) - { - if (frameRenderer is SpriteRenderer spriteRenderer) - spriteRenderer.sprite = targetSprite; - } - } - - Day2SleepPresentationController.Instance?.SetLargeSpriteImmediate(targetSprite); - } - private void SetBackgroundVisible(bool visible) { if (backgroundRenderers == null) return; diff --git a/Assets/Scripts/SceneManagement/DreamDoorYarnCommand.cs b/Assets/Scripts/SceneManagement/DreamDoorYarnCommand.cs index 8f9706c4e..752ae5a52 100644 --- a/Assets/Scripts/SceneManagement/DreamDoorYarnCommand.cs +++ b/Assets/Scripts/SceneManagement/DreamDoorYarnCommand.cs @@ -72,7 +72,8 @@ namespace AibisDream { var system = ResolveSystem(); if (system == null) return; - system.StartCoroutine(system.TweenListenCloseup(scale, alpha, duration)); + // 走系统内可取消入口,避免多个 async closeup 叠跑把 scale 卡在放大态。 + system.TweenListenCloseupAsync(scale, alpha, duration); } [YarnCommand("set_door_closeup")] diff --git a/Assets/Scripts/SceneManagement/SpriteShowcase.cs b/Assets/Scripts/SceneManagement/SpriteShowcase.cs index 1fb031eaa..40e7a3936 100644 --- a/Assets/Scripts/SceneManagement/SpriteShowcase.cs +++ b/Assets/Scripts/SceneManagement/SpriteShowcase.cs @@ -30,6 +30,14 @@ namespace AibisDream private bool IsSmallShowing => _spriteRenderer != null && _spriteRenderer.gameObject.activeSelf; private bool IsLargeShowing => _largeSpriteRenderer != null && _largeSpriteRenderer.gameObject.activeSelf; + /// + /// 立即隐藏全屏遮罩(黑底)。门近景透明区域需要透出 Door HDR 白底时调用。 + /// + public void HideBackgroundImmediate() + { + SetBackgroundImmediate(false); + } + public override void OnSingletonInit() { ClearSmallImmediate(); @@ -132,6 +140,10 @@ namespace AibisDream _largeSpriteRenderer.sprite = null; _largeSpriteRenderer.sprite = assigned; + // 门近景 closeup 可能把 large 留在 2x/4x;换图时强制回到 1x。 + Day2SleepPresentationController.Instance?.ResetLargePresentationImmediate(); + _largeSpriteRenderer.gameObject.SetActive(true); + _currentLargePicName = NormalizeDreamPicName(picName); if (CommonUtil.IsZero(duration)) _largeSpriteRenderer.SetAlpha(1f); diff --git a/Assets/Yarn/FP/FP_Day2_sleep/FP_Day2_sleep.yarn b/Assets/Yarn/FP/FP_Day2_sleep/FP_Day2_sleep.yarn index 55d07a868..f36c8ed73 100644 --- a/Assets/Yarn/FP/FP_Day2_sleep/FP_Day2_sleep.yarn +++ b/Assets/Yarn/FP/FP_Day2_sleep/FP_Day2_sleep.yarn @@ -1002,7 +1002,6 @@ position: -4,927 <> <> <> - <> <> <> <> @@ -1036,7 +1035,6 @@ position: 118,927 --- <> <> -<> <> <> <> @@ -1050,7 +1048,7 @@ position: 118,927 你知道穿过这扇门你就无法再回来了。 #line:05ac743 <> ->推门 #line:02467b3 - <> + <> <> 门的那边传来了一种窸窸窣窣的声音。 #line:03e96a2 一种让你感到安全的声音。你听到过它很多次了。 #line:08b0cf3 @@ -1080,8 +1078,6 @@ position: 118,927 <> <> <> - - <> <> <> <> @@ -1177,7 +1173,6 @@ me: 这就是…为什么…它会去找海。
这就是为什么总是海… <> //乌鸦: 她已经意识到了,这不是个梦。…
…但她依然在往陷阱中前进。一步一步。 #line:0759979 <> -<> <> <> <> @@ -1342,11 +1337,12 @@ me: 我… #line:04e1fb0 乌鸦: 我已经给出了我的回答。 #line:0c6b3b1 乌鸦: 时间不多了。我们会再见的。 #line:057774f <> -// CodeSea 展示结束:再次过曝,在全白时恢复白色门框,门内仍保持 CodeSea。 +// CodeSea 展示结束:再次过曝;全白时取消 4x closeup,恢复 scale=1 的白色门框,门内仍保持 CodeSea。 <> <> <> <> +<> <> <> <> @@ -1354,7 +1350,6 @@ me: 我… #line:04e1fb0 <> 乌鸦扑腾起翅膀,飞到了天上,很远很远的地方。 #line:0c892f8 <> -<> <> <> <>