refactor: 拆分D2 Sleep表现并接入快照

This commit is contained in:
2026-07-22 00:50:16 +08:00
parent 6dc0983961
commit aed9998d1a
32 changed files with 1977 additions and 1404 deletions
@@ -8,12 +8,18 @@ namespace AibisDream.SystemEditor.Tests
public sealed class PresentationSnapshotContractTests
{
[Test]
public void PlayToolProvider_UsesStableIdAndRestoreBeforeScreen()
public void PresentationProviders_UseStableIdsAndRestoreInDependencyOrder()
{
var showcase = new ShowcaseSnapshotProvider();
var day2Sleep = new Day2SleepPresentationSnapshotProvider();
var playTool = new PlayToolSnapshotProvider();
var screen = new ScreenSnapshotProvider();
Assert.That(showcase.SaveId, Is.EqualTo("showcase"));
Assert.That(day2Sleep.SaveId, Is.EqualTo("day2SleepPresentation"));
Assert.That(playTool.SaveId, Is.EqualTo("playTool"));
Assert.That(showcase.RestoreOrder, Is.LessThan(day2Sleep.RestoreOrder));
Assert.That(day2Sleep.RestoreOrder, Is.LessThan(playTool.RestoreOrder));
Assert.That(playTool.RestoreOrder, Is.LessThan(screen.RestoreOrder));
}
@@ -41,6 +47,42 @@ namespace AibisDream.SystemEditor.Tests
Assert.That(restored.schemaVersion, Is.EqualTo(1));
}
[Test]
public void ShowcaseAndDay2SleepSections_RoundTripSemanticState()
{
var source = new SaveSnapshot();
source.sections[SnapshotProviderIds.Showcase] = new ShowcaseSnapshotDto
{
displayMode = ShowcaseDisplayMode.Large.ToString(),
picName = "D2S星图",
isBackgroundVisible = true
};
source.sections[SnapshotProviderIds.Day2SleepPresentation] =
new Day2SleepPresentationSnapshotDto
{
mode = Day2SleepPresentationMode.LargeEffects.ToString(),
largeBlurAmount = 0.72f,
largeBlurSize = 0.015f,
largeScaleMultiplier = 1.1f,
largeAlpha = 0.4f
};
var json = JsonConvert.SerializeObject(source);
var restored = JsonConvert.DeserializeObject<SaveSnapshot>(json);
var showcase = ((JObject)restored.sections[SnapshotProviderIds.Showcase])
.ToObject<ShowcaseSnapshotDto>();
var day2Sleep = ((JObject)restored.sections[SnapshotProviderIds.Day2SleepPresentation])
.ToObject<Day2SleepPresentationSnapshotDto>();
Assert.That(showcase.displayMode, Is.EqualTo("Large"));
Assert.That(showcase.picName, Is.EqualTo("D2S星图"));
Assert.That(showcase.isBackgroundVisible, Is.True);
Assert.That(day2Sleep.mode, Is.EqualTo("LargeEffects"));
Assert.That(day2Sleep.largeBlurAmount, Is.EqualTo(0.72f));
Assert.That(day2Sleep.largeScaleMultiplier, Is.EqualTo(1.1f));
Assert.That(day2Sleep.largeAlpha, Is.EqualTo(0.4f));
}
[Test]
public void OldSnapshotWithoutPresentationSections_RemainsCompatible()
{
@@ -48,6 +90,8 @@ namespace AibisDream.SystemEditor.Tests
"{\"schemaVersion\":1,\"sections\":{}}");
Assert.That(restored.schemaVersion, Is.EqualTo(SaveSnapshotSchema.CurrentVersion));
Assert.That(restored.sections.ContainsKey(SnapshotProviderIds.Showcase), Is.False);
Assert.That(restored.sections.ContainsKey(SnapshotProviderIds.Day2SleepPresentation), Is.False);
Assert.That(restored.sections.ContainsKey(SnapshotProviderIds.PlayTool), Is.False);
}
}
+17 -1
View File
@@ -1188,6 +1188,7 @@ GameObject:
m_Component:
- component: {fileID: 861686055}
- component: {fileID: 861686057}
- component: {fileID: 861686058}
m_Layer: 0
m_Name: Timeline Kit
m_TagString: Untagged
@@ -1232,7 +1233,22 @@ MonoBehaviour:
_spriteRenderer: {fileID: 218986884}
_backgroundSpriteRenderer: {fileID: 969796818}
_largeSpriteRenderer: {fileID: 762853994}
_kiteFocusMaterial: {fileID: 2100000, guid: 78265ded39f017d45a2afc117effab89, type: 2}
--- !u!114 &861686058
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 861686054}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 9f771105a4b347ffbe2dc7da5acba320, type: 3}
m_Name:
m_EditorClassIdentifier:
_showcase: {fileID: 861686057}
_smallRenderer: {fileID: 218986884}
_largeRenderer: {fileID: 762853994}
_focusMaterial: {fileID: 2100000, guid: 78265ded39f017d45a2afc117effab89, type: 2}
_kiteWindSettings:
calmFloatAmp: 0.53
calmFloatFreq: 0.065
@@ -415,77 +415,6 @@ namespace AibisDream
yield return SpriteShowcase.Instance.ShowObj(picName, duration);
}
[YarnCommand("show_sprite_motion")]
public static IEnumerator ShowSpriteMotion(string picName, string motionProfile, float duration = 0.35f)
{
var showcase = SpriteShowcase.Instance;
if (showcase == null) yield break;
yield return showcase.ShowObjWithMotion(picName, motionProfile, duration);
}
[YarnCommand("sprite_motion")]
public static IEnumerator SpriteMotion(string motionProfile, float leadDuration = 0.18f)
{
var showcase = SpriteShowcase.Instance;
if (showcase == null) yield break;
showcase.PlayCurrentMotion(motionProfile);
if (leadDuration > 0f)
{
yield return new WaitForSeconds(leadDuration);
}
}
[YarnCommand("kite_show")]
public static IEnumerator KiteShow(float height = 0, int wind = 0, float duration = 0.35f)
{
var showcase = SpriteShowcase.Instance;
if (showcase == null) yield break;
yield return showcase.ShowKite(height, wind, duration);
}
[YarnCommand("kite_state")]
public static IEnumerator KiteState(float height, int wind, float duration = 0.9f, float leadDuration = 0.35f)
{
var showcase = SpriteShowcase.Instance;
if (showcase == null) yield break;
showcase.SetKiteState(height, wind, duration);
if (leadDuration > 0f)
{
yield return new WaitForSeconds(Mathf.Min(leadDuration, duration));
}
}
[YarnCommand("kite_wind")]
public static void KiteWind(int wind, float turbulence = -1f)
{
var showcase = SpriteShowcase.Instance;
if (showcase == null) return;
showcase.SetKiteWind(wind, turbulence);
}
[YarnCommand("kite_accent")]
public static IEnumerator KiteAccent(string profile, float leadDuration = 0.18f)
{
var showcase = SpriteShowcase.Instance;
if (showcase == null) yield break;
showcase.PlayKiteAccent(profile);
if (leadDuration > 0f)
{
yield return new WaitForSeconds(leadDuration);
}
}
[YarnCommand("kite_hide")]
public static IEnumerator KiteHide(float duration = 1)
{
var showcase = SpriteShowcase.Instance;
if (showcase == null) yield break;
yield return showcase.HideKite(duration);
}
[YarnCommand("hide_sprite")]
public static IEnumerator HideSprite(float duration = 1)
{
@@ -522,47 +451,6 @@ namespace AibisDream
yield return SpriteShowcase.Instance.ShowLargeObj(picName, duration);
}
[YarnCommand("show_large_sprite_animation")]
public static IEnumerator ShowLargeSpriteAnimation(
string picNamePrefix,
int frameCount,
float secondsPerFrame = 0.2f,
float duration = 1f)
{
yield return SpriteShowcase.Instance.ShowLargeSpriteAnimation(
picNamePrefix,
frameCount,
secondsPerFrame,
duration);
}
[YarnCommand("set_large_sprite_blur")]
public static void SetLargeSpriteBlur(float amount, float blurSize = 0.012f)
{
SpriteShowcase.Instance.SetLargeSpriteBlur(amount, blurSize);
}
[YarnCommand("tween_large_sprite_blur")]
public static IEnumerator TweenLargeSpriteBlur(
float targetAmount,
float duration,
float blurSize = 0.012f)
{
yield return SpriteShowcase.Instance.TweenLargeSpriteBlur(targetAmount, duration, blurSize);
}
[YarnCommand("tween_large_sprite_blur_async")]
public static void TweenLargeSpriteBlurAsync(
float targetAmount,
float duration,
float blurSize = 0.012f)
{
if (DialogController.Instance == null) return;
DialogController.Instance.StartCoroutine(
SpriteShowcase.Instance.TweenLargeSpriteBlur(targetAmount, duration, blurSize));
}
[YarnCommand("hide_large_sprite")]
public static IEnumerator HideLargeSprite(float duration = 1)
{
+1
View File
@@ -708,6 +708,7 @@ namespace AibisDream
{
ScreenEffectManager.Instance?.ResetSaturation();
UIManager.Instance.GetPanel<PlayToolPanel>()?.ClearPresentationEffects();
Day2SleepPresentationController.Instance?.ClearPresentationEffects();
SpriteShowcase.Instance?.ClearPresentationEffects();
CameraKit.Instance?.ResetCamera();
}
@@ -0,0 +1,35 @@
using System.Collections;
namespace AibisDream.SaveSystem
{
/// <summary>sections["day2SleepPresentation"]D2 Sleep 动态表现语义终态。</summary>
public sealed class Day2SleepPresentationSnapshotProvider : IAsyncSnapshotProvider
{
public string SaveId => SnapshotProviderIds.Day2SleepPresentation;
public int RestoreOrder => 70;
public object Capture()
{
var controller = Day2SleepPresentationController.Instance;
return controller != null && controller.HasPersistableState
? controller.CaptureSnapshot()
: null;
}
public IEnumerator RestoreAsync(object dto, SnapshotRestoreContext context)
{
if (dto is not Day2SleepPresentationSnapshotDto snapshot)
{
context.Warn("[Day2SleepPresentationSnapshotProvider] DTO 类型不匹配,跳过还原。");
yield break;
}
var controller = Day2SleepPresentationController.Instance;
if (controller == null)
{
context.Warn("[Day2SleepPresentationSnapshotProvider] 控制器未初始化,跳过还原。");
yield break;
}
yield return controller.RestoreSnapshotAsync(snapshot, context.Warn);
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 8f80937eaaba42da9baa4a2d8e73ff57
@@ -3,11 +3,11 @@ using AibisDream.UI;
namespace AibisDream.SaveSystem
{
/// <summary>sections["playTool"]:物品图与全屏图。RestoreOrder=70。</summary>
/// <summary>sections["playTool"]:物品图与全屏图。RestoreOrder=71。</summary>
public sealed class PlayToolSnapshotProvider : IAsyncSnapshotProvider
{
public string SaveId => SnapshotProviderIds.PlayTool;
public int RestoreOrder => 70;
public int RestoreOrder => 71;
public object Capture()
{
@@ -0,0 +1,34 @@
using System.Collections;
namespace AibisDream.SaveSystem
{
/// <summary>sections["showcase"]:梦境通用静态图片;先于场景专属表现恢复。</summary>
public sealed class ShowcaseSnapshotProvider : IAsyncSnapshotProvider
{
public string SaveId => SnapshotProviderIds.Showcase;
public int RestoreOrder => 69;
public object Capture()
{
return SpriteShowcase.Instance != null
? SpriteShowcase.Instance.CaptureSnapshot()
: null;
}
public IEnumerator RestoreAsync(object dto, SnapshotRestoreContext context)
{
if (dto is not ShowcaseSnapshotDto snapshot)
{
context.Warn("[ShowcaseSnapshotProvider] DTO 类型不匹配,跳过还原。");
yield break;
}
var showcase = SpriteShowcase.Instance;
if (showcase == null)
{
context.Warn("[ShowcaseSnapshotProvider] SpriteShowcase 未初始化,跳过还原。");
yield break;
}
yield return showcase.RestoreSnapshotAsync(snapshot, context.Warn);
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 83cccdfaca2a42a89ed515c16c6adfad
+5 -4
View File
@@ -1,4 +1,4 @@
# SaveSystem 存档快照层
# SaveSystem 存档快照层
> 当前代码包含 P1 快照层、P2 槽位/落盘基础版、P3 可存点判定基础版、P4 读档编排基础版。设计文档见 [`Docs/存档系统设计方案.md`](../../../Docs/存档系统设计方案.md)**§4.1 / D6** 描述读档编排模型)。
@@ -101,7 +101,8 @@ SaveSystem/
├── FixSnapshotProvider.cs order 65
├── FixPanelSnapshotProvider.cs order 66
├── ShowcaseSnapshotProvider.cs order 69
├── PlayToolSnapshotProvider.cs order 70
├── Day2SleepPresentationSnapshotProvider.cs order 70
├── PlayToolSnapshotProvider.cs order 71
└── ScreenSnapshotProvider.cs order 80
```
@@ -135,7 +136,7 @@ Phase 0 Yarn 变量(sync
Phase 1 场景加载(BarrierLoadSceneAsync)← 框架直管,不走 Provider
Phase 2 env / actor / audio / timeline / fix / showcase / playTool / screenProvider 按 RestoreOrder
Phase 2 env / actor / audio / timeline / fix / showcase / day2SleepPresentation / playTool / screenProvider 按 RestoreOrder
└─ timeline`Stopped` = untouched(从未 Evaluate),读档仅还原 `isActive`,不 Reset/Evaluate
Phase 2 可选 Barrier(如 Timeline Addressable 须显式等待)
@@ -202,7 +203,7 @@ Fix 场景各 State 的 `Enter()` 通常包含相机过渡、Timeline 播放、F
| `bodyModule` | `BodyModuleSnapshotDto` | 67 | 插线模块物理态 |
| `eye` | `EyeSnapshotDto` | 68 | Eye 叙事阶段 |
**还原顺序**punchTape → fixcue)→ fixPanel → bodyModule → eye → showcase → playTool → screen。`fixPanel` 须在 `fix` 之后,以覆盖 Cue `EnterImmediate` 中的 `ResetPlug`
**还原顺序**punchTape → fixcue)→ fixPanel → bodyModule → eye → showcase → day2SleepPresentation → playTool → screen。`fixPanel` 须在 `fix` 之后,以覆盖 Cue `EnterImmediate` 中的 `ResetPlug`;D2 动态表现须在通用 Showcase 图片之后恢复,才能叠加虚焦、缩放或场景专属动画
**维修场景门控**`FixSceneSnapshotHelper`):上述 section 仅在 `FixSystemCenter.Instance != null` 时 Capture/Restore。
+47
View File
@@ -190,6 +190,53 @@ namespace AibisDream.SaveSystem
public string fullScreenPicName;
}
public enum ShowcaseDisplayMode
{
None,
Small,
Large
}
/// <summary>sections["showcase"]:梦境通用静态图片与背景终态。</summary>
[Serializable]
public class ShowcaseSnapshotDto
{
public string displayMode;
public string picName;
public bool isBackgroundVisible;
}
public enum Day2SleepPresentationMode
{
None,
SmallMotion,
Kite,
LargeEffects
}
/// <summary>sections["day2SleepPresentation"]D2 Sleep 动态表现语义终态。</summary>
[Serializable]
public class Day2SleepPresentationSnapshotDto
{
public string mode;
public string picName;
public string motionProfile;
public float kiteHeight;
public int kiteWind;
public float kiteTurbulence;
public bool largeAnimationActive;
public string largeAnimationPrefix;
public int largeAnimationFrameCount;
public float largeAnimationSecondsPerFrame;
public float largeBlurAmount;
public float largeBlurSize;
public float largeScaleMultiplier = 1f;
public float largeAlpha = 1f;
}
/// <summary>sections["subway"]:地铁场景宏观状态(SubwayCenter / OutSideState)。</summary>
[Serializable]
public class SubwaySnapshotDto
@@ -16,6 +16,8 @@ namespace AibisDream.SaveSystem
public const string Timeline = "timeline";
public const string Screen = "screen";
public const string PlayTool = "playTool";
public const string Showcase = "showcase";
public const string Day2SleepPresentation = "day2SleepPresentation";
public const string Subway = "subway";
public const string PunchTape = "punchTape";
public const string Fix = "fix";
@@ -30,7 +32,7 @@ namespace AibisDream.SaveSystem
/// </summary>
public static readonly string[] RequiredForCapture =
{
Environment, Actor, Audio, Timeline, Subway, Fix, PlayTool, Screen
Environment, Actor, Audio, Timeline, Subway, Fix, Showcase, PlayTool, Screen
};
}
@@ -60,6 +62,8 @@ namespace AibisDream.SaveSystem
Register(new FixPanelSnapshotProvider());
Register(new BodyModuleSnapshotProvider());
Register(new EyeSnapshotProvider());
Register(new ShowcaseSnapshotProvider());
Register(new Day2SleepPresentationSnapshotProvider());
Register(new PlayToolSnapshotProvider());
Register(new ScreenSnapshotProvider());
}
@@ -257,6 +257,8 @@ namespace AibisDream.SaveSystem
SnapshotProviderIds.FixPanel => jobj.ToObject<FixPanelSnapshotDto>(),
SnapshotProviderIds.BodyModule => jobj.ToObject<BodyModuleSnapshotDto>(),
SnapshotProviderIds.Eye => jobj.ToObject<EyeSnapshotDto>(),
SnapshotProviderIds.Showcase => jobj.ToObject<ShowcaseSnapshotDto>(),
SnapshotProviderIds.Day2SleepPresentation => jobj.ToObject<Day2SleepPresentationSnapshotDto>(),
SnapshotProviderIds.PlayTool => jobj.ToObject<PlayToolSnapshotDto>(),
SnapshotProviderIds.Screen => jobj.ToObject<ScreenSnapshotDto>(),
_ => jobj
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: cb05144af0734df89dd21ed3c9db9f13
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,269 @@
using System;
using System.Collections;
using AibisDream.Kit;
using AibisDream.SaveSystem;
using UnityEngine;
namespace AibisDream
{
/// <summary>D2 Sleep 专属动态表现门面;普通静态图片仍由 SpriteShowcase 管理。</summary>
public sealed class Day2SleepPresentationController : Singleton<Day2SleepPresentationController>
{
[Header("通用展示引用")]
[SerializeField] private SpriteShowcase _showcase;
[SerializeField] private SpriteRenderer _smallRenderer;
[SerializeField] private SpriteRenderer _largeRenderer;
[Header("风筝 / 对焦材质")]
[SerializeField] private Material _focusMaterial;
[SerializeField] private KiteWindSettings _kiteWindSettings = new();
[SerializeField] private KiteCameraSettings _kiteCameraSettings = new();
[SerializeField] private KiteFocusSettings _kiteFocusSettings = new();
private ReelMotionPresenter _reel;
private LargeSpriteEffectsPresenter _largeEffects;
private KitePresentation _kite;
private Day2SleepPresentationMode _mode;
public bool HasPersistableState => _mode != Day2SleepPresentationMode.None;
public override void OnSingletonInit()
{
_showcase ??= SpriteShowcase.Instance;
_reel = new ReelMotionPresenter(_smallRenderer);
_largeEffects = new LargeSpriteEffectsPresenter(this, _largeRenderer, _focusMaterial);
_kite = new KitePresentation(
_smallRenderer,
_focusMaterial,
_kiteWindSettings,
_kiteCameraSettings,
_kiteFocusSettings);
if (_showcase != null)
_showcase.PresentationChanging += HandleGenericPresentationChanging;
ClearPresentationEffects();
}
private void OnDisable()
{
ClearPresentationEffects();
}
public override void OnSingletonDestroy()
{
if (_showcase != null)
_showcase.PresentationChanging -= HandleGenericPresentationChanging;
ClearPresentationEffects();
base.OnSingletonDestroy();
}
public IEnumerator ShowSpriteMotion(string picName, string motionProfile, float duration = 0.35f)
{
yield return PrepareExclusiveMode();
yield return _reel.Show(picName, motionProfile);
if (_reel.IsShowing)
_mode = Day2SleepPresentationMode.SmallMotion;
}
public void PlayCurrentMotion(string motionProfile)
{
if (_mode != Day2SleepPresentationMode.SmallMotion) return;
_reel.PlayCurrentMotion(motionProfile);
}
public IEnumerator ShowKite(float height, int wind, float duration = 0.35f)
{
if (_mode != Day2SleepPresentationMode.Kite)
yield return PrepareExclusiveMode();
yield return _kite.Show(height, wind, duration);
if (_kite.IsShowing)
_mode = Day2SleepPresentationMode.Kite;
}
public void SetKiteState(float height, int wind, float duration = 0.9f)
{
_kite.SetState(height, wind, duration);
}
public void SetKiteWind(int wind, float turbulence = -1f)
{
_kite.SetWind(wind, turbulence);
}
public void PlayKiteAccent(string profile)
{
_kite.PlayAccent(profile);
}
public IEnumerator HideKite(float duration = 1f)
{
yield return _kite.Hide(duration);
if (_mode == Day2SleepPresentationMode.Kite)
_mode = Day2SleepPresentationMode.None;
}
public IEnumerator ShowLargeSpriteAnimation(
string picNamePrefix,
int frameCount,
float secondsPerFrame = 0.2f,
float duration = 1f)
{
yield return PrepareExclusiveMode();
yield return _largeEffects.ShowAnimation(
picNamePrefix,
frameCount,
secondsPerFrame,
duration);
if (_largeEffects.IsAnimationPlaying)
_mode = Day2SleepPresentationMode.LargeEffects;
}
public void SetLargeSpriteBlur(float amount, float blurSize = 0.012f)
{
_largeEffects.SetBlur(amount, blurSize);
MarkLargeEffectsActive();
}
public IEnumerator TweenLargeSpriteBlur(
float targetAmount,
float duration,
float blurSize = 0.012f)
{
MarkLargeEffectsActive();
yield return _largeEffects.TweenBlur(targetAmount, duration, blurSize);
}
public IEnumerator AnimateLargePresentation(float scale, float alpha, float duration)
{
MarkLargeEffectsActive();
yield return _largeEffects.AnimatePresentation(scale, alpha, duration);
}
public void SetLargePresentationImmediate(float scale, float alpha)
{
_largeEffects.ApplyPresentationImmediate(scale, alpha);
MarkLargeEffectsActive();
}
public void ResetLargePresentationImmediate()
{
_largeEffects.ResetPresentationImmediate();
}
public void StopLargePresentationAnimation()
{
_largeEffects.StopPresentationAnimation();
}
public void HideLargePresentationImmediate()
{
_largeEffects.HidePresentationImmediate();
if (_mode == Day2SleepPresentationMode.LargeEffects)
_mode = Day2SleepPresentationMode.None;
}
public void SetLargeSpriteImmediate(Sprite sprite)
{
_largeEffects.SetSpriteImmediate(sprite);
}
public void ClearPresentationEffects()
{
_reel?.ClearImmediate();
_kite?.ClearImmediate();
_largeEffects?.ResetEffectsImmediate();
_mode = Day2SleepPresentationMode.None;
}
public Day2SleepPresentationSnapshotDto CaptureSnapshot()
{
var snapshot = new Day2SleepPresentationSnapshotDto
{
mode = _mode.ToString()
};
switch (_mode)
{
case Day2SleepPresentationMode.SmallMotion:
snapshot.picName = _reel.CurrentPicName;
snapshot.motionProfile = _reel.CurrentMotionProfile;
break;
case Day2SleepPresentationMode.Kite:
snapshot.kiteHeight = _kite.TargetHeight;
snapshot.kiteWind = _kite.CurrentWind;
snapshot.kiteTurbulence = _kite.Turbulence;
break;
case Day2SleepPresentationMode.LargeEffects:
snapshot.largeAnimationActive = _largeEffects.IsAnimationPlaying;
snapshot.largeAnimationPrefix = _largeEffects.AnimationPrefix;
snapshot.largeAnimationFrameCount = _largeEffects.AnimationFrameCount;
snapshot.largeAnimationSecondsPerFrame = _largeEffects.AnimationSecondsPerFrame;
snapshot.largeBlurAmount = _largeEffects.TargetBlurAmount;
snapshot.largeBlurSize = _largeEffects.TargetBlurSize;
snapshot.largeScaleMultiplier = _largeEffects.TargetScaleMultiplier;
snapshot.largeAlpha = _largeEffects.TargetAlpha;
break;
}
return snapshot;
}
public IEnumerator RestoreSnapshotAsync(
Day2SleepPresentationSnapshotDto snapshot,
Action<string> warning = null)
{
ClearPresentationEffects();
if (snapshot == null) yield break;
if (!Enum.TryParse(snapshot.mode, true, out Day2SleepPresentationMode mode))
{
warning?.Invoke($"[Day2SleepPresentation] Unknown mode: {snapshot.mode}");
yield break;
}
switch (mode)
{
case Day2SleepPresentationMode.SmallMotion:
if (!string.IsNullOrWhiteSpace(snapshot.picName))
yield return ShowSpriteMotion(snapshot.picName, snapshot.motionProfile, 0f);
break;
case Day2SleepPresentationMode.Kite:
yield return ShowKite(snapshot.kiteHeight, snapshot.kiteWind, 0f);
SetKiteWind(snapshot.kiteWind, snapshot.kiteTurbulence);
break;
case Day2SleepPresentationMode.LargeEffects:
if (snapshot.largeAnimationActive
&& !string.IsNullOrWhiteSpace(snapshot.largeAnimationPrefix)
&& snapshot.largeAnimationFrameCount > 0)
{
yield return ShowLargeSpriteAnimation(
snapshot.largeAnimationPrefix,
snapshot.largeAnimationFrameCount,
snapshot.largeAnimationSecondsPerFrame,
0f);
}
_largeEffects.SetBlur(snapshot.largeBlurAmount, snapshot.largeBlurSize);
_largeEffects.ApplyPresentationImmediate(
snapshot.largeScaleMultiplier <= 0f ? 1f : snapshot.largeScaleMultiplier,
snapshot.largeAlpha);
_mode = Day2SleepPresentationMode.LargeEffects;
break;
}
}
private IEnumerator PrepareExclusiveMode()
{
if (_showcase != null)
yield return _showcase.HideAll(0f);
ClearPresentationEffects();
}
private void HandleGenericPresentationChanging()
{
ClearPresentationEffects();
}
private void MarkLargeEffectsActive()
{
if (_largeRenderer != null && _largeRenderer.gameObject.activeSelf)
_mode = Day2SleepPresentationMode.LargeEffects;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 9f771105a4b347ffbe2dc7da5acba320
@@ -0,0 +1,69 @@
using System;
using System.Collections;
using AibisDream.Framework;
using UnityEngine;
using UnityEngine.ResourceManagement.AsyncOperations;
namespace AibisDream
{
internal static class Day2SleepSpriteLoader
{
private const string DreamSpritePath = "Sprite/梦境/{0}.png";
public static string NormalizeName(string picName)
{
if (string.IsNullOrEmpty(picName)) return picName;
return picName.EndsWith(".png", StringComparison.OrdinalIgnoreCase)
? picName.Substring(0, picName.Length - 4)
: picName;
}
public static IEnumerator LoadInto(
SpriteRenderer target,
string picName,
Action<bool> completed = null,
Action<string> warning = null)
{
if (target == null)
{
completed?.Invoke(false);
yield break;
}
var key = string.Format(DreamSpritePath, NormalizeName(picName));
var handle = ResourceSystem.LoadAsync<Sprite>(key);
yield return handle;
if (handle.Status == AsyncOperationStatus.Succeeded && handle.Result != null)
{
target.sprite = handle.Result;
completed?.Invoke(true);
yield break;
}
var message = $"[Day2SleepPresentation] Failed to load sprite: {key}";
Debug.LogError(message);
warning?.Invoke(message);
completed?.Invoke(false);
}
public static IEnumerator Load(
string picName,
Action<Sprite> completed,
Action<string> warning = null)
{
var key = string.Format(DreamSpritePath, NormalizeName(picName));
var handle = ResourceSystem.LoadAsync<Sprite>(key);
yield return handle;
if (handle.Status == AsyncOperationStatus.Succeeded && handle.Result != null)
{
completed?.Invoke(handle.Result);
yield break;
}
var message = $"[Day2SleepPresentation] Failed to load sprite: {key}";
Debug.LogError(message);
warning?.Invoke(message);
completed?.Invoke(null);
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 41ba6bf8f06f44a0870d6c72a2ebeb39
@@ -0,0 +1,131 @@
using System.Collections;
using UnityEngine;
using Yarn.Unity;
namespace AibisDream
{
public static class Day2SleepYarnCommand
{
private static Day2SleepPresentationController Resolve()
{
if (Day2SleepPresentationController.Instance != null)
return Day2SleepPresentationController.Instance;
var controller = Object.FindObjectOfType<Day2SleepPresentationController>(true);
if (controller == null)
Debug.LogError("[Day2SleepYarnCommand] 当前场景缺少 Day2SleepPresentationController。");
return controller;
}
[YarnCommand("show_sprite_motion")]
public static IEnumerator ShowSpriteMotion(
string picName,
string motionProfile,
float duration = 0.35f)
{
var controller = Resolve();
if (controller == null) yield break;
yield return controller.ShowSpriteMotion(picName, motionProfile, duration);
}
[YarnCommand("sprite_motion")]
public static IEnumerator SpriteMotion(string motionProfile, float leadDuration = 0.18f)
{
var controller = Resolve();
if (controller == null) yield break;
controller.PlayCurrentMotion(motionProfile);
if (leadDuration > 0f)
yield return new WaitForSeconds(leadDuration);
}
[YarnCommand("kite_show")]
public static IEnumerator KiteShow(float height = 0f, int wind = 0, float duration = 0.35f)
{
var controller = Resolve();
if (controller == null) yield break;
yield return controller.ShowKite(height, wind, duration);
}
[YarnCommand("kite_state")]
public static IEnumerator KiteState(
float height,
int wind,
float duration = 0.9f,
float leadDuration = 0.35f)
{
var controller = Resolve();
if (controller == null) yield break;
controller.SetKiteState(height, wind, duration);
if (leadDuration > 0f)
yield return new WaitForSeconds(Mathf.Min(leadDuration, duration));
}
[YarnCommand("kite_wind")]
public static void KiteWind(int wind, float turbulence = -1f)
{
Resolve()?.SetKiteWind(wind, turbulence);
}
[YarnCommand("kite_accent")]
public static IEnumerator KiteAccent(string profile, float leadDuration = 0.18f)
{
var controller = Resolve();
if (controller == null) yield break;
controller.PlayKiteAccent(profile);
if (leadDuration > 0f)
yield return new WaitForSeconds(leadDuration);
}
[YarnCommand("kite_hide")]
public static IEnumerator KiteHide(float duration = 1f)
{
var controller = Resolve();
if (controller == null) yield break;
yield return controller.HideKite(duration);
}
[YarnCommand("show_large_sprite_animation")]
public static IEnumerator ShowLargeSpriteAnimation(
string picNamePrefix,
int frameCount,
float secondsPerFrame = 0.2f,
float duration = 1f)
{
var controller = Resolve();
if (controller == null) yield break;
yield return controller.ShowLargeSpriteAnimation(
picNamePrefix,
frameCount,
secondsPerFrame,
duration);
}
[YarnCommand("set_large_sprite_blur")]
public static void SetLargeSpriteBlur(float amount, float blurSize = 0.012f)
{
Resolve()?.SetLargeSpriteBlur(amount, blurSize);
}
[YarnCommand("tween_large_sprite_blur")]
public static IEnumerator TweenLargeSpriteBlur(
float targetAmount,
float duration,
float blurSize = 0.012f)
{
var controller = Resolve();
if (controller == null) yield break;
yield return controller.TweenLargeSpriteBlur(targetAmount, duration, blurSize);
}
[YarnCommand("tween_large_sprite_blur_async")]
public static void TweenLargeSpriteBlurAsync(
float targetAmount,
float duration,
float blurSize = 0.012f)
{
var controller = Resolve();
if (controller == null) return;
controller.StartCoroutine(
controller.TweenLargeSpriteBlur(targetAmount, duration, blurSize));
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 60df707002da4e30b73897e04b63d911
@@ -0,0 +1,341 @@
using System.Collections;
using AibisDream.Utility;
using DG.Tweening;
using UnityEngine;
namespace AibisDream
{
internal sealed class KitePresentation
{
private const string KiteSpritePicName = "D2S风筝1_风筝层";
private const string KiteBasePicName = "D2S风筝1_底图";
private const float KiteMaxHeight = 13f;
private readonly SpriteRenderer _referenceRenderer;
private readonly Vector3 _baseLocalPosition;
private readonly Quaternion _baseLocalRotation;
private readonly Vector3 _baseLocalScale;
private readonly KiteWindSettings _windSettings;
private readonly KiteCameraSettings _cameraSettings;
private readonly KiteFocusSettings _focusSettings;
private Material _focusMaterial;
private Transform _cameraNode;
private Transform _rootPivot;
private Transform _swayPivot;
private SpriteRenderer _kiteRenderer;
private SpriteRenderer _baseRenderer;
private SpriteMask _mask;
private KiteRigDriver _driver;
private Sequence _accentSequence;
private bool _kiteSpriteLoaded;
private bool _baseSpriteLoaded;
public bool IsShowing => _cameraNode != null && _cameraNode.gameObject.activeSelf;
public float TargetHeight => _driver != null ? _driver.TargetHeight : 0f;
public int CurrentWind => _driver != null ? _driver.CurrentWind : 0;
public float Turbulence => _driver != null ? _driver.Turbulence : 0f;
public KitePresentation(
SpriteRenderer referenceRenderer,
Material focusMaterial,
KiteWindSettings windSettings,
KiteCameraSettings cameraSettings,
KiteFocusSettings focusSettings)
{
_referenceRenderer = referenceRenderer;
_focusMaterial = focusMaterial;
_windSettings = windSettings ?? new KiteWindSettings();
_cameraSettings = cameraSettings ?? new KiteCameraSettings();
_focusSettings = focusSettings ?? new KiteFocusSettings();
if (_referenceRenderer == null) return;
var transform = _referenceRenderer.transform;
_baseLocalPosition = transform.localPosition;
_baseLocalRotation = transform.localRotation;
_baseLocalScale = transform.localScale;
}
public IEnumerator Show(float height, int wind, float duration = 0.35f)
{
EnsureRig();
if (_kiteRenderer == null || _driver == null) yield break;
height = Mathf.Clamp(height, 0f, KiteMaxHeight);
if (IsShowing)
{
SetState(height, wind, duration);
yield break;
}
KillTweens();
_cameraNode.gameObject.SetActive(true);
_baseRenderer.gameObject.SetActive(true);
if (!_kiteSpriteLoaded)
{
var loaded = false;
yield return Day2SleepSpriteLoader.LoadInto(
_kiteRenderer,
KiteSpritePicName,
value => loaded = value);
_kiteSpriteLoaded = loaded;
EnsureFocusMaterial();
if (_focusMaterial != null)
_kiteRenderer.material = _focusMaterial;
}
if (!_baseSpriteLoaded)
{
var loaded = false;
yield return Day2SleepSpriteLoader.LoadInto(
_baseRenderer,
KiteBasePicName,
value => loaded = value);
_baseSpriteLoaded = loaded;
}
if (!_kiteSpriteLoaded || !_baseSpriteLoaded)
{
ClearImmediate();
yield break;
}
_mask.sprite = _baseRenderer.sprite;
_mask.frontSortingLayerID = _kiteRenderer.sortingLayerID;
_mask.backSortingLayerID = _kiteRenderer.sortingLayerID;
_mask.frontSortingOrder = _referenceRenderer.sortingOrder + 2;
_mask.backSortingOrder = _referenceRenderer.sortingOrder;
_mask.enabled = true;
_driver.Wake(height, wind);
_baseRenderer.SetAlpha(1f);
_kiteRenderer.SetAlpha(KiteRigDriver.Alpha(height));
}
public void SetState(float height, int wind, float duration = 0.9f)
{
if (!IsShowing || _driver == null) return;
_driver.SetHeight(Mathf.Clamp(height, 0f, KiteMaxHeight), duration);
_driver.SetWind(wind);
}
public void SetWind(int wind, float turbulence = -1f)
{
if (!IsShowing || _driver == null) return;
var value = turbulence >= 0f ? Mathf.Clamp01(turbulence) : 0f;
_driver.SetWind(wind, value);
}
public void PlayAccent(string profile)
{
if (!IsShowing || _kiteRenderer == null) return;
_accentSequence?.Kill();
_accentSequence = null;
var transform = _kiteRenderer.transform;
var sequence = DOTween.Sequence();
switch (NormalizeProfile(profile))
{
case "lift":
sequence.Append(transform.DOLocalPath(new[]
{
new Vector3(0.03f, 0.07f, 0f),
new Vector3(0.11f, 0.17f, 0f),
new Vector3(0.12f, 0.24f, 0f),
}, 0.75f, PathType.CatmullRom).SetEase(Ease.InOutSine));
sequence.Join(transform.DOLocalRotate(new Vector3(2f, -1.5f, -2f), 0.62f)
.SetEase(Ease.InOutSine));
break;
case "sink":
sequence.Append(transform.DOLocalPath(new[]
{
new Vector3(0.03f, -0.06f, 0f),
new Vector3(-0.02f, -0.17f, 0f),
new Vector3(-0.08f, -0.24f, 0f),
}, 0.82f, PathType.CatmullRom).SetEase(Ease.InOutSine));
sequence.Join(transform.DOLocalRotate(new Vector3(-2f, 1.5f, 2f), 0.68f)
.SetEase(Ease.InOutSine));
break;
case "taut":
sequence.Append(transform.DOLocalPath(new[]
{
new Vector3(0.08f, 0.01f, 0f),
new Vector3(0.2f, 0.07f, 0f),
new Vector3(0.26f, 0.05f, 0f),
}, 0.62f, PathType.CatmullRom).SetEase(Ease.OutSine));
sequence.Join(transform.DOLocalRotate(new Vector3(0f, -2f, -3f), 0.62f)
.SetEase(Ease.OutSine));
sequence.AppendInterval(0.32f);
break;
case "drop":
sequence.Append(transform.DOLocalPath(new[]
{
new Vector3(0.06f, -0.08f, 0f),
new Vector3(0.08f, -0.24f, 0f),
new Vector3(0.01f, -0.36f, 0f),
}, 0.92f, PathType.CatmullRom).SetEase(Ease.InSine));
sequence.Join(transform.DOLocalRotate(new Vector3(-2.5f, 1f, 3f), 0.86f)
.SetEase(Ease.InOutSine));
break;
case "wrench":
sequence.Append(transform.DOLocalPath(new[]
{
new Vector3(-0.06f, 0.03f, 0f),
new Vector3(0.3f, 0.18f, 0f),
new Vector3(0.64f, 0.12f, 0f),
new Vector3(0.78f, 0.24f, 0f),
}, 1.15f, PathType.CatmullRom).SetEase(Ease.InOutSine));
sequence.Join(transform.DOLocalRotate(new Vector3(1.5f, -3f, -6f), 0.92f)
.SetEase(Ease.InOutSine));
sequence.AppendInterval(0.42f);
break;
case "flutter":
var side = Random.value < 0.5f ? -1f : 1f;
sequence.Append(transform.DOLocalPath(new[]
{
new Vector3(0.05f * side, 0.04f, 0f),
new Vector3(0.16f * side, 0.11f, 0f),
new Vector3(0.24f * side, 0.07f, 0f),
}, 1.05f, PathType.CatmullRom).SetEase(Ease.InOutSine));
sequence.Join(transform.DOLocalRotate(
new Vector3(1.5f, -2.5f * side, -3f * side), 0.9f)
.SetEase(Ease.InOutSine));
sequence.AppendInterval(0.22f);
break;
default:
sequence.Kill();
return;
}
sequence.Append(transform.DOLocalMove(Vector3.zero, 0.82f).SetEase(Ease.InOutSine));
sequence.Join(transform.DOLocalRotate(Vector3.zero, 0.82f).SetEase(Ease.InOutSine));
_accentSequence = sequence;
}
public IEnumerator Hide(float duration = 1f)
{
ClearImmediate();
yield break;
}
public void ClearImmediate()
{
KillTweens();
if (_kiteRenderer != null)
{
_kiteRenderer.SetAlpha(0f);
_kiteRenderer.transform.localPosition = Vector3.zero;
_kiteRenderer.transform.localRotation = Quaternion.identity;
}
if (_baseRenderer != null)
{
_baseRenderer.SetAlpha(0f);
_baseRenderer.gameObject.SetActive(false);
}
if (_cameraNode != null)
_cameraNode.gameObject.SetActive(false);
if (_mask != null)
_mask.enabled = false;
}
private void EnsureRig()
{
if (_rootPivot != null || _referenceRenderer == null) return;
var parent = _referenceRenderer.transform.parent;
var cameraObject = new GameObject("Kite Camera");
cameraObject.transform.SetParent(parent);
cameraObject.transform.localPosition = _baseLocalPosition;
cameraObject.transform.localRotation = Quaternion.identity;
cameraObject.transform.localScale = Vector3.one;
var rootObject = new GameObject("Kite Rig");
rootObject.transform.SetParent(cameraObject.transform, false);
rootObject.transform.localRotation = _baseLocalRotation;
rootObject.transform.localScale = _baseLocalScale;
var swayObject = new GameObject("Kite Sway");
swayObject.transform.SetParent(rootObject.transform, false);
var accentObject = new GameObject("Kite Accent");
accentObject.transform.SetParent(swayObject.transform, false);
var baseObject = new GameObject("Kite Base");
baseObject.transform.SetParent(parent);
baseObject.transform.localPosition = _baseLocalPosition;
baseObject.transform.localRotation = _baseLocalRotation;
baseObject.transform.localScale = _baseLocalScale;
var maskObject = new GameObject("Kite Mask");
maskObject.transform.SetParent(parent);
maskObject.transform.localPosition = _baseLocalPosition;
maskObject.transform.localRotation = _baseLocalRotation;
maskObject.transform.localScale = _baseLocalScale;
_cameraNode = cameraObject.transform;
_rootPivot = rootObject.transform;
_swayPivot = swayObject.transform;
_kiteRenderer = accentObject.AddComponent<SpriteRenderer>();
CopyRendererSettings(_kiteRenderer, 1, SpriteMaskInteraction.VisibleInsideMask);
_baseRenderer = baseObject.AddComponent<SpriteRenderer>();
CopyRendererSettings(_baseRenderer, 0, SpriteMaskInteraction.None);
_mask = maskObject.AddComponent<SpriteMask>();
_mask.alphaCutoff = 0.01f;
_mask.isCustomRangeActive = true;
_mask.enabled = false;
EnsureFocusMaterial();
_driver = cameraObject.AddComponent<KiteRigDriver>();
_driver.Configure(
_cameraNode,
_rootPivot,
_swayPivot,
_kiteRenderer,
_baseLocalPosition,
_baseLocalRotation,
_baseLocalScale,
_windSettings,
_cameraSettings,
_focusSettings,
_focusMaterial);
_driver.enabled = false;
cameraObject.SetActive(false);
baseObject.SetActive(false);
_kiteRenderer.SetAlpha(0f);
_baseRenderer.SetAlpha(0f);
}
private void CopyRendererSettings(
SpriteRenderer target,
int sortingOrderOffset,
SpriteMaskInteraction maskInteraction)
{
target.sharedMaterial = _referenceRenderer.sharedMaterial;
target.sortingLayerID = _referenceRenderer.sortingLayerID;
target.sortingOrder = _referenceRenderer.sortingOrder + sortingOrderOffset;
target.drawMode = _referenceRenderer.drawMode;
target.size = _referenceRenderer.size;
target.flipX = _referenceRenderer.flipX;
target.flipY = _referenceRenderer.flipY;
target.maskInteraction = maskInteraction;
}
private void EnsureFocusMaterial()
{
if (_focusMaterial != null) return;
var shader = Shader.Find("AIBIS/KiteFocusSprite");
if (shader == null)
{
Debug.LogWarning("[KitePresentation] 找不到 AIBIS/KiteFocusSprite,风筝虚焦不可用。");
return;
}
_focusMaterial = new Material(shader) { name = "KiteFocusSprite (Runtime)" };
}
private void KillTweens()
{
_accentSequence?.Kill();
_accentSequence = null;
_driver?.ResetAndSleep();
}
private static string NormalizeProfile(string profile)
{
return string.IsNullOrEmpty(profile) ? string.Empty : profile.Trim().ToLowerInvariant();
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 6ade30c6ea7b492e8ee1ee2b32141673
@@ -265,6 +265,8 @@ namespace AibisDream
public float TailSway { get; private set; }
public float CurrentHeight => _height;
public float TargetHeight => _heightTarget;
public int CurrentWind => _wind;
public float Turbulence => _turbulence;
public bool IsRunning => _running;
@@ -0,0 +1,293 @@
using System.Collections;
using AibisDream.Utility;
using DG.Tweening;
using UnityEngine;
namespace AibisDream
{
internal sealed class LargeSpriteEffectsPresenter
{
private static readonly int BlurAmountId = Shader.PropertyToID("_BlurAmount");
private static readonly int BlurSizeId = Shader.PropertyToID("_BlurSize");
private static readonly int UseSoftFocusId = Shader.PropertyToID("_UseSoftFocus");
private readonly MonoBehaviour _host;
private readonly SpriteRenderer _renderer;
private readonly Material _focusMaterial;
private readonly Vector3 _baseLocalPosition;
private readonly Quaternion _baseLocalRotation;
private readonly Vector3 _baseLocalScale;
private readonly float _baseAlpha;
private readonly Material _baseMaterial;
private MaterialPropertyBlock _propertyBlock;
private Coroutine _animationCoroutine;
private Sequence _presentationSequence;
private int _blurTweenVersion;
public bool IsAnimationPlaying => _animationCoroutine != null;
public string AnimationPrefix { get; private set; }
public int AnimationFrameCount { get; private set; }
public float AnimationSecondsPerFrame { get; private set; } = 0.2f;
public float TargetBlurAmount { get; private set; }
public float TargetBlurSize { get; private set; } = 0.012f;
public float TargetScaleMultiplier { get; private set; } = 1f;
public float TargetAlpha { get; private set; } = 1f;
public LargeSpriteEffectsPresenter(
MonoBehaviour host,
SpriteRenderer renderer,
Material focusMaterial)
{
_host = host;
_renderer = renderer;
_focusMaterial = focusMaterial;
if (_renderer == null) return;
var transform = _renderer.transform;
_baseLocalPosition = transform.localPosition;
_baseLocalRotation = transform.localRotation;
_baseLocalScale = transform.localScale;
// SpriteShowcase 在自己的 Awake 中会先隐藏 Renderer;D2 的稳定基准仍然是完全可见。
_baseAlpha = 1f;
_baseMaterial = _renderer.sharedMaterial;
}
public IEnumerator ShowAnimation(
string picNamePrefix,
int frameCount,
float secondsPerFrame = 0.2f,
float duration = 1f)
{
if (_renderer == null || _host == null || frameCount <= 0) yield break;
ResetEffectsImmediate();
var frames = new Sprite[frameCount];
var normalizedPrefix = Day2SleepSpriteLoader.NormalizeName(picNamePrefix);
for (var index = 0; index < frameCount; index++)
{
Sprite loadedFrame = null;
yield return Day2SleepSpriteLoader.Load(
$"{normalizedPrefix}{index + 1}",
value => loadedFrame = value);
if (loadedFrame == null)
{
ResetEffectsImmediate();
yield break;
}
frames[index] = loadedFrame;
}
_renderer.gameObject.SetActive(true);
_renderer.sprite = frames[0];
_renderer.SetAlpha(0f);
AnimationPrefix = normalizedPrefix;
AnimationFrameCount = frameCount;
AnimationSecondsPerFrame = Mathf.Max(0.01f, secondsPerFrame);
_animationCoroutine = _host.StartCoroutine(LoopAnimation(frames, AnimationSecondsPerFrame));
if (duration <= 0f)
_renderer.SetAlpha(1f);
else
yield return _renderer.DOFade(1f, duration).WaitForCompletion();
TargetAlpha = 1f;
}
public void SetBlur(float amount, float blurSize = 0.012f)
{
_blurTweenVersion++;
TargetBlurAmount = Mathf.Clamp01(amount);
TargetBlurSize = Mathf.Max(0f, blurSize);
ApplyBlur(TargetBlurAmount, TargetBlurSize);
}
public IEnumerator TweenBlur(float targetAmount, float duration, float blurSize = 0.012f)
{
if (_renderer == null) yield break;
var tweenVersion = ++_blurTweenVersion;
var startAmount = ReadBlurAmount();
TargetBlurAmount = Mathf.Clamp01(targetAmount);
TargetBlurSize = Mathf.Max(0f, blurSize);
duration = Mathf.Max(0f, duration);
if (duration <= 0f)
{
ApplyBlur(TargetBlurAmount, TargetBlurSize);
yield break;
}
var elapsed = 0f;
while (elapsed < duration)
{
if (tweenVersion != _blurTweenVersion) yield break;
elapsed += Time.deltaTime;
var progress = Mathf.Clamp01(elapsed / duration);
progress = progress * progress * (3f - 2f * progress);
ApplyBlur(Mathf.Lerp(startAmount, TargetBlurAmount, progress), TargetBlurSize);
yield return null;
}
if (tweenVersion != _blurTweenVersion) yield break;
ApplyBlur(TargetBlurAmount, TargetBlurSize);
}
public IEnumerator AnimatePresentation(float scaleMultiplier, float targetAlpha, float duration)
{
if (_renderer == null) yield break;
KillPresentationTweens();
_renderer.gameObject.SetActive(true);
TargetScaleMultiplier = Mathf.Max(0.01f, scaleMultiplier);
TargetAlpha = Mathf.Clamp01(targetAlpha);
duration = Mathf.Max(0f, duration);
var targetScale = ScaleFor(TargetScaleMultiplier);
if (duration <= 0f)
{
ApplyPresentationImmediate(TargetScaleMultiplier, TargetAlpha);
yield break;
}
_presentationSequence = DOTween.Sequence();
_presentationSequence.Join(
_renderer.transform.DOScale(targetScale, duration).SetEase(Ease.InOutSine));
_presentationSequence.Join(
_renderer.transform.DOLocalMove(_baseLocalPosition, duration).SetEase(Ease.InOutSine));
_presentationSequence.Join(
_renderer.DOFade(TargetAlpha, duration).SetEase(Ease.InOutSine));
yield return _presentationSequence.WaitForCompletion();
_presentationSequence = null;
}
public void ApplyPresentationImmediate(float scaleMultiplier, float alpha)
{
if (_renderer == null) return;
KillPresentationTweens();
TargetScaleMultiplier = Mathf.Max(0.01f, scaleMultiplier);
TargetAlpha = Mathf.Clamp01(alpha);
var transform = _renderer.transform;
transform.localPosition = _baseLocalPosition;
transform.localRotation = _baseLocalRotation;
transform.localScale = ScaleFor(TargetScaleMultiplier);
_renderer.SetAlpha(TargetAlpha);
}
public void SetSpriteImmediate(Sprite sprite)
{
if (_renderer != null && sprite != null)
_renderer.sprite = sprite;
}
public void StopPresentationAnimation()
{
KillPresentationTweens();
}
public void ResetPresentationImmediate()
{
if (_renderer == null) return;
KillPresentationTweens();
TargetScaleMultiplier = 1f;
TargetAlpha = _baseAlpha;
var transform = _renderer.transform;
transform.localPosition = _baseLocalPosition;
transform.localRotation = _baseLocalRotation;
transform.localScale = _baseLocalScale;
_renderer.SetAlpha(_baseAlpha);
}
public void HidePresentationImmediate()
{
if (_renderer == null) return;
StopAnimation(hideRenderer: true);
ApplyBlur(0f, 0f);
ResetPresentationImmediate();
_renderer.SetAlpha(0f);
_renderer.gameObject.SetActive(false);
}
public void ResetEffectsImmediate()
{
_blurTweenVersion++;
var animationWasPlaying = IsAnimationPlaying;
StopAnimation(hideRenderer: animationWasPlaying);
TargetBlurAmount = 0f;
TargetBlurSize = 0.012f;
ApplyBlur(0f, 0f);
ResetPresentationImmediate();
}
private IEnumerator LoopAnimation(Sprite[] frames, float secondsPerFrame)
{
var index = 0;
var wait = new WaitForSeconds(Mathf.Max(0.01f, secondsPerFrame));
while (frames.Length > 0)
{
if (_renderer != null)
_renderer.sprite = frames[index];
index = (index + 1) % frames.Length;
yield return wait;
}
}
private void StopAnimation(bool hideRenderer)
{
if (_animationCoroutine != null && _host != null)
{
_host.StopCoroutine(_animationCoroutine);
_animationCoroutine = null;
}
AnimationPrefix = null;
AnimationFrameCount = 0;
if (hideRenderer && _renderer != null)
{
_renderer.SetAlpha(0f);
_renderer.gameObject.SetActive(false);
}
}
private float ReadBlurAmount()
{
if (_renderer == null || _renderer.sharedMaterial != _focusMaterial) return 0f;
_propertyBlock ??= new MaterialPropertyBlock();
_renderer.GetPropertyBlock(_propertyBlock);
return _propertyBlock.GetFloat(BlurAmountId);
}
private void ApplyBlur(float amount, float blurSize)
{
if (_renderer == null) return;
amount = Mathf.Clamp01(amount);
if (amount <= 0.001f)
{
_renderer.SetPropertyBlock(null);
_renderer.sharedMaterial = _baseMaterial;
return;
}
if (_focusMaterial == null)
{
Debug.LogWarning("[LargeSpriteEffectsPresenter] 未配置全屏图片对焦材质。");
return;
}
_renderer.sharedMaterial = _focusMaterial;
_propertyBlock ??= new MaterialPropertyBlock();
_renderer.GetPropertyBlock(_propertyBlock);
_propertyBlock.SetFloat(BlurAmountId, amount);
_propertyBlock.SetFloat(BlurSizeId, Mathf.Max(0f, blurSize));
_propertyBlock.SetFloat(UseSoftFocusId, 1f);
_renderer.SetPropertyBlock(_propertyBlock);
}
private Vector3 ScaleFor(float multiplier)
{
return new Vector3(
_baseLocalScale.x * multiplier,
_baseLocalScale.y * multiplier,
_baseLocalScale.z);
}
private void KillPresentationTweens()
{
_presentationSequence?.Kill();
_presentationSequence = null;
if (_renderer == null) return;
DOTween.Kill(_renderer);
DOTween.Kill(_renderer.transform);
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 2a1c4fea897e46bf94ed561eaa210ceb
@@ -0,0 +1,341 @@
using System.Collections;
using AibisDream.Utility;
using DG.Tweening;
using UnityEngine;
namespace AibisDream
{
internal sealed class ReelMotionPresenter
{
private const string BaseSuffix = "_底图";
private const string MotionSuffix = "_风筝层";
private const float MotionSpeedMultiplier = 2.3f;
private const float MotionAmplitude = 0.22f;
private readonly SpriteRenderer _baseRenderer;
private readonly Vector3 _baseLocalPosition;
private readonly Quaternion _baseLocalRotation;
private readonly Vector3 _baseLocalScale;
private SpriteRenderer _motionRenderer;
private SpriteMask _motionMask;
private Sequence _motionSequence;
private Vector3 _motionBaseOffset;
public bool IsShowing => _baseRenderer != null && _baseRenderer.gameObject.activeSelf;
public string CurrentPicName { get; private set; }
public string CurrentMotionProfile { get; private set; }
public ReelMotionPresenter(SpriteRenderer baseRenderer)
{
_baseRenderer = baseRenderer;
if (_baseRenderer == null) return;
var transform = _baseRenderer.transform;
_baseLocalPosition = transform.localPosition;
_baseLocalRotation = transform.localRotation;
_baseLocalScale = transform.localScale;
}
public IEnumerator Show(string picName, string motionProfile)
{
ClearImmediate();
if (_baseRenderer == null) yield break;
var normalizedName = Day2SleepSpriteLoader.NormalizeName(picName);
if (!IsReelPic(normalizedName))
{
Debug.LogWarning($"[ReelMotionPresenter] Unsupported reel sprite: {picName}");
yield break;
}
_motionBaseOffset = new Vector3(0f, -0.32f, 0f);
_baseRenderer.gameObject.SetActive(true);
var baseLoaded = false;
yield return Day2SleepSpriteLoader.LoadInto(
_baseRenderer,
normalizedName + BaseSuffix,
value => baseLoaded = value);
if (!baseLoaded)
{
ClearImmediate();
yield break;
}
EnsureMotionRenderer();
_motionRenderer.gameObject.SetActive(true);
CopyRendererSettings(_motionRenderer, 1, SpriteMaskInteraction.VisibleInsideMask);
ConfigureMotionMask();
ResetTransforms();
var motionLoaded = false;
yield return Day2SleepSpriteLoader.LoadInto(
_motionRenderer,
normalizedName + MotionSuffix,
value => motionLoaded = value);
if (!motionLoaded)
{
ClearImmediate();
yield break;
}
_baseRenderer.SetAlpha(1f);
_motionRenderer.SetAlpha(1f);
CurrentPicName = normalizedName;
PlayMotion(motionProfile);
}
public void PlayCurrentMotion(string motionProfile)
{
if (!IsShowing) return;
PlayMotion(motionProfile);
}
public void ClearImmediate()
{
KillTweenState();
ResetTransforms();
CurrentPicName = null;
CurrentMotionProfile = null;
if (_baseRenderer != null)
{
_baseRenderer.DOKill();
_baseRenderer.SetAlpha(0f);
_baseRenderer.gameObject.SetActive(false);
}
if (_motionRenderer != null)
{
_motionRenderer.DOKill();
_motionRenderer.SetAlpha(0f);
_motionRenderer.gameObject.SetActive(false);
_motionRenderer.maskInteraction = SpriteMaskInteraction.None;
}
if (_motionMask != null)
_motionMask.enabled = false;
}
private void EnsureMotionRenderer()
{
if (_motionRenderer != null || _baseRenderer == null) return;
var motionObject = new GameObject($"{_baseRenderer.name} Motion");
motionObject.transform.SetParent(_baseRenderer.transform.parent);
_motionRenderer = motionObject.AddComponent<SpriteRenderer>();
CopyRendererSettings(_motionRenderer, 1, SpriteMaskInteraction.VisibleInsideMask);
_motionRenderer.gameObject.SetActive(false);
_motionRenderer.SetAlpha(0f);
}
private void EnsureMotionMask()
{
if (_motionMask != null || _baseRenderer == null) return;
var maskObject = new GameObject($"{_baseRenderer.name} Motion Mask");
maskObject.transform.SetParent(_baseRenderer.transform.parent);
_motionMask = maskObject.AddComponent<SpriteMask>();
_motionMask.alphaCutoff = 0.01f;
_motionMask.isCustomRangeActive = true;
_motionMask.enabled = false;
}
private void CopyRendererSettings(
SpriteRenderer target,
int sortingOrderOffset,
SpriteMaskInteraction maskInteraction)
{
if (target == null || _baseRenderer == null) return;
target.sharedMaterial = _baseRenderer.sharedMaterial;
target.sortingLayerID = _baseRenderer.sortingLayerID;
target.sortingOrder = _baseRenderer.sortingOrder + sortingOrderOffset;
target.drawMode = _baseRenderer.drawMode;
target.size = _baseRenderer.size;
target.flipX = _baseRenderer.flipX;
target.flipY = _baseRenderer.flipY;
target.maskInteraction = maskInteraction;
}
private void ConfigureMotionMask()
{
EnsureMotionMask();
if (_motionMask == null) return;
_motionMask.sprite = _baseRenderer.sprite;
_motionMask.frontSortingLayerID = _baseRenderer.sortingLayerID;
_motionMask.backSortingLayerID = _baseRenderer.sortingLayerID;
_motionMask.frontSortingOrder = _baseRenderer.sortingOrder + 2;
_motionMask.backSortingOrder = _baseRenderer.sortingOrder;
var transform = _motionMask.transform;
transform.localPosition = _baseLocalPosition;
transform.localRotation = _baseLocalRotation;
transform.localScale = _baseLocalScale;
_motionMask.enabled = true;
}
private void ResetTransforms()
{
if (_baseRenderer == null) return;
var baseTransform = _baseRenderer.transform;
baseTransform.localPosition = _baseLocalPosition;
baseTransform.localRotation = _baseLocalRotation;
baseTransform.localScale = _baseLocalScale;
if (_motionRenderer == null) return;
var motionTransform = _motionRenderer.transform;
motionTransform.localPosition = _baseLocalPosition + _motionBaseOffset;
motionTransform.localRotation = _baseLocalRotation;
motionTransform.localScale = _baseLocalScale;
}
private static bool IsReelPic(string picName)
{
return picName == "D2S轱辘" || picName == "D2S轱辘没线";
}
private static string NormalizeProfile(string motionProfile)
{
return string.IsNullOrEmpty(motionProfile)
? string.Empty
: motionProfile.Trim().ToLowerInvariant();
}
private Vector3 BaseOffset(float x, float y)
{
return _baseLocalPosition + _motionBaseOffset + new Vector3(x, y, 0f);
}
private void PlayMotion(string motionProfile)
{
KillTweenState();
var profile = NormalizeProfile(motionProfile);
CurrentMotionProfile = profile;
if (string.IsNullOrEmpty(profile) || profile == "none") return;
if (profile != "reel_tension")
{
Debug.LogWarning($"[ReelMotionPresenter] Unknown motion profile: {motionProfile}");
return;
}
var target = _motionRenderer != null && _motionRenderer.gameObject.activeSelf
? _motionRenderer.transform
: _baseRenderer.transform;
var sequence = DOTween.Sequence();
sequence.Append(target.DOLocalMove(
BaseOffset(0.13f * MotionAmplitude, -0.32f * MotionAmplitude), 0.38f)
.SetEase(Ease.OutCubic));
sequence.Join(target.DOLocalRotate(
new Vector3(0f, 0f, -2.8f * MotionAmplitude), 0.38f)
.SetEase(Ease.OutCubic));
sequence.OnComplete(() => PlayReelHold(target));
sequence.timeScale = MotionSpeedMultiplier;
_motionSequence = sequence;
}
private void PlayReelHold(Transform transform, float strain = 0f)
{
_motionSequence?.Kill();
var amp = MotionAmplitude;
var deepX = strain * 0.03f;
var deepY = strain * 0.07f;
var speed = Mathf.Lerp(1f, 0.78f, strain);
var rotBias = strain * 0.35f;
var sequence = DOTween.Sequence();
var strainDuration = Random.Range(0.55f, 0.9f) * speed;
sequence.Append(transform.DOLocalMove(
BaseOffset((Random.Range(0.17f, 0.23f) + deepX) * amp,
(Random.Range(-0.42f, -0.34f) - deepY) * amp), strainDuration)
.SetEase(Ease.InOutSine));
sequence.Join(transform.DOLocalRotate(
new Vector3(0f, 0f, (Random.Range(-3.6f, -2.4f) - rotBias) * amp), strainDuration)
.SetEase(Ease.InOutSine));
if (Random.value < 0.4f + strain * 0.25f)
{
var yankDuration = Random.Range(0.18f, 0.28f) * speed;
sequence.Append(transform.DOLocalMove(
BaseOffset((Random.Range(0.25f, 0.34f) + deepX) * amp,
(Random.Range(-0.62f, -0.5f) - deepY) * amp), yankDuration)
.SetEase(Ease.OutCubic));
sequence.Join(transform.DOLocalRotate(
new Vector3(0f, 0f, (Random.Range(-4.8f, -3.4f) - rotBias) * amp), yankDuration)
.SetEase(Ease.OutCubic));
sequence.AppendInterval(Random.Range(0.1f, 0.18f) * speed);
var biteDuration = Random.Range(0.2f, 0.32f) * speed;
sequence.Append(transform.DOLocalMove(
BaseOffset((Random.Range(0.19f, 0.27f) + deepX) * amp,
(Random.Range(-0.48f, -0.4f) - deepY) * amp), biteDuration)
.SetEase(Ease.OutCubic));
sequence.Join(transform.DOLocalRotate(
new Vector3(0f, 0f, (Random.Range(-3.4f, -2.2f) - rotBias) * amp), biteDuration)
.SetEase(Ease.OutCubic));
}
var recoverDuration = Random.Range(0.28f, 0.42f) * speed;
sequence.Append(transform.DOLocalMove(
BaseOffset((Random.Range(0.12f, 0.16f) + deepX) * amp,
(Random.Range(-0.32f, -0.26f) - deepY * 0.55f) * amp), recoverDuration)
.SetEase(Ease.OutCubic));
sequence.Join(transform.DOLocalRotate(
new Vector3(0f, 0f, (Random.Range(-2.3f, -1.3f) - rotBias * 0.7f) * amp), recoverDuration)
.SetEase(Ease.OutCubic));
var slipDuration = Random.Range(0.16f, 0.26f) * speed;
sequence.Append(transform.DOLocalMove(
BaseOffset((Random.Range(0.2f, 0.27f) + deepX) * amp,
(Random.Range(-0.5f, -0.4f) - deepY) * amp), slipDuration)
.SetEase(Ease.OutQuad));
sequence.Join(transform.DOLocalRotate(
new Vector3(0f, 0f, (Random.Range(-4.1f, -2.8f) - rotBias) * amp), slipDuration)
.SetEase(Ease.OutQuad));
if (Random.value < 0.3f)
{
var dipDuration = Random.Range(0.14f, 0.22f) * speed;
sequence.Append(transform.DOLocalMove(
BaseOffset((Random.Range(0.21f, 0.28f) + deepX) * amp,
(Random.Range(-0.56f, -0.47f) - deepY) * amp), dipDuration)
.SetEase(Ease.OutQuad));
sequence.Join(transform.DOLocalRotate(
new Vector3(0f, 0f, (Random.Range(-4.4f, -3f) - rotBias) * amp), dipDuration)
.SetEase(Ease.OutQuad));
var braceDuration = Random.Range(0.18f, 0.28f) * speed;
sequence.Append(transform.DOLocalMove(
BaseOffset((Random.Range(0.16f, 0.22f) + deepX) * amp,
(Random.Range(-0.4f, -0.33f) - deepY) * amp), braceDuration)
.SetEase(Ease.OutCubic));
sequence.Join(transform.DOLocalRotate(
new Vector3(0f, 0f, (Random.Range(-3f, -1.8f) - rotBias) * amp), braceDuration)
.SetEase(Ease.OutCubic));
}
var pullDuration = Random.Range(0.4f, 0.58f) * speed;
sequence.Append(transform.DOLocalMove(
BaseOffset((Random.Range(0.13f, 0.18f) + deepX) * amp,
(Random.Range(-0.35f, -0.28f) - deepY * 0.75f) * amp), pullDuration)
.SetEase(Ease.InOutSine));
sequence.Join(transform.DOLocalRotate(
new Vector3(0f, 0f, (Random.Range(-2.8f, -1.6f) - rotBias) * amp), pullDuration)
.SetEase(Ease.InOutSine));
if (Random.value < 0.25f)
{
var sagDuration = Random.Range(0.2f, 0.32f) * speed;
sequence.Append(transform.DOLocalMove(
BaseOffset((Random.Range(0.18f, 0.24f) + deepX) * amp,
(Random.Range(-0.46f, -0.38f) - deepY) * amp), sagDuration)
.SetEase(Ease.InOutSine));
sequence.Join(transform.DOLocalRotate(
new Vector3(0f, 0f, (Random.Range(-3.5f, -2.2f) - rotBias) * amp), sagDuration)
.SetEase(Ease.InOutSine));
}
sequence.OnComplete(() => PlayReelHold(transform, Mathf.Min(1f, strain + 0.12f)));
sequence.timeScale = MotionSpeedMultiplier;
_motionSequence = sequence;
}
private void KillTweenState()
{
_motionSequence?.Kill();
_motionSequence = null;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e5bccd7819704be4afad76507faac330
@@ -182,9 +182,7 @@ namespace AibisDream
ResetCodeSeaImmediate();
ResetTerminalImmediate();
var showcase = SpriteShowcase.Instance;
if (showcase != null)
showcase.SetLargePresentationImmediate(1f, 1f);
Day2SleepPresentationController.Instance?.SetLargePresentationImmediate(1f, 1f);
if (showSea && glassSea != null)
{
@@ -263,16 +261,16 @@ namespace AibisDream
/// </summary>
public IEnumerator TweenListenCloseup(float scale, float alpha, float duration)
{
var showcase = SpriteShowcase.Instance;
if (showcase == null)
var presentation = Day2SleepPresentationController.Instance;
if (presentation == null)
yield break;
yield return showcase.AnimateLargePresentation(scale, alpha, duration);
yield return presentation.AnimateLargePresentation(scale, alpha, duration);
}
public void SetListenCloseup(float scale, float alpha)
{
SpriteShowcase.Instance?.SetLargePresentationImmediate(scale, alpha);
Day2SleepPresentationController.Instance?.SetLargePresentationImmediate(scale, alpha);
}
/// <summary>
@@ -334,7 +332,7 @@ namespace AibisDream
: null;
panel?.FadeOutSync(0f);
SpriteShowcase.Instance?.ResetLargePresentationImmediate();
Day2SleepPresentationController.Instance?.ResetLargePresentationImmediate();
}
#endregion
@@ -516,7 +514,7 @@ namespace AibisDream
DOTween.Kill(_doorShellTransform);
DOTween.Kill(flashOverlay);
DOTween.Kill(sceneBlackOverlay);
SpriteShowcase.Instance?.StopLargePresentationAnimation();
Day2SleepPresentationController.Instance?.StopLargePresentationAnimation();
}
#endregion
@@ -568,11 +566,7 @@ namespace AibisDream
if (sceneBlackOverlay != null)
sceneBlackOverlay.SetAlpha(0);
var showcase = SpriteShowcase.Instance;
if (showcase != null)
{
showcase.HideLargePresentationImmediate();
}
Day2SleepPresentationController.Instance?.HideLargePresentationImmediate();
_presentationPhase = DoorPresentationPhase.Hidden;
}
@@ -864,7 +858,7 @@ namespace AibisDream
}
}
SpriteShowcase.Instance?.SetLargeSpriteImmediate(targetSprite);
Day2SleepPresentationController.Instance?.SetLargeSpriteImmediate(targetSprite);
}
private void SetBackgroundVisible(bool visible)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,130 @@
# D2 Sleep 表现系统拆分与存档重构方案
## 目标
将 D2 Sleep 新增的轱辘、风筝、全屏序列帧、虚焦和门近景控制从通用 `SpriteShowcase` 中剥离,同时为通用图片与 D2 专属表现分别建立可扩展的语义快照。
本次重构保持 Yarn 命令名、参数默认值、Addressable key、场景视觉参数和剧情文本不变。
## 职责边界
### SpriteShowcase
只保留跨章节可复用的能力:
- 普通小图显示、隐藏。
- 普通大图显示、隐藏。
- 背景淡入淡出。
- 通用即时清理。
- 通用图片终态的捕获与恢复。
- 在图片切换、隐藏或清理前触发 `PresentationChanging`,通知场景专属表现清除叠加层。
`SpriteShowcase` 不再持有 D2 配置、运行时节点、遮罩、序列帧协程、虚焦材质或 D2 Tween。
### Day2SleepPresentationController
D2 Sleep 场景级统一入口,负责:
- 普通小图、轱辘、风筝和大图特效的互斥。
- 监听 `SpriteShowcase.PresentationChanging` 并停止全部 D2 表现。
- 为 Yarn 和 `DreamDoorSystem` 提供稳定 API。
- 捕获、恢复 D2 专属表现的语义终态。
内部 Presenter
| 类型 | 职责 |
| --- | --- |
| `ReelMotionPresenter` | 轱辘底图/运动层、SpriteMask、`reel_tension` 常驻拉锯循环 |
| `KitePresentation` | 风筝相机节点、底图、遮罩、sway/accent 层、资源加载与显隐 |
| `KiteRigDriver` | 风筝连续运动、风况、高度、镜头补偿与对焦算法 |
| `LargeSpriteEffectsPresenter` | 全屏序列帧、虚焦、大图缩放/透明度及 Tween |
## Yarn 与系统调用
D2 专属命令移动到 `Day2SleepYarnCommand`
- `show_sprite_motion`
- `sprite_motion`
- `kite_show`
- `kite_state`
- `kite_wind`
- `kite_accent`
- `kite_hide`
- `show_large_sprite_animation`
- `set_large_sprite_blur`
- `tween_large_sprite_blur`
- `tween_large_sprite_blur_async`
普通 `show_sprite``show_large_sprite``hide_*` 和 Showcase 背景命令仍由 `BaseYarnCommand` 调用 `SpriteShowcase`
`DreamDoorSystem` 的大图换图、近景缩放和透明度控制调用 `Day2SleepPresentationController`。Bloom、曝光、海浪和终端逻辑仍属于 `DreamDoorSystem`
## 存档模型
存档只记录可重建的语义终态,不保存 Coroutine、Tween、运行时 GameObject、SpriteMask、材质实例或随机运动瞬时相位。
### showcase section
稳定 id`showcase`RestoreOrder69。
`ShowcaseSnapshotDto`
- `displayMode``None` / `Small` / `Large`
- `picName`:逻辑图片名,不保存加载后的 `Sprite`
- `isBackgroundVisible`:背景遮罩终态。
恢复时通过 `ResourceSystem` 重新加载图片,并以 0 秒时长应用终态。
### day2SleepPresentation section
稳定 id`day2SleepPresentation`RestoreOrder70。
`Day2SleepPresentationSnapshotDto` 按模式保存:
| 模式 | 保存内容 |
| --- | --- |
| `SmallMotion` | 图片名、motion profile |
| `Kite` | 目标高度、风向、乱度 |
| `LargeEffects` | 序列帧前缀/帧数/帧间隔、虚焦参数、缩放倍率、透明度 |
`kite_accent` 属于一次性顿挫,不进入快照;轱辘和风筝的随机相位在恢复后从对应语义状态重新开始。
若当前没有 D2 专属状态,Provider 返回 `null`,不写入该 section。通用大图叠加了 D2 虚焦或门近景效果时,会同时写入 `showcase``day2SleepPresentation`:先恢复底图,再恢复特效。
### 恢复顺序
相关表现 Provider 顺序为:
1. `showcase`69):恢复普通底图和背景。
2. `day2SleepPresentation`(70):恢复 D2 动态层或大图特效。
3. `playTool`71):恢复物品展示 UI。
4. `screen`80):恢复饱和度和全屏遮罩。
旧存档没有新增 section 时直接跳过,不提升 `schemaVersion`。旧 `showcase` JSON 中的额外字段由 Json.NET 忽略。
## 场景迁移
`梦境` 场景保留原 `SpriteShowcase` 组件及脚本 GUID,只留下三个 Renderer 引用。新增 `Day2SleepPresentationController`,迁移原对焦材质和三组风筝设置。轱辘、风筝和遮罩节点仍在运行时动态创建。
## 生命周期清理
- 普通图片切换前:`PresentationChanging` 清理 D2 表现。
- D2 模式切换前:控制器清理普通图片与上一种 D2 表现。
- 返回菜单、切章和读档前:`GameManager` 分别清理 D2 控制器与 `SpriteShowcase`
- 组件禁用或销毁:停止 Coroutine/Tween、隐藏运行时节点、关闭 SpriteMask,并恢复大图材质属性、缩放和透明度。
## 验证范围
- Unity 编译和现有 EditMode 测试通过。
- FP/Fiction Day1 普通小图、大图和背景淡入淡出无回退。
- D2 Sleep 轱辘、风筝、全屏序列帧、星图虚焦和 DreamDoor 近景表现保持一致。
- 普通图片命令能中断并清理任意 D2 动效。
- 通用小图/大图、风筝、轱辘、序列帧和大图特效可按快照恢复。
- 返回菜单、切章、读档和重复进入梦境后没有残留动态节点、材质属性或 DOTween。
## 不在本次范围
- 不保存动画的逐帧进度、Tween 中间值或随机种子。
- 不重构 Bloom、曝光和 `DreamDoorSystem` 自身状态机。
- 不修改 Yarn 剧情内容或 `#line:` 标签。
- 不修改现有资源名、Addressable key 和视觉参数。
+3 -3
View File
@@ -6,7 +6,7 @@
放风筝段落原来用 `D2S风筝1/2/3` 三张切图按高度阈值硬切,跨阈值时尺寸跳变,且节点顶部的常驻动效会覆盖选项里的一次性反馈。现已改为**单张风筝图 + 连续参数驱动**,不再使用切图。
实现在 [`SpriteShowcase.cs`](../Assets/Scripts/SceneManagement/SpriteShowcase.cs) 的“风筝 Rig”区域(`#region 风筝 Rig`),命令注册在 [`BaseYarnCommand.cs`](../Assets/Scripts/Dialog%20System/BaseYarnCommand.cs)。
场景入口在 [`Day2SleepPresentationController.cs`](../Assets/Scripts/SceneManagement/Day2Sleep/Day2SleepPresentationController.cs),风筝节点与资源由 [`KitePresentation.cs`](../Assets/Scripts/SceneManagement/Day2Sleep/KitePresentation.cs) 管理,连续运动仍由 [`KiteRigDriver.cs`](../Assets/Scripts/SceneManagement/Day2Sleep/KiteRigDriver.cs) 驱动。命令统一注册在 [`Day2SleepYarnCommand.cs`](../Assets/Scripts/SceneManagement/Day2Sleep/Day2SleepYarnCommand.cs)。
### Rig 结构
@@ -65,7 +65,7 @@ Kite MaskSpriteMask,以底图为形状,风筝层被裁在天空范围内
| `flutter` | 左右交替的几下小拉扯,力度递减 | 保持不动、原地挣扎 |
| `wrench` | 被阵风猛地拽向一侧,停顿后回正 | 强乱流、角色被风筝拖拽 |
轱辘特写(`D2S轱辘`)不属于这套 Rig,仍用旧的 `<<show_sprite_motion "D2S轱辘" "reel_tension" duration>>`,动效在 `SpriteShowcase.PlaySmallMotion` / `PlayReelHold`维护:先被猛地拽进受力姿态(只进一次),之后绷在那里持续高频颤抖,偶尔再被拽深一点——是“全力拉住”的发力感,不会松回原位。
轱辘特写(`D2S轱辘`)不属于这套 Rig,仍使`<<show_sprite_motion "D2S轱辘" "reel_tension" duration>>`;分层、遮罩和常驻拉锯循环由 [`ReelMotionPresenter.cs`](../Assets/Scripts/SceneManagement/Day2Sleep/ReelMotionPresenter.cs) 维护:先被猛地拽进受力姿态(只进一次),之后绷在那里持续拉锯,偶尔再被拽深一点——是“全力拉住”的发力感,不会松回原位。
## 典型用法模式
@@ -109,7 +109,7 @@ Kite MaskSpriteMask,以底图为形状,风筝层被裁在天空范围内
### 4. 离场:`kite_hide` 或让互斥逻辑自动处理
风筝 Rig 已经接入 `ShowObj` / `ShowObjWithMotion` / `ShowLargeObj` / `HideObj` / `HideAll` 的互斥逻辑,切到大图或普通小图时会自动隐藏,不必每次手动 `kite_hide`。只有明确要求“风筝消失但暂不显示别的图”时才需要显式调用。
`Day2SleepPresentationController` 会监听 `SpriteShowcase.PresentationChanging`。执行普通 `show_sprite``show_large_sprite``hide_*` 或通用清理时,会自动停止风筝、轱辘、序列帧与 Tween,因此不必每次手动 `kite_hide`。只有明确要求“风筝消失但暂不显示别的图”时才需要显式调用。
## 常见坑