From aed9998d1ac468d33304f8e83c0f88edbd0abcf9 Mon Sep 17 00:00:00 2001 From: bottlefish <781230111@qq.com> Date: Wed, 22 Jul 2026 00:50:16 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E6=8B=86=E5=88=86D2=20Sleep?= =?UTF-8?q?=E8=A1=A8=E7=8E=B0=E5=B9=B6=E6=8E=A5=E5=85=A5=E5=BF=AB=E7=85=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PresentationSnapshotContractTests.cs | 46 +- Assets/Scenes/梦境.unity | 18 +- .../Scripts/Dialog System/BaseYarnCommand.cs | 112 -- Assets/Scripts/Game Loop/GameManager.cs | 1 + .../Day2SleepPresentationSnapshotProvider.cs | 35 + ...2SleepPresentationSnapshotProvider.cs.meta | 2 + .../Providers/PlayToolSnapshotProvider.cs | 4 +- .../Providers/ShowcaseSnapshotProvider.cs | 34 + .../ShowcaseSnapshotProvider.cs.meta | 2 + Assets/Scripts/SaveSystem/README.md | 9 +- Assets/Scripts/SaveSystem/SaveSnapshot.cs | 47 + Assets/Scripts/SaveSystem/SnapshotRegistry.cs | 6 +- Assets/Scripts/SaveSystem/SnapshotRestore.cs | 2 + Assets/Scripts/SceneManagement/Day2Sleep.meta | 8 + .../Day2SleepPresentationController.cs | 269 +++ .../Day2SleepPresentationController.cs.meta | 2 + .../Day2Sleep/Day2SleepSpriteLoader.cs | 69 + .../Day2Sleep/Day2SleepSpriteLoader.cs.meta | 2 + .../Day2Sleep/Day2SleepYarnCommand.cs | 131 ++ .../Day2Sleep/Day2SleepYarnCommand.cs.meta | 2 + .../Day2Sleep/KitePresentation.cs | 341 ++++ .../Day2Sleep/KitePresentation.cs.meta | 2 + .../{ => Day2Sleep}/KiteRigDriver.cs | 2 + .../{ => Day2Sleep}/KiteRigDriver.cs.meta | 0 .../Day2Sleep/LargeSpriteEffectsPresenter.cs | 293 ++++ .../LargeSpriteEffectsPresenter.cs.meta | 2 + .../Day2Sleep/ReelMotionPresenter.cs | 341 ++++ .../Day2Sleep/ReelMotionPresenter.cs.meta | 2 + .../SceneManagement/DreamDoorSystem.cs | 24 +- .../Scripts/SceneManagement/SpriteShowcase.cs | 1437 ++--------------- Docs/D2Sleep表现系统拆分与存档重构方案.md | 130 ++ Docs/风筝命令使用指南.md | 6 +- 32 files changed, 1977 insertions(+), 1404 deletions(-) create mode 100644 Assets/Scripts/SaveSystem/Providers/Day2SleepPresentationSnapshotProvider.cs create mode 100644 Assets/Scripts/SaveSystem/Providers/Day2SleepPresentationSnapshotProvider.cs.meta create mode 100644 Assets/Scripts/SaveSystem/Providers/ShowcaseSnapshotProvider.cs create mode 100644 Assets/Scripts/SaveSystem/Providers/ShowcaseSnapshotProvider.cs.meta create mode 100644 Assets/Scripts/SceneManagement/Day2Sleep.meta create mode 100644 Assets/Scripts/SceneManagement/Day2Sleep/Day2SleepPresentationController.cs create mode 100644 Assets/Scripts/SceneManagement/Day2Sleep/Day2SleepPresentationController.cs.meta create mode 100644 Assets/Scripts/SceneManagement/Day2Sleep/Day2SleepSpriteLoader.cs create mode 100644 Assets/Scripts/SceneManagement/Day2Sleep/Day2SleepSpriteLoader.cs.meta create mode 100644 Assets/Scripts/SceneManagement/Day2Sleep/Day2SleepYarnCommand.cs create mode 100644 Assets/Scripts/SceneManagement/Day2Sleep/Day2SleepYarnCommand.cs.meta create mode 100644 Assets/Scripts/SceneManagement/Day2Sleep/KitePresentation.cs create mode 100644 Assets/Scripts/SceneManagement/Day2Sleep/KitePresentation.cs.meta rename Assets/Scripts/SceneManagement/{ => Day2Sleep}/KiteRigDriver.cs (99%) rename Assets/Scripts/SceneManagement/{ => Day2Sleep}/KiteRigDriver.cs.meta (100%) create mode 100644 Assets/Scripts/SceneManagement/Day2Sleep/LargeSpriteEffectsPresenter.cs create mode 100644 Assets/Scripts/SceneManagement/Day2Sleep/LargeSpriteEffectsPresenter.cs.meta create mode 100644 Assets/Scripts/SceneManagement/Day2Sleep/ReelMotionPresenter.cs create mode 100644 Assets/Scripts/SceneManagement/Day2Sleep/ReelMotionPresenter.cs.meta create mode 100644 Docs/D2Sleep表现系统拆分与存档重构方案.md diff --git a/Assets/Editor/PresentationSnapshotContractTests.cs b/Assets/Editor/PresentationSnapshotContractTests.cs index e71f5b52e..3f5bbfedd 100644 --- a/Assets/Editor/PresentationSnapshotContractTests.cs +++ b/Assets/Editor/PresentationSnapshotContractTests.cs @@ -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(json); + var showcase = ((JObject)restored.sections[SnapshotProviderIds.Showcase]) + .ToObject(); + var day2Sleep = ((JObject)restored.sections[SnapshotProviderIds.Day2SleepPresentation]) + .ToObject(); + + 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); } } diff --git a/Assets/Scenes/梦境.unity b/Assets/Scenes/梦境.unity index e013a019b..fd5989550 100644 --- a/Assets/Scenes/梦境.unity +++ b/Assets/Scenes/梦境.unity @@ -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 diff --git a/Assets/Scripts/Dialog System/BaseYarnCommand.cs b/Assets/Scripts/Dialog System/BaseYarnCommand.cs index d822b64f7..a0d12b283 100644 --- a/Assets/Scripts/Dialog System/BaseYarnCommand.cs +++ b/Assets/Scripts/Dialog System/BaseYarnCommand.cs @@ -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) { diff --git a/Assets/Scripts/Game Loop/GameManager.cs b/Assets/Scripts/Game Loop/GameManager.cs index 2330b756c..008297193 100644 --- a/Assets/Scripts/Game Loop/GameManager.cs +++ b/Assets/Scripts/Game Loop/GameManager.cs @@ -708,6 +708,7 @@ namespace AibisDream { ScreenEffectManager.Instance?.ResetSaturation(); UIManager.Instance.GetPanel()?.ClearPresentationEffects(); + Day2SleepPresentationController.Instance?.ClearPresentationEffects(); SpriteShowcase.Instance?.ClearPresentationEffects(); CameraKit.Instance?.ResetCamera(); } diff --git a/Assets/Scripts/SaveSystem/Providers/Day2SleepPresentationSnapshotProvider.cs b/Assets/Scripts/SaveSystem/Providers/Day2SleepPresentationSnapshotProvider.cs new file mode 100644 index 000000000..8345cf233 --- /dev/null +++ b/Assets/Scripts/SaveSystem/Providers/Day2SleepPresentationSnapshotProvider.cs @@ -0,0 +1,35 @@ +using System.Collections; + +namespace AibisDream.SaveSystem +{ + /// sections["day2SleepPresentation"]:D2 Sleep 动态表现语义终态。 + 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); + } + } +} diff --git a/Assets/Scripts/SaveSystem/Providers/Day2SleepPresentationSnapshotProvider.cs.meta b/Assets/Scripts/SaveSystem/Providers/Day2SleepPresentationSnapshotProvider.cs.meta new file mode 100644 index 000000000..fc6dd3917 --- /dev/null +++ b/Assets/Scripts/SaveSystem/Providers/Day2SleepPresentationSnapshotProvider.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 8f80937eaaba42da9baa4a2d8e73ff57 diff --git a/Assets/Scripts/SaveSystem/Providers/PlayToolSnapshotProvider.cs b/Assets/Scripts/SaveSystem/Providers/PlayToolSnapshotProvider.cs index 1cef469ac..76de43cbb 100644 --- a/Assets/Scripts/SaveSystem/Providers/PlayToolSnapshotProvider.cs +++ b/Assets/Scripts/SaveSystem/Providers/PlayToolSnapshotProvider.cs @@ -3,11 +3,11 @@ using AibisDream.UI; namespace AibisDream.SaveSystem { - /// sections["playTool"]:物品图与全屏图。RestoreOrder=70。 + /// sections["playTool"]:物品图与全屏图。RestoreOrder=71。 public sealed class PlayToolSnapshotProvider : IAsyncSnapshotProvider { public string SaveId => SnapshotProviderIds.PlayTool; - public int RestoreOrder => 70; + public int RestoreOrder => 71; public object Capture() { diff --git a/Assets/Scripts/SaveSystem/Providers/ShowcaseSnapshotProvider.cs b/Assets/Scripts/SaveSystem/Providers/ShowcaseSnapshotProvider.cs new file mode 100644 index 000000000..a48501158 --- /dev/null +++ b/Assets/Scripts/SaveSystem/Providers/ShowcaseSnapshotProvider.cs @@ -0,0 +1,34 @@ +using System.Collections; + +namespace AibisDream.SaveSystem +{ + /// sections["showcase"]:梦境通用静态图片;先于场景专属表现恢复。 + 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); + } + } +} diff --git a/Assets/Scripts/SaveSystem/Providers/ShowcaseSnapshotProvider.cs.meta b/Assets/Scripts/SaveSystem/Providers/ShowcaseSnapshotProvider.cs.meta new file mode 100644 index 000000000..57a784070 --- /dev/null +++ b/Assets/Scripts/SaveSystem/Providers/ShowcaseSnapshotProvider.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 83cccdfaca2a42a89ed515c16c6adfad diff --git a/Assets/Scripts/SaveSystem/README.md b/Assets/Scripts/SaveSystem/README.md index d77a38faf..0b411caa7 100644 --- a/Assets/Scripts/SaveSystem/README.md +++ b/Assets/Scripts/SaveSystem/README.md @@ -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 场景加载(Barrier:LoadSceneAsync)← 框架直管,不走 Provider ↓ -Phase 2 env / actor / audio / timeline / fix / showcase / playTool / screen(Provider 按 RestoreOrder) +Phase 2 env / actor / audio / timeline / fix / showcase / day2SleepPresentation / playTool / screen(Provider 按 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 → fix(cue)→ fixPanel → bodyModule → eye → showcase → playTool → screen。`fixPanel` 须在 `fix` 之后,以覆盖 Cue `EnterImmediate` 中的 `ResetPlug`。 +**还原顺序**:punchTape → fix(cue)→ fixPanel → bodyModule → eye → showcase → day2SleepPresentation → playTool → screen。`fixPanel` 须在 `fix` 之后,以覆盖 Cue `EnterImmediate` 中的 `ResetPlug`;D2 动态表现须在通用 Showcase 图片之后恢复,才能叠加虚焦、缩放或场景专属动画。 **维修场景门控**(`FixSceneSnapshotHelper`):上述 section 仅在 `FixSystemCenter.Instance != null` 时 Capture/Restore。 diff --git a/Assets/Scripts/SaveSystem/SaveSnapshot.cs b/Assets/Scripts/SaveSystem/SaveSnapshot.cs index bbaef67a7..daba7a69b 100644 --- a/Assets/Scripts/SaveSystem/SaveSnapshot.cs +++ b/Assets/Scripts/SaveSystem/SaveSnapshot.cs @@ -190,6 +190,53 @@ namespace AibisDream.SaveSystem public string fullScreenPicName; } + public enum ShowcaseDisplayMode + { + None, + Small, + Large + } + + /// sections["showcase"]:梦境通用静态图片与背景终态。 + [Serializable] + public class ShowcaseSnapshotDto + { + public string displayMode; + public string picName; + public bool isBackgroundVisible; + } + + public enum Day2SleepPresentationMode + { + None, + SmallMotion, + Kite, + LargeEffects + } + + /// sections["day2SleepPresentation"]:D2 Sleep 动态表现语义终态。 + [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; + } + /// sections["subway"]:地铁场景宏观状态(SubwayCenter / OutSideState)。 [Serializable] public class SubwaySnapshotDto diff --git a/Assets/Scripts/SaveSystem/SnapshotRegistry.cs b/Assets/Scripts/SaveSystem/SnapshotRegistry.cs index 37f52c94a..63bb10b4e 100644 --- a/Assets/Scripts/SaveSystem/SnapshotRegistry.cs +++ b/Assets/Scripts/SaveSystem/SnapshotRegistry.cs @@ -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 /// 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()); } diff --git a/Assets/Scripts/SaveSystem/SnapshotRestore.cs b/Assets/Scripts/SaveSystem/SnapshotRestore.cs index ff57b5b33..8a6950ee9 100644 --- a/Assets/Scripts/SaveSystem/SnapshotRestore.cs +++ b/Assets/Scripts/SaveSystem/SnapshotRestore.cs @@ -257,6 +257,8 @@ namespace AibisDream.SaveSystem SnapshotProviderIds.FixPanel => jobj.ToObject(), SnapshotProviderIds.BodyModule => jobj.ToObject(), SnapshotProviderIds.Eye => jobj.ToObject(), + SnapshotProviderIds.Showcase => jobj.ToObject(), + SnapshotProviderIds.Day2SleepPresentation => jobj.ToObject(), SnapshotProviderIds.PlayTool => jobj.ToObject(), SnapshotProviderIds.Screen => jobj.ToObject(), _ => jobj diff --git a/Assets/Scripts/SceneManagement/Day2Sleep.meta b/Assets/Scripts/SceneManagement/Day2Sleep.meta new file mode 100644 index 000000000..d11575858 --- /dev/null +++ b/Assets/Scripts/SceneManagement/Day2Sleep.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: cb05144af0734df89dd21ed3c9db9f13 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/SceneManagement/Day2Sleep/Day2SleepPresentationController.cs b/Assets/Scripts/SceneManagement/Day2Sleep/Day2SleepPresentationController.cs new file mode 100644 index 000000000..12882081d --- /dev/null +++ b/Assets/Scripts/SceneManagement/Day2Sleep/Day2SleepPresentationController.cs @@ -0,0 +1,269 @@ +using System; +using System.Collections; +using AibisDream.Kit; +using AibisDream.SaveSystem; +using UnityEngine; + +namespace AibisDream +{ + /// D2 Sleep 专属动态表现门面;普通静态图片仍由 SpriteShowcase 管理。 + public sealed class Day2SleepPresentationController : Singleton + { + [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 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; + } + } +} diff --git a/Assets/Scripts/SceneManagement/Day2Sleep/Day2SleepPresentationController.cs.meta b/Assets/Scripts/SceneManagement/Day2Sleep/Day2SleepPresentationController.cs.meta new file mode 100644 index 000000000..4dcc72e4a --- /dev/null +++ b/Assets/Scripts/SceneManagement/Day2Sleep/Day2SleepPresentationController.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 9f771105a4b347ffbe2dc7da5acba320 diff --git a/Assets/Scripts/SceneManagement/Day2Sleep/Day2SleepSpriteLoader.cs b/Assets/Scripts/SceneManagement/Day2Sleep/Day2SleepSpriteLoader.cs new file mode 100644 index 000000000..70de3150e --- /dev/null +++ b/Assets/Scripts/SceneManagement/Day2Sleep/Day2SleepSpriteLoader.cs @@ -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 completed = null, + Action warning = null) + { + if (target == null) + { + completed?.Invoke(false); + yield break; + } + + var key = string.Format(DreamSpritePath, NormalizeName(picName)); + var handle = ResourceSystem.LoadAsync(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 completed, + Action warning = null) + { + var key = string.Format(DreamSpritePath, NormalizeName(picName)); + var handle = ResourceSystem.LoadAsync(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); + } + } +} diff --git a/Assets/Scripts/SceneManagement/Day2Sleep/Day2SleepSpriteLoader.cs.meta b/Assets/Scripts/SceneManagement/Day2Sleep/Day2SleepSpriteLoader.cs.meta new file mode 100644 index 000000000..cc255bf3e --- /dev/null +++ b/Assets/Scripts/SceneManagement/Day2Sleep/Day2SleepSpriteLoader.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 41ba6bf8f06f44a0870d6c72a2ebeb39 diff --git a/Assets/Scripts/SceneManagement/Day2Sleep/Day2SleepYarnCommand.cs b/Assets/Scripts/SceneManagement/Day2Sleep/Day2SleepYarnCommand.cs new file mode 100644 index 000000000..4f4ae436d --- /dev/null +++ b/Assets/Scripts/SceneManagement/Day2Sleep/Day2SleepYarnCommand.cs @@ -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(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)); + } + } +} diff --git a/Assets/Scripts/SceneManagement/Day2Sleep/Day2SleepYarnCommand.cs.meta b/Assets/Scripts/SceneManagement/Day2Sleep/Day2SleepYarnCommand.cs.meta new file mode 100644 index 000000000..01ac5b99f --- /dev/null +++ b/Assets/Scripts/SceneManagement/Day2Sleep/Day2SleepYarnCommand.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 60df707002da4e30b73897e04b63d911 diff --git a/Assets/Scripts/SceneManagement/Day2Sleep/KitePresentation.cs b/Assets/Scripts/SceneManagement/Day2Sleep/KitePresentation.cs new file mode 100644 index 000000000..462bf5e6d --- /dev/null +++ b/Assets/Scripts/SceneManagement/Day2Sleep/KitePresentation.cs @@ -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(); + CopyRendererSettings(_kiteRenderer, 1, SpriteMaskInteraction.VisibleInsideMask); + _baseRenderer = baseObject.AddComponent(); + CopyRendererSettings(_baseRenderer, 0, SpriteMaskInteraction.None); + _mask = maskObject.AddComponent(); + _mask.alphaCutoff = 0.01f; + _mask.isCustomRangeActive = true; + _mask.enabled = false; + + EnsureFocusMaterial(); + _driver = cameraObject.AddComponent(); + _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(); + } + } +} diff --git a/Assets/Scripts/SceneManagement/Day2Sleep/KitePresentation.cs.meta b/Assets/Scripts/SceneManagement/Day2Sleep/KitePresentation.cs.meta new file mode 100644 index 000000000..b923efd8a --- /dev/null +++ b/Assets/Scripts/SceneManagement/Day2Sleep/KitePresentation.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 6ade30c6ea7b492e8ee1ee2b32141673 diff --git a/Assets/Scripts/SceneManagement/KiteRigDriver.cs b/Assets/Scripts/SceneManagement/Day2Sleep/KiteRigDriver.cs similarity index 99% rename from Assets/Scripts/SceneManagement/KiteRigDriver.cs rename to Assets/Scripts/SceneManagement/Day2Sleep/KiteRigDriver.cs index 732c0c92c..8519c169b 100644 --- a/Assets/Scripts/SceneManagement/KiteRigDriver.cs +++ b/Assets/Scripts/SceneManagement/Day2Sleep/KiteRigDriver.cs @@ -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; diff --git a/Assets/Scripts/SceneManagement/KiteRigDriver.cs.meta b/Assets/Scripts/SceneManagement/Day2Sleep/KiteRigDriver.cs.meta similarity index 100% rename from Assets/Scripts/SceneManagement/KiteRigDriver.cs.meta rename to Assets/Scripts/SceneManagement/Day2Sleep/KiteRigDriver.cs.meta diff --git a/Assets/Scripts/SceneManagement/Day2Sleep/LargeSpriteEffectsPresenter.cs b/Assets/Scripts/SceneManagement/Day2Sleep/LargeSpriteEffectsPresenter.cs new file mode 100644 index 000000000..90e4dc537 --- /dev/null +++ b/Assets/Scripts/SceneManagement/Day2Sleep/LargeSpriteEffectsPresenter.cs @@ -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); + } + } +} diff --git a/Assets/Scripts/SceneManagement/Day2Sleep/LargeSpriteEffectsPresenter.cs.meta b/Assets/Scripts/SceneManagement/Day2Sleep/LargeSpriteEffectsPresenter.cs.meta new file mode 100644 index 000000000..8583b2200 --- /dev/null +++ b/Assets/Scripts/SceneManagement/Day2Sleep/LargeSpriteEffectsPresenter.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 2a1c4fea897e46bf94ed561eaa210ceb diff --git a/Assets/Scripts/SceneManagement/Day2Sleep/ReelMotionPresenter.cs b/Assets/Scripts/SceneManagement/Day2Sleep/ReelMotionPresenter.cs new file mode 100644 index 000000000..b82594361 --- /dev/null +++ b/Assets/Scripts/SceneManagement/Day2Sleep/ReelMotionPresenter.cs @@ -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(); + 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(); + _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; + } + } +} diff --git a/Assets/Scripts/SceneManagement/Day2Sleep/ReelMotionPresenter.cs.meta b/Assets/Scripts/SceneManagement/Day2Sleep/ReelMotionPresenter.cs.meta new file mode 100644 index 000000000..ec5b075cd --- /dev/null +++ b/Assets/Scripts/SceneManagement/Day2Sleep/ReelMotionPresenter.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: e5bccd7819704be4afad76507faac330 diff --git a/Assets/Scripts/SceneManagement/DreamDoorSystem.cs b/Assets/Scripts/SceneManagement/DreamDoorSystem.cs index 2f2396cde..1af15f640 100644 --- a/Assets/Scripts/SceneManagement/DreamDoorSystem.cs +++ b/Assets/Scripts/SceneManagement/DreamDoorSystem.cs @@ -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 /// 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); } /// @@ -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) diff --git a/Assets/Scripts/SceneManagement/SpriteShowcase.cs b/Assets/Scripts/SceneManagement/SpriteShowcase.cs index dddc79d05..d54a77342 100644 --- a/Assets/Scripts/SceneManagement/SpriteShowcase.cs +++ b/Assets/Scripts/SceneManagement/SpriteShowcase.cs @@ -1,281 +1,45 @@ -using System; +using System; using System.Collections; using AibisDream.Framework; using AibisDream.Kit; -using AibisDream.Utility;using DG.Tweening; +using AibisDream.SaveSystem; +using AibisDream.Utility; +using DG.Tweening; using UnityEngine; using UnityEngine.ResourceManagement.AsyncOperations; namespace AibisDream { + /// + /// 梦境场景通用图片展示器。只负责普通小图、大图与背景遮罩; + /// 场景专属动态表现通过 与本组件解耦。 + /// public class SpriteShowcase : Singleton { - /// Addressable Key 模板,与 Scene_Dream 中梦境图 Address 一致。 private const string DreamSpritePath = "Sprite/梦境/{0}.png"; - private const string KiteBaseSuffix = "_底图"; - private const string KiteMotionSuffix = "_风筝层"; - /// 轱辘拉锯整体倍速;>1 更快。结尾全力拉住时用偏高倍速表现用力带来的身体微颤。 - private const float ReelMotionSpeedMultiplier = 2.3f; - /// 轱辘拉锯位移/旋转幅度倍率;偏小以贴近发力颤抖而非大幅晃动。 - private const float ReelMotionAmplitude = 0.22f; [SerializeField] private SpriteRenderer _spriteRenderer; [SerializeField] private SpriteRenderer _backgroundSpriteRenderer; [SerializeField] private SpriteRenderer _largeSpriteRenderer; - private SpriteRenderer _smallMotionRenderer; - private SpriteMask _smallMotionMask; - private DG.Tweening.Sequence _smallMotionSequence; - private Vector3 _smallBaseLocalPosition; - private Quaternion _smallBaseLocalRotation; - private Vector3 _smallBaseLocalScale; - private Vector3 _smallMotionBaseOffset; private string _currentSmallPicName; private string _currentLargePicName; - private bool _currentSmallUsesSplitLayers; - private DG.Tweening.Sequence _largePresentationSequence; - private Vector3 _largeBaseLocalPosition; - private Quaternion _largeBaseLocalRotation; - private Vector3 _largeBaseLocalScale; - private float _largeBaseAlpha; - private Material _largeBaseMaterial; - private MaterialPropertyBlock _largePropertyBlock; - private Coroutine _largeSpriteAnimationCoroutine; + public event Action PresentationChanging; - private static readonly int BlurAmountId = Shader.PropertyToID("_BlurAmount"); - private static readonly int BlurSizeId = Shader.PropertyToID("_BlurSize"); - private static readonly int UseSoftFocusId = Shader.PropertyToID("_UseSoftFocus"); - - // 通过 activeSelf 判断当前状态 - private bool IsSmallShowing => _spriteRenderer.gameObject.activeSelf; + private bool IsSmallShowing => _spriteRenderer != null && _spriteRenderer.gameObject.activeSelf; private bool IsLargeShowing => _largeSpriteRenderer != null && _largeSpriteRenderer.gameObject.activeSelf; public override void OnSingletonInit() { - CaptureSmallBaseTransform(); - CaptureLargeBaseTransform(); - - // 初始状态:隐藏 - _spriteRenderer.gameObject.SetActive(false); - _spriteRenderer.SetAlpha(0); - - // 大图初始状态:隐藏 - if (_largeSpriteRenderer != null) - { - _largeSpriteRenderer.gameObject.SetActive(false); - _largeSpriteRenderer.SetAlpha(0); - } + ClearSmallImmediate(); + ClearLargeImmediate(); } private void OnDisable() { - HideLargePresentationImmediate(); - } - - public override void OnSingletonDestroy() - { - HideLargePresentationImmediate(); - base.OnSingletonDestroy(); - } - - private void CaptureSmallBaseTransform() - { - if (_spriteRenderer == null) return; - - var tr = _spriteRenderer.transform; - _smallBaseLocalPosition = tr.localPosition; - _smallBaseLocalRotation = tr.localRotation; - _smallBaseLocalScale = tr.localScale; - } - - private void CaptureLargeBaseTransform() - { - if (_largeSpriteRenderer == null) return; - - var tr = _largeSpriteRenderer.transform; - _largeBaseLocalPosition = tr.localPosition; - _largeBaseLocalRotation = tr.localRotation; - _largeBaseLocalScale = tr.localScale; - _largeBaseAlpha = _largeSpriteRenderer.color.a; - _largeBaseMaterial = _largeSpriteRenderer.sharedMaterial; - } - - private void StopLargeSpriteAnimation() - { - if (_largeSpriteAnimationCoroutine == null) return; - - StopCoroutine(_largeSpriteAnimationCoroutine); - _largeSpriteAnimationCoroutine = null; - } - - private IEnumerator LoopLargeSpriteAnimation(Sprite[] frames, float secondsPerFrame) - { - var frameIndex = 0; - var wait = new WaitForSeconds(Mathf.Max(0.01f, secondsPerFrame)); - - while (frames.Length > 0) - { - if (_largeSpriteRenderer != null) - { - _largeSpriteRenderer.sprite = frames[frameIndex]; - } - - frameIndex = (frameIndex + 1) % frames.Length; - yield return wait; - } - } - - private void ApplyLargeSpriteBlur(float amount, float blurSize) - { - if (_largeSpriteRenderer == null) return; - - amount = Mathf.Clamp01(amount); - if (amount <= 0.001f) - { - _largeSpriteRenderer.SetPropertyBlock(null); - _largeSpriteRenderer.sharedMaterial = _largeBaseMaterial; - return; - } - - if (_kiteFocusMaterial == null) - { - Debug.LogWarning("[SpriteShowcase] 未配置全屏图片可用的对焦材质。", this); - return; - } - - _largeSpriteRenderer.sharedMaterial = _kiteFocusMaterial; - _largePropertyBlock ??= new MaterialPropertyBlock(); - _largeSpriteRenderer.GetPropertyBlock(_largePropertyBlock); - _largePropertyBlock.SetFloat(BlurAmountId, amount); - _largePropertyBlock.SetFloat(BlurSizeId, Mathf.Max(0f, blurSize)); - _largePropertyBlock.SetFloat(UseSoftFocusId, 1f); - _largeSpriteRenderer.SetPropertyBlock(_largePropertyBlock); - } - - public void SetLargeSpriteBlur(float amount, float blurSize = 0.012f) - { - ApplyLargeSpriteBlur(amount, blurSize); - } - - public IEnumerator TweenLargeSpriteBlur(float targetAmount, float duration, float blurSize = 0.012f) - { - if (_largeSpriteRenderer == null) yield break; - - _largePropertyBlock ??= new MaterialPropertyBlock(); - _largeSpriteRenderer.GetPropertyBlock(_largePropertyBlock); - var startAmount = _largeSpriteRenderer.sharedMaterial == _kiteFocusMaterial - ? _largePropertyBlock.GetFloat(BlurAmountId) - : 0f; - targetAmount = Mathf.Clamp01(targetAmount); - duration = Mathf.Max(0f, duration); - - if (duration <= 0f) - { - ApplyLargeSpriteBlur(targetAmount, blurSize); - yield break; - } - - var elapsed = 0f; - while (elapsed < duration) - { - elapsed += Time.deltaTime; - var t = Mathf.Clamp01(elapsed / duration); - t = t * t * (3f - 2f * t); - ApplyLargeSpriteBlur(Mathf.Lerp(startAmount, targetAmount, t), blurSize); - yield return null; - } - - ApplyLargeSpriteBlur(targetAmount, blurSize); - } - - public IEnumerator AnimateLargePresentation(float scaleMultiplier, float targetAlpha, float duration) - { - if (_largeSpriteRenderer == null) yield break; - - KillLargePresentationTweens(); - _largeSpriteRenderer.gameObject.SetActive(true); - - scaleMultiplier = Mathf.Max(0.01f, scaleMultiplier); - targetAlpha = Mathf.Clamp01(targetAlpha); - duration = Mathf.Max(0f, duration); - - var targetScale = new Vector3( - _largeBaseLocalScale.x * scaleMultiplier, - _largeBaseLocalScale.y * scaleMultiplier, - _largeBaseLocalScale.z); - - if (duration <= 0f) - { - _largeSpriteRenderer.transform.localPosition = _largeBaseLocalPosition; - _largeSpriteRenderer.transform.localRotation = _largeBaseLocalRotation; - _largeSpriteRenderer.transform.localScale = targetScale; - _largeSpriteRenderer.SetAlpha(targetAlpha); - yield break; - } - - _largePresentationSequence = DOTween.Sequence(); - _largePresentationSequence.Join( - _largeSpriteRenderer.transform.DOScale(targetScale, duration).SetEase(Ease.InOutSine)); - _largePresentationSequence.Join( - _largeSpriteRenderer.transform.DOLocalMove(_largeBaseLocalPosition, duration).SetEase(Ease.InOutSine)); - _largePresentationSequence.Join( - _largeSpriteRenderer.DOFade(targetAlpha, duration).SetEase(Ease.InOutSine)); - - yield return _largePresentationSequence.WaitForCompletion(); - _largePresentationSequence = null; - } - - public void SetLargePresentationImmediate(float scaleMultiplier, float alpha) - { - if (_largeSpriteRenderer == null) return; - - KillLargePresentationTweens(); - scaleMultiplier = Mathf.Max(0.01f, scaleMultiplier); - - _largeSpriteRenderer.transform.localPosition = _largeBaseLocalPosition; - _largeSpriteRenderer.transform.localRotation = _largeBaseLocalRotation; - _largeSpriteRenderer.transform.localScale = new Vector3( - _largeBaseLocalScale.x * scaleMultiplier, - _largeBaseLocalScale.y * scaleMultiplier, - _largeBaseLocalScale.z); - _largeSpriteRenderer.SetAlpha(Mathf.Clamp01(alpha)); - } - - public void StopLargePresentationAnimation() - { - KillLargePresentationTweens(); - } - - public void ResetLargePresentationImmediate() - { - if (_largeSpriteRenderer == null) return; - - KillLargePresentationTweens(); - _largeSpriteRenderer.transform.localPosition = _largeBaseLocalPosition; - _largeSpriteRenderer.transform.localRotation = _largeBaseLocalRotation; - _largeSpriteRenderer.transform.localScale = _largeBaseLocalScale; - _largeSpriteRenderer.SetAlpha(_largeBaseAlpha); - } - - public void HideLargePresentationImmediate() - { - if (_largeSpriteRenderer == null) return; - - StopLargeSpriteAnimation(); - ApplyLargeSpriteBlur(0f, 0f); - ResetLargePresentationImmediate(); - _largeSpriteRenderer.gameObject.SetActive(false); - } - - private void KillLargePresentationTweens() - { - _largePresentationSequence?.Kill(); - _largePresentationSequence = null; - - if (_largeSpriteRenderer == null) return; - DOTween.Kill(_largeSpriteRenderer); - DOTween.Kill(_largeSpriteRenderer.transform); + ClearSmallImmediate(); + ClearLargeImmediate(); } private static string NormalizeDreamPicName(string picName) @@ -292,6 +56,12 @@ namespace AibisDream Action completed = null, Action warning = null) { + if (target == null) + { + completed?.Invoke(false); + yield break; + } + var key = string.Format(DreamSpritePath, NormalizeDreamPicName(picName)); var handle = ResourceSystem.LoadAsync(key); yield return handle; @@ -303,403 +73,21 @@ namespace AibisDream else { var message = $"[SpriteShowcase] Failed to load sprite: {key}"; - Debug.LogError(message); + Debug.LogError(message, this); warning?.Invoke(message); completed?.Invoke(false); } } - private void EnsureSmallMotionRenderer() + public IEnumerator ShowObj(string picName, float duration = 1f) { - if (_smallMotionRenderer != null || _spriteRenderer == null) return; + NotifyPresentationChanging(); - var motionObject = new GameObject($"{_spriteRenderer.name} Motion"); - motionObject.transform.SetParent(_spriteRenderer.transform.parent); - _smallMotionRenderer = motionObject.AddComponent(); - CopySmallRendererSettings(_smallMotionRenderer, 1, SpriteMaskInteraction.VisibleInsideMask); - _smallMotionRenderer.gameObject.SetActive(false); - _smallMotionRenderer.SetAlpha(0); - } - - private void CopySmallRendererSettings(SpriteRenderer target, int sortingOrderOffset, SpriteMaskInteraction maskInteraction) - { - if (target == null || _spriteRenderer == null) return; - - target.sharedMaterial = _spriteRenderer.sharedMaterial; - target.sortingLayerID = _spriteRenderer.sortingLayerID; - target.sortingOrder = _spriteRenderer.sortingOrder + sortingOrderOffset; - target.drawMode = _spriteRenderer.drawMode; - target.size = _spriteRenderer.size; - target.flipX = _spriteRenderer.flipX; - target.flipY = _spriteRenderer.flipY; - target.maskInteraction = maskInteraction; - } - - private void EnsureSmallMotionMask() - { - if (_smallMotionMask != null || _spriteRenderer == null) return; - - var maskObject = new GameObject($"{_spriteRenderer.name} Motion Mask"); - maskObject.transform.SetParent(_spriteRenderer.transform.parent); - _smallMotionMask = maskObject.AddComponent(); - _smallMotionMask.alphaCutoff = 0.01f; - _smallMotionMask.isCustomRangeActive = true; - _smallMotionMask.enabled = false; - } - - private void ConfigureSmallMotionMask() - { - EnsureSmallMotionMask(); - if (_smallMotionMask == null) return; - - _smallMotionMask.sprite = _spriteRenderer.sprite; - _smallMotionMask.frontSortingLayerID = _spriteRenderer.sortingLayerID; - _smallMotionMask.backSortingLayerID = _spriteRenderer.sortingLayerID; - _smallMotionMask.frontSortingOrder = _spriteRenderer.sortingOrder + 2; - _smallMotionMask.backSortingOrder = _spriteRenderer.sortingOrder; - - var maskTr = _smallMotionMask.transform; - maskTr.localPosition = _smallBaseLocalPosition; - maskTr.localRotation = _smallBaseLocalRotation; - maskTr.localScale = _smallBaseLocalScale; - _smallMotionMask.enabled = true; - } - - private void KillSmallTweenState() - { - _smallMotionSequence?.Kill(); - _smallMotionSequence = null; - } - - private void ResetSmallTransform(bool resetMotionLayer = true) - { - if (_spriteRenderer == null) return; - - var tr = _spriteRenderer.transform; - tr.localPosition = _smallBaseLocalPosition; - tr.localRotation = _smallBaseLocalRotation; - tr.localScale = _smallBaseLocalScale; - - if (!resetMotionLayer || _smallMotionRenderer == null) return; - - var motionTr = _smallMotionRenderer.transform; - motionTr.localPosition = _smallBaseLocalPosition + _smallMotionBaseOffset; - motionTr.localRotation = _smallBaseLocalRotation; - motionTr.localScale = _smallBaseLocalScale; - } - - private static string NormalizeMotionProfile(string motionProfile) - { - return string.IsNullOrEmpty(motionProfile) - ? string.Empty - : motionProfile.Trim().ToLowerInvariant(); - } - - private static bool IsReelPic(string picName) - { - var normalized = NormalizeDreamPicName(picName); - return normalized == "D2S轱辘" || normalized == "D2S轱辘没线"; - } - - private Vector3 BaseOffset(float x, float y) - { - return _smallBaseLocalPosition + _smallMotionBaseOffset + new Vector3(x, y, 0f); - } - - private static bool TryGetSplitMotionPicNames(string picName, out string basePicName, out string motionPicName) - { - var normalized = NormalizeDreamPicName(picName); - - // 风筝图已改由风筝 Rig(ShowKite 等)连续驱动,分层底图+遮罩通路仅保留给轱辘特写。 - // 普通阶段用有线轱辘,结尾受力晃动改用无线轱辘;两张图各自使用对应的拆分层。 - if (IsReelPic(normalized)) - { - basePicName = $"{normalized}{KiteBaseSuffix}"; - motionPicName = $"{normalized}{KiteMotionSuffix}"; - return true; - } - - basePicName = picName; - motionPicName = null; - return false; - } - - private void PlaySmallMotion(string motionProfile) - { - _smallMotionSequence?.Kill(); - _smallMotionSequence = null; - - var profile = NormalizeMotionProfile(motionProfile); - if (string.IsNullOrEmpty(profile) || profile == "none") return; - - var target = _smallMotionRenderer != null && _smallMotionRenderer.gameObject.activeSelf - ? _smallMotionRenderer - : _spriteRenderer; - var tr = target.transform; - var seq = DOTween.Sequence(); - - switch (profile) - { - // 轱辘:全力拉住——先被拽进受力姿态(只进一次),之后进入拔河式的角力拉锯。 - // 拉锯循环见 PlayReelHold。 - case "reel_tension": - seq.Append(tr.DOLocalMove( - BaseOffset(0.13f * ReelMotionAmplitude, -0.32f * ReelMotionAmplitude), 0.38f) - .SetEase(Ease.OutCubic)); - seq.Join(tr.DOLocalRotate( - new Vector3(0f, 0f, -2.8f * ReelMotionAmplitude), 0.38f) - .SetEase(Ease.OutCubic)); - seq.OnComplete(() => PlayReelHold(tr)); - break; - - default: - seq.Kill(); - return; - } - - seq.timeScale = ReelMotionSpeedMultiplier; - _smallMotionSequence = seq; - } - - /// - /// 轱辘“拔河角力”常驻循环:一场正在输掉的拔河。 - /// strain 随回合递增(0→1):节奏越来越快、姿态被拽得越来越深、猛夺越来越频繁; - /// 往回挣不再是平滑回弹,而是“向下被拖住→抬回一点→又沉下去→再咬住”的三段拉锯。 - /// 每回合参数随机,OnComplete 续下一回合;用纵向位移和停顿表达强拉,避免机械抖动。 - /// - private void PlayReelHold(Transform tr, float strain = 0f) - { - _smallMotionSequence?.Kill(); - - // 姿态整体随 strain 加深;时长随 strain 略收紧。旋转只做小角度,避免贴图边缘穿帮。 - // deep*/rotBias 先按原尺度算,最后统一乘 amp,避免叠乘。 - var amp = ReelMotionAmplitude; - var deepX = strain * 0.03f; - var deepY = strain * 0.07f; - // strain 越高越快,但收紧幅度比原先更克制,避免后期抖得诡异。 - var speed = Mathf.Lerp(1f, 0.78f, strain); - var rotBias = strain * 0.35f; - - var seq = DOTween.Sequence(); - - // 对面加力:被一点点拽深 - var strainDur = UnityEngine.Random.Range(0.55f, 0.9f) * speed; - seq.Append(tr.DOLocalMove( - BaseOffset((UnityEngine.Random.Range(0.17f, 0.23f) + deepX) * amp, - (UnityEngine.Random.Range(-0.42f, -0.34f) - deepY) * amp), - strainDur) - .SetEase(Ease.InOutSine)); - seq.Join(tr.DOLocalRotate( - new Vector3(0f, 0f, (UnityEngine.Random.Range(-3.6f, -2.4f) - rotBias) * amp), strainDur) - .SetEase(Ease.InOutSine)); - - // 猛夺一小截:概率与深度都随 strain 提升;夺完手上发麻—— - // 不是连续 shake,而是被向下拽住、短暂停顿,再勉强抬回一点。 - if (UnityEngine.Random.value < 0.4f + strain * 0.25f) - { - var yankDur = UnityEngine.Random.Range(0.18f, 0.28f) * speed; - seq.Append(tr.DOLocalMove( - BaseOffset((UnityEngine.Random.Range(0.25f, 0.34f) + deepX) * amp, - (UnityEngine.Random.Range(-0.62f, -0.5f) - deepY) * amp), - yankDur) - .SetEase(Ease.OutCubic)); - seq.Join(tr.DOLocalRotate( - new Vector3(0f, 0f, (UnityEngine.Random.Range(-4.8f, -3.4f) - rotBias) * amp), yankDur) - .SetEase(Ease.OutCubic)); - seq.AppendInterval(UnityEngine.Random.Range(0.1f, 0.18f) * speed); - var biteDur = UnityEngine.Random.Range(0.2f, 0.32f) * speed; - seq.Append(tr.DOLocalMove( - BaseOffset((UnityEngine.Random.Range(0.19f, 0.27f) + deepX) * amp, - (UnityEngine.Random.Range(-0.48f, -0.4f) - deepY) * amp), - biteDur) - .SetEase(Ease.OutCubic)); - seq.Join(tr.DOLocalRotate( - new Vector3(0f, 0f, (UnityEngine.Random.Range(-3.4f, -2.2f) - rotBias) * amp), biteDur) - .SetEase(Ease.OutCubic)); - } - - // 往回挣(一):咬牙拉回一点 - var recoverDur = UnityEngine.Random.Range(0.28f, 0.42f) * speed; - seq.Append(tr.DOLocalMove( - BaseOffset((UnityEngine.Random.Range(0.12f, 0.16f) + deepX) * amp, - (UnityEngine.Random.Range(-0.32f, -0.26f) - deepY * 0.55f) * amp), - recoverDur) - .SetEase(Ease.OutCubic)); - seq.Join(tr.DOLocalRotate( - new Vector3(0f, 0f, (UnityEngine.Random.Range(-2.3f, -1.3f) - rotBias * 0.7f) * amp), recoverDur) - .SetEase(Ease.OutCubic)); - // 滑走半截:手上又被抽走一段 - var slipDur = UnityEngine.Random.Range(0.16f, 0.26f) * speed; - seq.Append(tr.DOLocalMove( - BaseOffset((UnityEngine.Random.Range(0.2f, 0.27f) + deepX) * amp, - (UnityEngine.Random.Range(-0.5f, -0.4f) - deepY) * amp), - slipDur) - .SetEase(Ease.OutQuad)); - seq.Join(tr.DOLocalRotate( - new Vector3(0f, 0f, (UnityEngine.Random.Range(-4.1f, -2.8f) - rotBias) * amp), slipDur) - .SetEase(Ease.OutQuad)); - // 三成回合再缓缓向下沉半口气又咬住,避免碎步太密显得机械。 - if (UnityEngine.Random.value < 0.3f) - { - var dipDur = UnityEngine.Random.Range(0.14f, 0.22f) * speed; - seq.Append(tr.DOLocalMove( - BaseOffset((UnityEngine.Random.Range(0.21f, 0.28f) + deepX) * amp, - (UnityEngine.Random.Range(-0.56f, -0.47f) - deepY) * amp), - dipDur) - .SetEase(Ease.OutQuad)); - seq.Join(tr.DOLocalRotate( - new Vector3(0f, 0f, (UnityEngine.Random.Range(-4.4f, -3f) - rotBias) * amp), dipDur) - .SetEase(Ease.OutQuad)); - var braceDur = UnityEngine.Random.Range(0.18f, 0.28f) * speed; - seq.Append(tr.DOLocalMove( - BaseOffset((UnityEngine.Random.Range(0.16f, 0.22f) + deepX) * amp, - (UnityEngine.Random.Range(-0.4f, -0.33f) - deepY) * amp), - braceDur) - .SetEase(Ease.OutCubic)); - seq.Join(tr.DOLocalRotate( - new Vector3(0f, 0f, (UnityEngine.Random.Range(-3f, -1.8f) - rotBias) * amp), braceDur) - .SetEase(Ease.OutCubic)); - } - // 往回挣(二):再咬住,但回不到最初的姿态 - var pullDur = UnityEngine.Random.Range(0.4f, 0.58f) * speed; - seq.Append(tr.DOLocalMove( - BaseOffset((UnityEngine.Random.Range(0.13f, 0.18f) + deepX) * amp, - (UnityEngine.Random.Range(-0.35f, -0.28f) - deepY * 0.75f) * amp), - pullDur) - .SetEase(Ease.InOutSine)); - seq.Join(tr.DOLocalRotate( - new Vector3(0f, 0f, (UnityEngine.Random.Range(-2.8f, -1.6f) - rotBias) * amp), pullDur) - .SetEase(Ease.InOutSine)); - // 偶尔结尾再沉一下,像手腕被拖住,不做高频震动。 - if (UnityEngine.Random.value < 0.25f) - { - var sagDur = UnityEngine.Random.Range(0.2f, 0.32f) * speed; - seq.Append(tr.DOLocalMove( - BaseOffset((UnityEngine.Random.Range(0.18f, 0.24f) + deepX) * amp, - (UnityEngine.Random.Range(-0.46f, -0.38f) - deepY) * amp), - sagDur) - .SetEase(Ease.InOutSine)); - seq.Join(tr.DOLocalRotate( - new Vector3(0f, 0f, (UnityEngine.Random.Range(-3.5f, -2.2f) - rotBias) * amp), sagDur) - .SetEase(Ease.InOutSine)); - } - - // strain 爬升更慢,后期不会很快进入“狂抖” - seq.OnComplete(() => PlayReelHold(tr, Mathf.Min(1f, strain + 0.12f))); - seq.timeScale = ReelMotionSpeedMultiplier; - _smallMotionSequence = seq; - } - - public void PlayCurrentMotion(string motionProfile) - { - if (_spriteRenderer == null || !IsSmallShowing) return; - PlaySmallMotion(motionProfile); - } - - private void ClearSmallImmediate() - { - KillSmallTweenState(); - ResetSmallTransform(); - - if (_spriteRenderer != null) - { - _spriteRenderer.DOKill(); - _spriteRenderer.SetAlpha(0f); - _spriteRenderer.gameObject.SetActive(false); - } - - _currentSmallPicName = null; - _currentSmallUsesSplitLayers = false; - - if (_smallMotionRenderer != null) - { - _smallMotionRenderer.DOKill(); - _smallMotionRenderer.SetAlpha(0f); - _smallMotionRenderer.gameObject.SetActive(false); - _smallMotionRenderer.maskInteraction = SpriteMaskInteraction.None; - } - - if (_smallMotionMask != null) - { - _smallMotionMask.enabled = false; - } - } - - private void ClearLargeImmediate() - { - StopLargeSpriteAnimation(); - ApplyLargeSpriteBlur(0f, 0f); - - if (_largeSpriteRenderer != null) - { - _largeSpriteRenderer.DOKill(); - _largeSpriteRenderer.SetAlpha(0f); - _largeSpriteRenderer.gameObject.SetActive(false); - } - - _currentLargePicName = null; - } - - private void ClearKiteImmediate() - { - KillKiteTweens(); - if (_kiteRenderer != null) - { - _kiteRenderer.SetAlpha(0f); - } - - if (_kiteBaseRenderer != null) - { - _kiteBaseRenderer.SetAlpha(0f); - _kiteBaseRenderer.gameObject.SetActive(false); - } - - if (_kiteCameraNode != null) - { - _kiteCameraNode.gameObject.SetActive(false); - } - - if (_kiteMask != null) - { - _kiteMask.enabled = false; - } - } - - /// - /// 显示物体(小图) - /// - /// 图片名(无扩展名,与 Yarn 中一致;Addressable Key 由 拼接) - /// 淡入时长 - /// 协程 - public IEnumerator ShowObj(string picName, float duration = 1) - { - // 互斥:如果大图正在显示,先隐藏 if (IsLargeShowing) - { - yield return HideLargeObj(duration); - } - - // 互斥:风筝 Rig 正在显示时先隐藏 - if (IsKiteShowing) - { - yield return HideKite(Mathf.Min(duration, 0.25f)); - } - - KillSmallTweenState(); - ResetSmallTransform(); - - if (_smallMotionRenderer != null) - { - _smallMotionRenderer.gameObject.SetActive(false); - _smallMotionRenderer.SetAlpha(0); - _smallMotionRenderer.maskInteraction = SpriteMaskInteraction.None; - } - - if (_smallMotionMask != null) - { - _smallMotionMask.enabled = false; - } + yield return HideLargeInternal(duration); + if (_spriteRenderer == null) yield break; + _spriteRenderer.DOKill(); _spriteRenderer.gameObject.SetActive(true); var loaded = false; @@ -711,116 +99,21 @@ namespace AibisDream } _currentSmallPicName = NormalizeDreamPicName(picName); - _currentSmallUsesSplitLayers = false; - - yield return _spriteRenderer.DOFade(1, duration).WaitForCompletion(); + if (duration <= 0f) + _spriteRenderer.SetAlpha(1f); + else + yield return _spriteRenderer.DOFade(1f, duration).WaitForCompletion(); } - public IEnumerator ShowObjWithMotion(string picName, string motionProfile, float duration) + public IEnumerator ShowLargeObj(string picName, float duration = 1f) { - // 互斥:如果大图正在显示,先隐藏 - if (IsLargeShowing) - { - yield return HideLargeObj(duration); - } + NotifyPresentationChanging(); - // 互斥:风筝 Rig 正在显示时先隐藏 - if (IsKiteShowing) - { - yield return HideKite(Mathf.Min(duration, 0.25f)); - } - - var normalizedName = NormalizeDreamPicName(picName); - var useSplitMotion = TryGetSplitMotionPicNames(picName, out var basePicName, out var motionPicName); - var isSamePic = string.Equals(_currentSmallPicName, normalizedName, StringComparison.OrdinalIgnoreCase) - && _currentSmallUsesSplitLayers == useSplitMotion; - _smallMotionBaseOffset = IsReelPic(normalizedName) ? new Vector3(0f, -0.32f, 0f) : Vector3.zero; - - KillSmallTweenState(); - - ResetSmallTransform(resetMotionLayer: !isSamePic); - _spriteRenderer.gameObject.SetActive(true); - - if (!isSamePic || _spriteRenderer.sprite == null) - { - var baseLoaded = false; - yield return LoadDreamSpriteInto( - _spriteRenderer, - useSplitMotion ? basePicName : picName, - value => baseLoaded = value); - if (!baseLoaded) - { - ClearSmallImmediate(); - yield break; - } - - _currentSmallPicName = normalizedName; - } - _spriteRenderer.SetAlpha(1f); - - if (useSplitMotion) - { - EnsureSmallMotionRenderer(); - _smallMotionRenderer.gameObject.SetActive(true); - CopySmallRendererSettings(_smallMotionRenderer, 1, SpriteMaskInteraction.VisibleInsideMask); - ConfigureSmallMotionMask(); - ResetSmallTransform(resetMotionLayer: !isSamePic); - if (!isSamePic || _smallMotionRenderer.sprite == null) - { - var motionLoaded = false; - yield return LoadDreamSpriteInto( - _smallMotionRenderer, - motionPicName, - value => motionLoaded = value); - if (!motionLoaded) - { - ClearSmallImmediate(); - yield break; - } - } - _smallMotionRenderer.SetAlpha(1f); - } - else if (_smallMotionRenderer != null) - { - _smallMotionRenderer.gameObject.SetActive(false); - _smallMotionRenderer.SetAlpha(0); - _smallMotionRenderer.maskInteraction = SpriteMaskInteraction.None; - - if (_smallMotionMask != null) - { - _smallMotionMask.enabled = false; - } - } - - _currentSmallPicName = normalizedName; - _currentSmallUsesSplitLayers = useSplitMotion; - PlaySmallMotion(motionProfile); - } - - /// - /// 显示大图 - /// - /// 图片名(无扩展名,与 Yarn 中一致;Addressable Key 由 拼接) - /// 淡入时长 - /// 协程 - public IEnumerator ShowLargeObj(string picName, float duration = 1) - { - StopLargeSpriteAnimation(); - - // 互斥:小图立刻关掉,避免风筝层与底图不同步淡出造成的怪 fade if (IsSmallShowing) - { - yield return HideObj(0f); - } - - if (IsKiteShowing) - { - yield return HideKite(0f); - } + yield return HideSmallInternal(0f); if (_largeSpriteRenderer == null) yield break; - - ResetLargePresentationImmediate(); + _largeSpriteRenderer.DOKill(); _largeSpriteRenderer.gameObject.SetActive(true); _largeSpriteRenderer.SetAlpha(0f); @@ -834,562 +127,176 @@ namespace AibisDream _currentLargePicName = NormalizeDreamPicName(picName); if (duration <= 0f) - { _largeSpriteRenderer.SetAlpha(1f); - } else - { - yield return _largeSpriteRenderer.DOFade(1, duration).WaitForCompletion(); - } - } - - /// - /// 按“前缀1.png … 前缀N.png”加载并循环播放全屏序列帧。 - /// - public IEnumerator ShowLargeSpriteAnimation( - string picNamePrefix, - int frameCount, - float secondsPerFrame = 0.2f, - float duration = 1f) - { - if (_largeSpriteRenderer == null || frameCount <= 0) yield break; - - StopLargeSpriteAnimation(); - - if (IsSmallShowing) - { - yield return HideObj(0f); - } - - if (IsKiteShowing) - { - yield return HideKite(0f); - } - - var frames = new Sprite[frameCount]; - for (var i = 0; i < frameCount; i++) - { - var picName = $"{NormalizeDreamPicName(picNamePrefix)}{i + 1}"; - var key = string.Format(DreamSpritePath, picName); - var handle = ResourceSystem.LoadAsync(key); - yield return handle; - - if (handle.Status != AsyncOperationStatus.Succeeded || handle.Result == null) - { - Debug.LogError($"[SpriteShowcase] Failed to load animation frame: {key}"); - ClearLargeImmediate(); - yield break; - } - - frames[i] = handle.Result; - } - - ResetLargePresentationImmediate(); - _largeSpriteRenderer.gameObject.SetActive(true); - _largeSpriteRenderer.sprite = frames[0]; - _largeSpriteRenderer.SetAlpha(0f); - _currentLargePicName = $"{NormalizeDreamPicName(picNamePrefix)}1"; - _largeSpriteAnimationCoroutine = StartCoroutine( - LoopLargeSpriteAnimation(frames, secondsPerFrame)); - - if (duration <= 0f) - { - _largeSpriteRenderer.SetAlpha(1f); - } - else - { yield return _largeSpriteRenderer.DOFade(1f, duration).WaitForCompletion(); - } } - public void SetLargeSpriteImmediate(Sprite sprite) + public IEnumerator HideObj(float duration = 1f) { - if (_largeSpriteRenderer != null && sprite != null) - _largeSpriteRenderer.sprite = sprite; + yield return HideObj(false, duration); } - /// - /// 隐藏物体(小图) - /// - /// 淡出时长 - /// 协程 - public IEnumerator HideObj(float duration = 1) + public IEnumerator HideObj(bool force, float duration = 1f) { - yield return HideObj(force: false, duration); - } - - /// - /// 隐藏物体(小图) - /// - /// 是否强制隐藏 - /// 淡出时长 - /// 协程 - public IEnumerator HideObj(bool force, float duration = 1) - { - // 风筝 Rig 视作小图的一种,hide_sprite 时一并隐藏 - if (IsKiteShowing) - { - yield return HideKite(duration); - } - - // force=false 时检查状态,未显示则直接返回 + NotifyPresentationChanging(); if (!force && !IsSmallShowing) yield break; - - KillSmallTweenState(); - ResetSmallTransform(); - - var motionActive = _smallMotionRenderer != null && _smallMotionRenderer.gameObject.activeSelf; - if (duration <= 0f) - { - _spriteRenderer.SetAlpha(0); - if (motionActive) - _smallMotionRenderer.SetAlpha(0); - } - else - { - var fade = _spriteRenderer.DOFade(0, duration); - if (motionActive) - _smallMotionRenderer.DOFade(0, duration); - yield return fade.WaitForCompletion(); - } - - _spriteRenderer.gameObject.SetActive(false); - _currentSmallPicName = null; - _currentSmallUsesSplitLayers = false; - - if (_smallMotionRenderer != null) - { - _smallMotionRenderer.gameObject.SetActive(false); - _smallMotionRenderer.SetAlpha(0); - _smallMotionRenderer.maskInteraction = SpriteMaskInteraction.None; - } - - if (_smallMotionMask != null) - { - _smallMotionMask.enabled = false; - } + yield return HideSmallInternal(duration); } - - /// - /// 隐藏大图 - /// - /// 是否强制隐藏 - /// 淡出时长 - /// 协程 - public IEnumerator HideLargeObj(float duration = 1) + + public IEnumerator HideLargeObj(float duration = 1f) + { + NotifyPresentationChanging(); + if (!IsLargeShowing) yield break; + yield return HideLargeInternal(duration); + } + + public IEnumerator HideAll(float duration = 1f) + { + NotifyPresentationChanging(); + if (IsSmallShowing) + yield return HideSmallInternal(duration); + if (IsLargeShowing) + yield return HideLargeInternal(duration); + } + + private IEnumerator HideSmallInternal(float duration) + { + if (_spriteRenderer == null) yield break; + _spriteRenderer.DOKill(); + if (duration > 0f) + yield return _spriteRenderer.DOFade(0f, duration).WaitForCompletion(); + ClearSmallImmediate(); + } + + private IEnumerator HideLargeInternal(float duration) { if (_largeSpriteRenderer == null) yield break; - - StopLargeSpriteAnimation(); - KillLargePresentationTweens(); - if (duration <= 0f) - { - _largeSpriteRenderer.SetAlpha(0f); - } - else - { - yield return _largeSpriteRenderer.DOFade(0, duration).WaitForCompletion(); - } - - _largeSpriteRenderer.gameObject.SetActive(false); - _currentLargePicName = null; - ApplyLargeSpriteBlur(0f, 0f); - ResetLargePresentationImmediate(); - } - - /// - /// 隐藏所有(小图和大图) - /// - /// 淡出时长 - /// 协程 - public IEnumerator HideAll(float duration = 1) - { - if (IsSmallShowing || IsKiteShowing) - yield return HideObj(duration); - - if (IsLargeShowing) - yield return HideLargeObj(duration); - } - - public void ClearPresentationEffects() - { - ClearSmallImmediate(); + _largeSpriteRenderer.DOKill(); + if (duration > 0f) + yield return _largeSpriteRenderer.DOFade(0f, duration).WaitForCompletion(); ClearLargeImmediate(); - ClearKiteImmediate(); - - if (_backgroundSpriteRenderer != null) - { - _backgroundSpriteRenderer.DOKill(); - _backgroundSpriteRenderer.SetAlpha(0f); - _backgroundSpriteRenderer.gameObject.SetActive(false); - } } - #region 风筝 Rig - - // Kite Camera(虚拟镜头完全补偿)→ Rig(高度+漂移)→ Sway(鱼式流体摆动)→ Accent(DOTween 顿挫) - // 底图 / Mask 保持静态,不进镜头节点。 - private const string KiteSpritePicName = "D2S风筝1_风筝层"; - private const string KiteBasePicName = "D2S风筝1_底图"; - private const float KiteMaxHeight = 13f; - - [Header("风筝 / 对焦材质")] - [SerializeField] private Material _kiteFocusMaterial; - [SerializeField] private KiteWindSettings _kiteWindSettings = new KiteWindSettings(); - [SerializeField] private KiteCameraSettings _kiteCameraSettings = new KiteCameraSettings(); - [SerializeField] private KiteFocusSettings _kiteFocusSettings = new KiteFocusSettings(); - - private Transform _kiteCameraNode; - private Transform _kiteRootPivot; - private Transform _kiteSwayPivot; - private SpriteRenderer _kiteRenderer; - private SpriteRenderer _kiteBaseRenderer; - private SpriteMask _kiteMask; - private KiteRigDriver _kiteDriver; - private DG.Tweening.Sequence _kiteAccentSequence; - private bool _kiteSpriteLoaded; - private bool _kiteBaseSpriteLoaded; - - private bool IsKiteShowing => - _kiteCameraNode != null && _kiteCameraNode.gameObject.activeSelf; - - private void EnsureKiteRig() - { - if (_kiteRootPivot != null || _spriteRenderer == null) return; - - var parent = _spriteRenderer.transform.parent; - - var cameraObject = new GameObject("Kite Camera"); - cameraObject.transform.SetParent(parent); - cameraObject.transform.localPosition = _smallBaseLocalPosition; - cameraObject.transform.localRotation = Quaternion.identity; - cameraObject.transform.localScale = Vector3.one; - - var root = new GameObject("Kite Rig"); - root.transform.SetParent(cameraObject.transform, false); - root.transform.localPosition = Vector3.zero; - root.transform.localRotation = _smallBaseLocalRotation; - root.transform.localScale = _smallBaseLocalScale; - - var sway = new GameObject("Kite Sway"); - sway.transform.SetParent(root.transform, false); - - var accent = new GameObject("Kite Accent"); - accent.transform.SetParent(sway.transform, false); - - // 白色底图:独立静态节点,不随镜头/高度缩放 - var baseObject = new GameObject("Kite Base"); - baseObject.transform.SetParent(parent); - baseObject.transform.localPosition = _smallBaseLocalPosition; - baseObject.transform.localRotation = _smallBaseLocalRotation; - baseObject.transform.localScale = _smallBaseLocalScale; - - // 遮罩:把风筝层限制在底图(天空)范围内 - var maskObject = new GameObject("Kite Mask"); - maskObject.transform.SetParent(parent); - maskObject.transform.localPosition = _smallBaseLocalPosition; - maskObject.transform.localRotation = _smallBaseLocalRotation; - maskObject.transform.localScale = _smallBaseLocalScale; - - _kiteCameraNode = cameraObject.transform; - _kiteRootPivot = root.transform; - _kiteSwayPivot = sway.transform; - _kiteRenderer = accent.AddComponent(); - CopySmallRendererSettings(_kiteRenderer, 1, SpriteMaskInteraction.VisibleInsideMask); - _kiteBaseRenderer = baseObject.AddComponent(); - CopySmallRendererSettings(_kiteBaseRenderer, 0, SpriteMaskInteraction.None); - _kiteMask = maskObject.AddComponent(); - _kiteMask.alphaCutoff = 0.01f; - _kiteMask.isCustomRangeActive = true; - _kiteMask.enabled = false; - - EnsureKiteFocusMaterial(); - _kiteDriver = cameraObject.AddComponent(); - _kiteDriver.Configure( - _kiteCameraNode, - _kiteRootPivot, - _kiteSwayPivot, - _kiteRenderer, - _smallBaseLocalPosition, - _smallBaseLocalRotation, - _smallBaseLocalScale, - _kiteWindSettings, - _kiteCameraSettings, - _kiteFocusSettings, - _kiteFocusMaterial); - _kiteDriver.enabled = false; - - cameraObject.SetActive(false); - baseObject.SetActive(false); - _kiteRenderer.SetAlpha(0); - _kiteBaseRenderer.SetAlpha(0); - } - - private void EnsureKiteFocusMaterial() - { - if (_kiteFocusMaterial != null) return; - - var shader = Shader.Find("AIBIS/KiteFocusSprite"); - if (shader == null) - { - Debug.LogWarning("[SpriteShowcase] 找不到 AIBIS/KiteFocusSprite,风筝虚焦不可用。", this); - return; - } - - _kiteFocusMaterial = new Material(shader) { name = "KiteFocusSprite (Runtime)" }; - } - - private void KillKiteTweens() - { - _kiteAccentSequence?.Kill(); - _kiteAccentSequence = null; - _kiteDriver?.ResetAndSleep(); - } - - /// - /// 显示风筝 Rig。已显示时等价于 (幂等)。 - /// - public IEnumerator ShowKite(float height, int wind, float duration = 0.35f) - { - if (IsLargeShowing) - { - yield return HideLargeObj(duration); - } - - if (IsSmallShowing) - { - yield return HideObj(Mathf.Min(duration, 0.25f)); - } - - EnsureKiteRig(); - if (_kiteRenderer == null || _kiteDriver == null) yield break; - - height = Mathf.Clamp(height, 0f, KiteMaxHeight); - - if (IsKiteShowing) - { - SetKiteState(height, wind, duration); - yield break; - } - - KillKiteTweens(); - _kiteCameraNode.gameObject.SetActive(true); - _kiteBaseRenderer.gameObject.SetActive(true); - - if (!_kiteSpriteLoaded) - { - yield return LoadDreamSpriteInto(_kiteRenderer, KiteSpritePicName); - _kiteSpriteLoaded = _kiteRenderer.sprite != null; - // 贴图加载后重新赋 focus 材质(Load 可能换过 material) - EnsureKiteFocusMaterial(); - if (_kiteFocusMaterial != null) - { - _kiteRenderer.material = _kiteFocusMaterial; - } - } - - if (!_kiteBaseSpriteLoaded) - { - yield return LoadDreamSpriteInto(_kiteBaseRenderer, KiteBasePicName); - _kiteBaseSpriteLoaded = _kiteBaseRenderer.sprite != null; - } - - if (_kiteBaseSpriteLoaded) - { - _kiteMask.sprite = _kiteBaseRenderer.sprite; - _kiteMask.frontSortingLayerID = _kiteRenderer.sortingLayerID; - _kiteMask.backSortingLayerID = _kiteRenderer.sortingLayerID; - _kiteMask.frontSortingOrder = _spriteRenderer.sortingOrder + 2; - _kiteMask.backSortingOrder = _spriteRenderer.sortingOrder; - _kiteMask.enabled = true; - } - - _kiteDriver.Wake(height, wind); - _kiteBaseRenderer.SetAlpha(1f); - _kiteRenderer.SetAlpha(KiteRigDriver.Alpha(height)); - } - - /// - /// 高度 SmoothDamp + 切换风向。乱度粘滞由 driver 保留。 - /// - public void SetKiteState(float height, int wind, float duration = 0.9f) - { - if (!IsKiteShowing || _kiteDriver == null) return; - - height = Mathf.Clamp(height, 0f, KiteMaxHeight); - _kiteDriver.SetHeight(height, duration); - _kiteDriver.SetWind(wind); - } - - /// 仅切换风况(选择前 telegraph 风向)。 - /// 乱度 0~1;负数或省略表示平稳(清零)。设定后粘滞,kite_state 沿用。 - public void SetKiteWind(int wind, float turbulence = -1f) - { - if (!IsKiteShowing || _kiteDriver == null) return; - // 与旧语义对齐:省略/负数 → 平稳(乱度归零);显式传入 ≥0 则设定并粘滞 - float t = turbulence >= 0f ? Mathf.Clamp01(turbulence) : 0f; - _kiteDriver.SetWind(wind, t); - } - - /// - /// 一次性顿挫动效,叠加在 Accent 层上,结束回到原位。 - /// 镜头采样合成位姿,会自然追着 accent 跑。 - /// - public void PlayKiteAccent(string profile) - { - if (!IsKiteShowing) return; - - _kiteAccentSequence?.Kill(); - _kiteAccentSequence = null; - - var tr = _kiteRenderer.transform; - var seq = DOTween.Sequence(); - - switch (NormalizeMotionProfile(profile)) - { - case "lift": - seq.Append(tr.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)); - seq.Join(tr.DOLocalRotate(new Vector3(2f, -1.5f, -2f), 0.62f).SetEase(Ease.InOutSine)); - break; - - case "sink": - seq.Append(tr.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)); - seq.Join(tr.DOLocalRotate(new Vector3(-2f, 1.5f, 2f), 0.68f).SetEase(Ease.InOutSine)); - break; - - case "taut": - seq.Append(tr.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)); - seq.Join(tr.DOLocalRotate(new Vector3(0f, -2f, -3f), 0.62f).SetEase(Ease.OutSine)); - seq.AppendInterval(0.32f); - break; - - case "drop": - seq.Append(tr.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)); - seq.Join(tr.DOLocalRotate(new Vector3(-2.5f, 1f, 3f), 0.86f).SetEase(Ease.InOutSine)); - break; - - case "wrench": - seq.Append(tr.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)); - seq.Join(tr.DOLocalRotate(new Vector3(1.5f, -3f, -6f), 0.92f).SetEase(Ease.InOutSine)); - seq.AppendInterval(0.42f); - break; - - case "flutter": - { - float side = UnityEngine.Random.value < 0.5f ? -1f : 1f; - seq.Append(tr.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)); - seq.Join(tr.DOLocalRotate(new Vector3(1.5f, -2.5f * side, -3f * side), 0.9f) - .SetEase(Ease.InOutSine)); - seq.AppendInterval(0.22f); - break; - } - - default: - seq.Kill(); - return; - } - - seq.Append(tr.DOLocalMove(Vector3.zero, 0.82f).SetEase(Ease.InOutSine)); - seq.Join(tr.DOLocalRotate(Vector3.zero, 0.82f).SetEase(Ease.InOutSine)); - _kiteAccentSequence = seq; - } - - /// - /// 隐藏风筝 Rig。与显示一致不做淡出,直接消失。 - /// - public IEnumerator HideKite(float duration = 1) - { - if (!IsKiteShowing) yield break; - - KillKiteTweens(); - - _kiteRenderer.SetAlpha(0); - _kiteBaseRenderer.SetAlpha(0); - - _kiteCameraNode.gameObject.SetActive(false); - _kiteBaseRenderer.gameObject.SetActive(false); - if (_kiteMask != null) - { - _kiteMask.enabled = false; - } - } - - #endregion - - /// - /// 淡入(异步协程版本) - /// - /// 淡入时长 - /// 协程 - public IEnumerator FadeInAsync(float duration = 1) + public IEnumerator FadeInAsync(float duration = 1f) { if (_backgroundSpriteRenderer == null) yield break; - _backgroundSpriteRenderer.gameObject.SetActive(true); - yield return _backgroundSpriteRenderer.DOFade(1, duration).WaitForCompletion(); + if (duration <= 0f) + _backgroundSpriteRenderer.SetAlpha(1f); + else + yield return _backgroundSpriteRenderer.DOFade(1f, duration).WaitForCompletion(); } - /// - /// 淡出(异步协程版本) - /// - /// 淡出时长 - /// 协程 - public IEnumerator FadeOutAsync(float duration = 1) + public IEnumerator FadeOutAsync(float duration = 1f) { if (_backgroundSpriteRenderer == null) yield break; - _backgroundSpriteRenderer.gameObject.SetActive(true); - yield return _backgroundSpriteRenderer.DOFade(0, duration).WaitForCompletion(); - _backgroundSpriteRenderer.gameObject.SetActive(false); + if (duration > 0f) + yield return _backgroundSpriteRenderer.DOFade(0f, duration).WaitForCompletion(); + SetBackgroundImmediate(false); } - /// - /// 淡入(同步版本,启动协程) - /// - /// 淡入时长 - public void FadeInSync(float duration = 1) + public void FadeInSync(float duration = 1f) { StartCoroutine(FadeInAsync(duration)); } - /// - /// 淡出(同步版本,启动协程) - /// - /// 淡出时长 - public void FadeOutSync(float duration = 1) + public void FadeOutSync(float duration = 1f) { StartCoroutine(FadeOutAsync(duration)); } + + public void ClearPresentationEffects() + { + NotifyPresentationChanging(); + ClearSmallImmediate(); + ClearLargeImmediate(); + SetBackgroundImmediate(false); + } + + public ShowcaseSnapshotDto CaptureSnapshot() + { + var mode = ShowcaseDisplayMode.None; + var picName = string.Empty; + if (IsSmallShowing && !string.IsNullOrEmpty(_currentSmallPicName)) + { + mode = ShowcaseDisplayMode.Small; + picName = _currentSmallPicName; + } + else if (IsLargeShowing && !string.IsNullOrEmpty(_currentLargePicName)) + { + mode = ShowcaseDisplayMode.Large; + picName = _currentLargePicName; + } + + return new ShowcaseSnapshotDto + { + displayMode = mode.ToString(), + picName = picName, + isBackgroundVisible = _backgroundSpriteRenderer != null + && _backgroundSpriteRenderer.gameObject.activeSelf + && _backgroundSpriteRenderer.color.a > 0.001f + }; + } + + public IEnumerator RestoreSnapshotAsync(ShowcaseSnapshotDto snapshot, Action warning = null) + { + ClearPresentationEffects(); + if (snapshot == null) yield break; + + if (!Enum.TryParse(snapshot.displayMode, true, out ShowcaseDisplayMode mode)) + { + warning?.Invoke($"[SpriteShowcase] Unknown display mode: {snapshot.displayMode}"); + mode = ShowcaseDisplayMode.None; + } + + switch (mode) + { + case ShowcaseDisplayMode.Small when !string.IsNullOrWhiteSpace(snapshot.picName): + yield return ShowObj(snapshot.picName, 0f); + break; + case ShowcaseDisplayMode.Large when !string.IsNullOrWhiteSpace(snapshot.picName): + yield return ShowLargeObj(snapshot.picName, 0f); + break; + } + + SetBackgroundImmediate(snapshot.isBackgroundVisible); + } + + private void NotifyPresentationChanging() + { + PresentationChanging?.Invoke(); + } + + private void ClearSmallImmediate() + { + if (_spriteRenderer != null) + { + _spriteRenderer.DOKill(); + _spriteRenderer.SetAlpha(0f); + _spriteRenderer.gameObject.SetActive(false); + } + _currentSmallPicName = null; + } + + private void ClearLargeImmediate() + { + if (_largeSpriteRenderer != null) + { + _largeSpriteRenderer.DOKill(); + _largeSpriteRenderer.SetAlpha(0f); + _largeSpriteRenderer.gameObject.SetActive(false); + } + _currentLargePicName = null; + } + + private void SetBackgroundImmediate(bool visible) + { + if (_backgroundSpriteRenderer == null) return; + _backgroundSpriteRenderer.DOKill(); + _backgroundSpriteRenderer.SetAlpha(visible ? 1f : 0f); + _backgroundSpriteRenderer.gameObject.SetActive(visible); + } } } diff --git a/Docs/D2Sleep表现系统拆分与存档重构方案.md b/Docs/D2Sleep表现系统拆分与存档重构方案.md new file mode 100644 index 000000000..fb1be1fd7 --- /dev/null +++ b/Docs/D2Sleep表现系统拆分与存档重构方案.md @@ -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`,RestoreOrder:69。 + +`ShowcaseSnapshotDto`: + +- `displayMode`:`None` / `Small` / `Large`。 +- `picName`:逻辑图片名,不保存加载后的 `Sprite`。 +- `isBackgroundVisible`:背景遮罩终态。 + +恢复时通过 `ResourceSystem` 重新加载图片,并以 0 秒时长应用终态。 + +### day2SleepPresentation section + +稳定 id:`day2SleepPresentation`,RestoreOrder:70。 + +`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 和视觉参数。 diff --git a/Docs/风筝命令使用指南.md b/Docs/风筝命令使用指南.md index cb536c066..899435ced 100644 --- a/Docs/风筝命令使用指南.md +++ b/Docs/风筝命令使用指南.md @@ -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 Mask(SpriteMask,以底图为形状,风筝层被裁在天空范围内 | `flutter` | 左右交替的几下小拉扯,力度递减 | 保持不动、原地挣扎 | | `wrench` | 被阵风猛地拽向一侧,停顿后回正 | 强乱流、角色被风筝拖拽 | -轱辘特写(`D2S轱辘`)不属于这套 Rig,仍用旧的 `<>`,动效在 `SpriteShowcase.PlaySmallMotion` / `PlayReelHold` 里维护:先被猛地拽进受力姿态(只进一次),之后绷在那里持续高频颤抖,偶尔再被拽深一点——是“全力拉住”的发力感,不会松回原位。 +轱辘特写(`D2S轱辘`)不属于这套 Rig,仍使用 `<>`;分层、遮罩和常驻拉锯循环由 [`ReelMotionPresenter.cs`](../Assets/Scripts/SceneManagement/Day2Sleep/ReelMotionPresenter.cs) 维护:先被猛地拽进受力姿态(只进一次),之后绷在那里持续拉锯,偶尔再被拽深一点——是“全力拉住”的发力感,不会松回原位。 ## 典型用法模式 @@ -109,7 +109,7 @@ Kite Mask(SpriteMask,以底图为形状,风筝层被裁在天空范围内 ### 4. 离场:`kite_hide` 或让互斥逻辑自动处理 -风筝 Rig 已经接入 `ShowObj` / `ShowObjWithMotion` / `ShowLargeObj` / `HideObj` / `HideAll` 的互斥逻辑,切到大图或普通小图时会自动隐藏,不必每次手动 `kite_hide`。只有明确要求“风筝消失但暂不显示别的图”时才需要显式调用。 +`Day2SleepPresentationController` 会监听 `SpriteShowcase.PresentationChanging`。执行普通 `show_sprite`、`show_large_sprite`、`hide_*` 或通用清理时,会自动停止风筝、轱辘、序列帧与 Tween,因此不必每次手动 `kite_hide`。只有明确要求“风筝消失但暂不显示别的图”时才需要显式调用。 ## 常见坑