fix(dreamdoor): 近景门框改大图并修复 closeup 残留

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-25 21:24:56 +08:00
co-authored by Cursor
parent 08594fae10
commit 166b0ac4af
4 changed files with 162 additions and 61 deletions
+145 -52
View File
@@ -7,18 +7,29 @@ using UnityEngine;
namespace AibisDream
{
/// <summary>
/// Day2 门演出。可见门框与门后内容分层如下,勿混用:
/// <list type="bullet">
/// <item><b>可见门框(近景)</b>:只用 SpriteShowcase 大图(白色门框/金色门框)。
/// 缩放/透明度走 set_door_closeup / tween_door_closeup,不要再 show_large_sprite「门框特写」叠一层。</item>
/// <item><b>Door/background</b>HDR 白底或门后内容,从大图透明洞透出。</item>
/// <item><b>Door/Frame</b>:场景里保持 inactive,缩放与近景大图不同;仅作 terminalShapeReference / 贴图同步,不要 SetActive 当第二层门框。</item>
/// <item><b>远景整门立绘</b>hide_dream_door + show_large_sprite「D2S门」,不走本系统 Frame。</item>
/// </list>
/// </summary>
public class DreamDoorSystem : Singleton<DreamDoorSystem>
{
[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("变形为终端开始时并行播放的 TimelineTimelineCenter 名称,如 姐姐/头痛;留空则不播)")]
[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
}
/// <summary>
/// 进入“仔细听”的门近景。首次是白底门,之后直接显示门后的金色海浪。
/// 门近景:大图显示白/金色门框 + Door 内 background/海浪。
/// Yarn 无需再先 show_large_sprite「门框特写」(会被本命令覆盖)。
/// </summary>
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
/// </summary>
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);
}
/// <summary>异步近景;再次调用 set/tween/show_framed 时会取消上一次。</summary>
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();
}
/// <summary>
/// 全白遮罩下调用的瞬时状态切换:隐藏门底门框,仅显示全屏金色海浪。
/// 全白:隐藏门底与大图门框,仅全屏金色海浪。
/// </summary>
public void ShowGlassSeaFullscreen()
{
@@ -282,23 +333,25 @@ namespace AibisDream
doorRoot.gameObject.SetActive(true);
SetBackgroundVisible(false);
SetFrameVisible(false);
HideLargeFrame();
ResetCodeSeaImmediate();
EnsureGlassSeaVisible(glassSeaStateName);
}
/// <summary>
/// 全白遮罩下调用的瞬时状态切换:恢复门底和门框,金色海浪留在门后。
/// 全白时:恢复大图门框,金色海浪留在门后。
/// </summary>
public void ShowGlassSeaFramed()
{
StopCloseupCoroutine();
ResetCodeSeaImmediate();
EnsureDoorBaseVisible(hasSea: true);
ShowLargeFrame(hasSea: true, scale: 1f, alpha: 1f);
EnsureGlassSeaVisible(glassSeaFramedStateName);
}
/// <summary>
/// 全白时切到无门的全屏 CodeSea;状态职责与 ShowGlassSeaFullscreen 一致
/// 全白时:隐藏门底与大图门框,全屏 CodeSea
/// </summary>
public void ShowCodeSeaFullscreen()
{
@@ -308,20 +361,23 @@ namespace AibisDream
var restartPlayback = _presentationPhase != DoorPresentationPhase.CodeSea
|| !_codeSeaPlaybackActive;
SetBackgroundVisible(false);
SetFrameVisible(false);
HideLargeFrame();
ResetGlassSeaImmediate();
EnsureCodeSeaVisible(restartPlayback);
}
/// <summary>
/// 全白时恢复门框CodeSea 保持显示在门内。
/// 全白时恢复大图白色门框(scale=1CodeSea 在门内。
/// 会取消进行中的 tween_door_closeup_async,避免停在 4x 放大。
/// </summary>
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
/// <summary>
/// 打开 Door 根与门后背景;不同时显示场景 Frame(近景门框只走大图)。
/// </summary>
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();
}
/// <summary>可见门框:大图贴白/金色门框并设缩放透明度。</summary>
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();
}
/// <summary>只同步场景 Frame 贴图,不激活(供 morph 参考一致)。</summary>
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;
}
}
/// <summary>
/// 场景 Frame 必须保持 inactive。其 localScale≈0.73,若被激活会叠出一层「缩小门框」。
/// </summary>
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;
@@ -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")]
@@ -30,6 +30,14 @@ namespace AibisDream
private bool IsSmallShowing => _spriteRenderer != null && _spriteRenderer.gameObject.activeSelf;
private bool IsLargeShowing => _largeSpriteRenderer != null && _largeSpriteRenderer.gameObject.activeSelf;
/// <summary>
/// 立即隐藏全屏遮罩(黑底)。门近景透明区域需要透出 Door HDR 白底时调用。
/// </summary>
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);
@@ -1002,7 +1002,6 @@ position: -4,927
<<show_large_sprite "D2S门">>
<<set_global_param dream1Stage 1.6>>
<<else>>
<<show_large_sprite "门框特写" 0>>
<<set_global_param dream1Stage 2>>
<<show_door_listen_view false 0>>
<<set_door_closeup 1 1>>
@@ -1036,7 +1035,6 @@ position: 118,927
---
<<set $doorCheck = true>>
<<set_global_param dream1Stage 2>>
<<show_large_sprite "门框特写" 0>>
<<show_door_listen_view false 0>>
<<set_door_closeup 1 1>>
<<set_volume_weight "BloomEffect" 0.4>>
@@ -1050,7 +1048,7 @@ position: 118,927
<i>你知道穿过这扇门你就无法再回来了。</i> #line:05ac743
<<show_large_sprite "门把手特写" 0>>
->推门 #line:02467b3
<<show_large_sprite "门框特写" 0>>
<<show_door_listen_view false 0>>
<<tween_door_closeup_async 1.2 1 10>>
<i>门的那边传来了一种窸窸窣窣的声音。</i> #line:03e96a2
<i>一种让你感到安全的声音。你听到过它很多次了。</i> #line:08b0cf3
@@ -1080,8 +1078,6 @@ position: 118,927
<<tween_volume_weight_async "BloomEffect" 1 2.2>>
<<tween_volume_brightness_async "BloomEffect" 1 2.2>>
<<wait 2>>
<<show_large_sprite "门框特写" 0>>
<<tween_door_closeup_async 1 1 0>>
<<show_door_listen_view true 0>>
<<tween_door_closeup_async 1 1 1>>
@@ -1177,7 +1173,6 @@ me: 这就是…为什么…它会去找海。<br>这就是为什么总是海…
<<tween_door_closeup_async 1.5 1 20>>
//乌鸦: 她已经意识到了,这不是个梦。…<br>…但她依然在往陷阱中前进。一步一步。 #line:0759979
<<set_door_closeup 1.1 1>>
<<show_large_sprite "门框特写" 0>>
<<show_door_listen_view true 1>>
<<set_volume_weight "BloomEffect" 0.02>>
<<set_volume_brightness "BloomEffect" 0>>
@@ -1342,11 +1337,12 @@ me: 我… #line:04e1fb0
乌鸦: 我已经给出了我的回答。 #line:0c6b3b1
乌鸦: 时间不多了。我们会再见的。 #line:057774f
<<tween_door_closeup_async 4 1 20>>
// CodeSea 展示结束:再次过曝,在全白时恢复白色门框,门内仍保持 CodeSea。
// CodeSea 展示结束:再次过曝全白时取消 4x closeup,恢复 scale=1 的白色门框,门内仍保持 CodeSea。
<<hide_dialog>>
<<tween_volume_weight_async "BloomEffect" 1 2.4>>
<<tween_volume_brightness_async "BloomEffect" 1 2.4>>
<<wait 2.4>>
<<set_door_closeup 1 1>>
<<show_door_code_sea_framed>>
<<set_door_closeup 1 0>>
<<tween_volume_weight_async "BloomEffect" 0 2>>
@@ -1354,7 +1350,6 @@ me: 我… #line:04e1fb0
<<wait 2>>
乌鸦扑腾起翅膀,飞到了天上,很远很远的地方。 #line:0c892f8
<<hide_dream_door>>
<<tween_door_closeup_async 1 1 0>>
<<show_large_sprite "D2S门">>
<<wait 1>>
<<jump 靠近黑暗>>