diff --git a/Assets/Scripts/Game Loop/GameManager.cs b/Assets/Scripts/Game Loop/GameManager.cs index 705519b0a..d0e894367 100644 --- a/Assets/Scripts/Game Loop/GameManager.cs +++ b/Assets/Scripts/Game Loop/GameManager.cs @@ -543,6 +543,10 @@ namespace AibisDream yield return DialogController.Instance.StopDialogRoutine(); } + // The showcase belongs to the loaded gameplay scene, so clear it + // while that scene is still alive and hidden behind the fade. + ResetPresentationEffects(); + SceneOperationResult unloadResult = null; const int maxUnloadAttempts = 2; for (var attempt = 1; attempt <= maxUnloadAttempts; attempt++) @@ -568,7 +572,6 @@ namespace AibisDream yield break; } - ResetPresentationEffects(); UIManager.Instance.HidePanel(); yield return LocalizationKit.WaitUntilReady(); @@ -703,6 +706,7 @@ namespace AibisDream { ScreenEffectManager.Instance?.ResetSaturation(); UIManager.Instance.GetPanel()?.ClearPresentationEffects(); + SpriteShowcase.Instance?.ClearPresentationEffects(); CameraKit.Instance?.ResetCamera(); } diff --git a/Assets/Scripts/SaveSystem/Providers/FixSnapshotProvider.cs b/Assets/Scripts/SaveSystem/Providers/FixSnapshotProvider.cs index 33da68a59..a7a908cc6 100644 --- a/Assets/Scripts/SaveSystem/Providers/FixSnapshotProvider.cs +++ b/Assets/Scripts/SaveSystem/Providers/FixSnapshotProvider.cs @@ -5,7 +5,7 @@ namespace AibisDream.SaveSystem { /// /// sections["fix"]:Fix 场景宏观模式(FixSceneDirector)。 - /// RestoreOrder=65,在 timeline(60) 之后、screen(70) 之前。 + /// RestoreOrder=65,在 timeline(60) 之后、showcase(69) 之前。 /// public class FixSnapshotProvider : IAsyncSnapshotProvider { diff --git a/Assets/Scripts/SaveSystem/Providers/PlayToolSnapshotProvider.cs b/Assets/Scripts/SaveSystem/Providers/PlayToolSnapshotProvider.cs new file mode 100644 index 000000000..1cef469ac --- /dev/null +++ b/Assets/Scripts/SaveSystem/Providers/PlayToolSnapshotProvider.cs @@ -0,0 +1,36 @@ +using System.Collections; +using AibisDream.UI; + +namespace AibisDream.SaveSystem +{ + /// sections["playTool"]:物品图与全屏图。RestoreOrder=70。 + public sealed class PlayToolSnapshotProvider : IAsyncSnapshotProvider + { + public string SaveId => SnapshotProviderIds.PlayTool; + public int RestoreOrder => 70; + + public object Capture() + { + var panel = UIManager.Instance?.GetPanel(); + return panel != null ? panel.CaptureSnapshot() : null; + } + + public IEnumerator RestoreAsync(object dto, SnapshotRestoreContext context) + { + if (dto is not PlayToolSnapshotDto snapshot) + { + context.Warn("[PlayToolSnapshotProvider] DTO 类型不匹配,跳过还原。"); + yield break; + } + + var panel = UIManager.Instance?.GetPanel(); + if (panel == null) + { + context.Warn("[PlayToolSnapshotProvider] PlayToolPanel 未初始化,跳过还原。"); + yield break; + } + + yield return panel.RestoreSnapshotAsync(snapshot, context.Warn); + } + } +} diff --git a/Assets/Scripts/SaveSystem/Providers/PlayToolSnapshotProvider.cs.meta b/Assets/Scripts/SaveSystem/Providers/PlayToolSnapshotProvider.cs.meta new file mode 100644 index 000000000..f5e8f77b1 --- /dev/null +++ b/Assets/Scripts/SaveSystem/Providers/PlayToolSnapshotProvider.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 6c8b98755ccb44089e8ac66b30c02c68 diff --git a/Assets/Scripts/SaveSystem/Providers/ScreenSnapshotProvider.cs b/Assets/Scripts/SaveSystem/Providers/ScreenSnapshotProvider.cs index a3860323e..efc1ceb3f 100644 --- a/Assets/Scripts/SaveSystem/Providers/ScreenSnapshotProvider.cs +++ b/Assets/Scripts/SaveSystem/Providers/ScreenSnapshotProvider.cs @@ -2,12 +2,12 @@ namespace AibisDream.SaveSystem { /// /// sections["screen"]:屏幕饱和度与演出淡入淡出遮罩。 - /// 委托 ;RestoreOrder=70,通常在表现类最后还原。 + /// 委托 ;RestoreOrder=80,在表现类最后还原。 /// public class ScreenSnapshotProvider : ISyncSnapshotProvider { public string SaveId => SnapshotProviderIds.Screen; - public int RestoreOrder => 70; + public int RestoreOrder => 80; 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..07e362010 --- /dev/null +++ b/Assets/Scripts/SaveSystem/Providers/ShowcaseSnapshotProvider.cs @@ -0,0 +1,36 @@ +using System.Collections; + +namespace AibisDream.SaveSystem +{ + /// sections["showcase"]:梦境普通小图或大图。RestoreOrder=69。 + 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 78f8e62e5..11cec161e 100644 --- a/Assets/Scripts/SaveSystem/README.md +++ b/Assets/Scripts/SaveSystem/README.md @@ -100,7 +100,9 @@ SaveSystem/ ├── TimelineSnapshotProvider.cs order 60 ├── FixSnapshotProvider.cs order 65 ├── FixPanelSnapshotProvider.cs order 66 - └── ScreenSnapshotProvider.cs order 70 + ├── ShowcaseSnapshotProvider.cs order 69 + ├── PlayToolSnapshotProvider.cs order 70 + └── ScreenSnapshotProvider.cs order 80 ``` ## 存盘 / 读盘流程 @@ -133,7 +135,7 @@ Phase 0 Yarn 变量(sync) ↓ Phase 1 场景加载(Barrier:LoadSceneAsync)← 框架直管,不走 Provider ↓ -Phase 2 env / actor / audio / timeline / fix / screen(Provider 按 RestoreOrder) +Phase 2 env / actor / audio / timeline / fix / showcase / playTool / screen(Provider 按 RestoreOrder) └─ timeline:`Stopped` = untouched(从未 Evaluate),读档仅还原 `isActive`,不 Reset/Evaluate ↓ Phase 2′ 可选 Barrier(如 Timeline Addressable 须显式等待) @@ -200,7 +202,7 @@ Fix 场景各 State 的 `Enter()` 通常包含相机过渡、Timeline 播放、F | `bodyModule` | `BodyModuleSnapshotDto` | 67 | 插线模块物理态 | | `eye` | `EyeSnapshotDto` | 68 | Eye 叙事阶段 | -**还原顺序**:punchTape → fix(cue)→ fixPanel → bodyModule → eye → screen。`fixPanel` 须在 `fix` 之后,以覆盖 Cue `EnterImmediate` 中的 `ResetPlug`。 +**还原顺序**:punchTape → fix(cue)→ fixPanel → bodyModule → eye → showcase → playTool → screen。`fixPanel` 须在 `fix` 之后,以覆盖 Cue `EnterImmediate` 中的 `ResetPlug`。 **维修场景门控**(`FixSceneSnapshotHelper`):上述 section 仅在 `FixSystemCenter.Instance != null` 时 Capture/Restore。 diff --git a/Assets/Scripts/SaveSystem/SaveSnapshot.cs b/Assets/Scripts/SaveSystem/SaveSnapshot.cs index d72792805..54234138c 100644 --- a/Assets/Scripts/SaveSystem/SaveSnapshot.cs +++ b/Assets/Scripts/SaveSystem/SaveSnapshot.cs @@ -180,6 +180,36 @@ namespace AibisDream.SaveSystem public bool isFadeScreenCovered; } + /// sections["playTool"]:常驻演出 UI 中的物品图与全屏图终态。 + [Serializable] + public class PlayToolSnapshotDto + { + public bool isObjVisible; + public string objPicName; + public bool isFullScreenVisible; + public string fullScreenPicName; + } + + /// Showcase 当前持久化的图片类型;不包含风筝及其他瞬时演出。 + public enum ShowcaseDisplayMode + { + None, + Small, + Large + } + + /// sections["showcase"]:梦境 Showcase 普通小图或大图终态。 + [Serializable] + public class ShowcaseSnapshotDto + { + /// 枚举名。 + public string displayMode; + public string picName; + + /// 小图是否由分层资源组成;只恢复静止画面,不恢复运动。 + public bool usesSplitLayers; + } + /// sections["subway"]:地铁场景宏观状态(SubwayCenter / OutSideState)。 [Serializable] public class SubwaySnapshotDto diff --git a/Assets/Scripts/SaveSystem/SnapshotRegistry.cs b/Assets/Scripts/SaveSystem/SnapshotRegistry.cs index 1851ee4d3..e2c011842 100644 --- a/Assets/Scripts/SaveSystem/SnapshotRegistry.cs +++ b/Assets/Scripts/SaveSystem/SnapshotRegistry.cs @@ -15,6 +15,8 @@ namespace AibisDream.SaveSystem public const string Audio = "audio"; public const string Timeline = "timeline"; public const string Screen = "screen"; + public const string PlayTool = "playTool"; + public const string Showcase = "showcase"; public const string Subway = "subway"; public const string PunchTape = "punchTape"; public const string Fix = "fix"; @@ -29,7 +31,7 @@ namespace AibisDream.SaveSystem /// public static readonly string[] RequiredForCapture = { - Environment, Actor, Audio, Timeline, Subway, Fix, Screen + Environment, Actor, Audio, Timeline, Subway, Fix, Showcase, PlayTool, Screen }; } @@ -59,6 +61,8 @@ namespace AibisDream.SaveSystem Register(new FixPanelSnapshotProvider()); Register(new BodyModuleSnapshotProvider()); Register(new EyeSnapshotProvider()); + Register(new ShowcaseSnapshotProvider()); + Register(new PlayToolSnapshotProvider()); Register(new ScreenSnapshotProvider()); } diff --git a/Assets/Scripts/SaveSystem/SnapshotRestore.cs b/Assets/Scripts/SaveSystem/SnapshotRestore.cs index b101239eb..02beddfa7 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.PlayTool => jobj.ToObject(), SnapshotProviderIds.Screen => jobj.ToObject(), _ => jobj }; diff --git a/Assets/Scripts/SceneManagement/SpriteShowcase.cs b/Assets/Scripts/SceneManagement/SpriteShowcase.cs index 96e8ad112..f0b9f2a4f 100644 --- a/Assets/Scripts/SceneManagement/SpriteShowcase.cs +++ b/Assets/Scripts/SceneManagement/SpriteShowcase.cs @@ -2,6 +2,7 @@ using System.Collections; using AibisDream.Framework; using AibisDream.Kit; +using AibisDream.SaveSystem; using AibisDream.Utility; using DG.Tweening; using UnityEngine; @@ -55,6 +56,8 @@ namespace AibisDream private Vector3 _smallBaseLocalScale; private Vector3 _smallMotionBaseOffset; private string _currentSmallPicName; + private string _currentLargePicName; + private bool _currentSmallUsesSplitLayers; // 通过 activeSelf 判断当前状态 private bool IsSmallShowing => _spriteRenderer.gameObject.activeSelf; @@ -221,15 +224,27 @@ namespace AibisDream : picName; } - private IEnumerator LoadDreamSpriteInto(SpriteRenderer target, string picName) + private IEnumerator LoadDreamSpriteInto( + SpriteRenderer target, + string picName, + Action completed = null, + Action warning = null) { var key = string.Format(DreamSpritePath, NormalizeDreamPicName(picName)); var handle = ResourceSystem.LoadAsync(key); yield return handle; if (handle.Status == AsyncOperationStatus.Succeeded && handle.Result != null) + { target.sprite = handle.Result; + completed?.Invoke(true); + } else - Debug.LogError($"[SpriteShowcase] Failed to load sprite: {key}"); + { + var message = $"[SpriteShowcase] Failed to load sprite: {key}"; + Debug.LogError(message); + warning?.Invoke(message); + completed?.Invoke(false); + } } private void EnsureSmallMotionRenderer() @@ -503,6 +518,72 @@ namespace AibisDream 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() + { + 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; + } + } + /// /// 显示物体(小图) /// @@ -540,8 +621,16 @@ namespace AibisDream _spriteRenderer.gameObject.SetActive(true); - yield return LoadDreamSpriteInto(_spriteRenderer, picName); + var loaded = false; + yield return LoadDreamSpriteInto(_spriteRenderer, picName, value => loaded = value); + if (!loaded) + { + ClearSmallImmediate(); + yield break; + } + _currentSmallPicName = NormalizeDreamPicName(picName); + _currentSmallUsesSplitLayers = false; yield return _spriteRenderer.DOFade(1, duration).WaitForCompletion(); } @@ -562,7 +651,8 @@ namespace AibisDream var normalizedName = NormalizeDreamPicName(picName); var useSplitMotion = TryGetSplitMotionPicNames(picName, out var basePicName, out var motionPicName); - var isSamePic = string.Equals(_currentSmallPicName, normalizedName, StringComparison.OrdinalIgnoreCase); + var isSamePic = string.Equals(_currentSmallPicName, normalizedName, StringComparison.OrdinalIgnoreCase) + && _currentSmallUsesSplitLayers == useSplitMotion; _smallMotionBaseOffset = normalizedName == "D2S轱辘" ? new Vector3(0f, -0.32f, 0f) : Vector3.zero; KillSmallTweenState(); @@ -572,7 +662,17 @@ namespace AibisDream if (!isSamePic || _spriteRenderer.sprite == null) { - yield return LoadDreamSpriteInto(_spriteRenderer, useSplitMotion ? basePicName : picName); + var baseLoaded = false; + yield return LoadDreamSpriteInto( + _spriteRenderer, + useSplitMotion ? basePicName : picName, + value => baseLoaded = value); + if (!baseLoaded) + { + ClearSmallImmediate(); + yield break; + } + _currentSmallPicName = normalizedName; } _spriteRenderer.SetAlpha(1f); @@ -586,7 +686,16 @@ namespace AibisDream ResetSmallTransform(resetMotionLayer: !isSamePic); if (!isSamePic || _smallMotionRenderer.sprite == null) { - yield return LoadDreamSpriteInto(_smallMotionRenderer, motionPicName); + var motionLoaded = false; + yield return LoadDreamSpriteInto( + _smallMotionRenderer, + motionPicName, + value => motionLoaded = value); + if (!motionLoaded) + { + ClearSmallImmediate(); + yield break; + } } _smallMotionRenderer.SetAlpha(1f); } @@ -602,6 +711,8 @@ namespace AibisDream } } + _currentSmallPicName = normalizedName; + _currentSmallUsesSplitLayers = useSplitMotion; PlaySmallMotion(motionProfile); } @@ -628,8 +739,15 @@ namespace AibisDream _largeSpriteRenderer.gameObject.SetActive(true); - yield return LoadDreamSpriteInto(_largeSpriteRenderer, picName); + var loaded = false; + yield return LoadDreamSpriteInto(_largeSpriteRenderer, picName, value => loaded = value); + if (!loaded) + { + ClearLargeImmediate(); + yield break; + } + _currentLargePicName = NormalizeDreamPicName(picName); yield return _largeSpriteRenderer.DOFade(1, duration).WaitForCompletion(); } @@ -680,6 +798,7 @@ namespace AibisDream _spriteRenderer.gameObject.SetActive(false); _currentSmallPicName = null; + _currentSmallUsesSplitLayers = false; if (_smallMotionRenderer != null) { @@ -706,6 +825,7 @@ namespace AibisDream yield return _largeSpriteRenderer.DOFade(0, duration).WaitForCompletion(); _largeSpriteRenderer.gameObject.SetActive(false); + _currentLargePicName = null; } /// @@ -722,6 +842,186 @@ namespace AibisDream yield return HideLargeObj(duration); } + public ShowcaseSnapshotDto CaptureSnapshot() + { + if (IsKiteShowing) + { + return HiddenShowcaseSnapshot(); + } + + if (IsSmallShowing && !string.IsNullOrEmpty(_currentSmallPicName)) + { + return new ShowcaseSnapshotDto + { + displayMode = nameof(ShowcaseDisplayMode.Small), + picName = _currentSmallPicName, + usesSplitLayers = _currentSmallUsesSplitLayers + }; + } + + if (IsLargeShowing && !string.IsNullOrEmpty(_currentLargePicName)) + { + return new ShowcaseSnapshotDto + { + displayMode = nameof(ShowcaseDisplayMode.Large), + picName = _currentLargePicName + }; + } + + return HiddenShowcaseSnapshot(); + } + + private static ShowcaseSnapshotDto HiddenShowcaseSnapshot() + { + return new ShowcaseSnapshotDto + { + displayMode = nameof(ShowcaseDisplayMode.None) + }; + } + + 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}"); + yield break; + } + + switch (mode) + { + case ShowcaseDisplayMode.None: + yield break; + + case ShowcaseDisplayMode.Small: + if (string.IsNullOrWhiteSpace(snapshot.picName)) + { + warning?.Invoke("[SpriteShowcase] Small snapshot is missing picName."); + yield break; + } + + yield return RestoreSmallImmediate(snapshot.picName, snapshot.usesSplitLayers, warning); + yield break; + + case ShowcaseDisplayMode.Large: + if (string.IsNullOrWhiteSpace(snapshot.picName)) + { + warning?.Invoke("[SpriteShowcase] Large snapshot is missing picName."); + yield break; + } + + yield return RestoreLargeImmediate(snapshot.picName, warning); + yield break; + } + } + + private IEnumerator RestoreSmallImmediate(string picName, bool usesSplitLayers, Action warning) + { + if (_spriteRenderer == null) yield break; + + var normalizedName = NormalizeDreamPicName(picName); + _smallMotionBaseOffset = normalizedName == "D2S轱辘" ? new Vector3(0f, -0.32f, 0f) : Vector3.zero; + _spriteRenderer.gameObject.SetActive(true); + + if (usesSplitLayers) + { + if (!TryGetSplitMotionPicNames(picName, out var basePicName, out var motionPicName)) + { + warning?.Invoke($"[SpriteShowcase] Split-layer snapshot is unsupported: {picName}"); + ClearSmallImmediate(); + yield break; + } + + var baseLoaded = false; + yield return LoadDreamSpriteInto( + _spriteRenderer, + basePicName, + value => baseLoaded = value, + warning); + if (!baseLoaded) + { + ClearSmallImmediate(); + yield break; + } + + EnsureSmallMotionRenderer(); + _smallMotionRenderer.gameObject.SetActive(true); + CopySmallRendererSettings(_smallMotionRenderer, 1, SpriteMaskInteraction.VisibleInsideMask); + ConfigureSmallMotionMask(); + ResetSmallTransform(); + + var motionLoaded = false; + yield return LoadDreamSpriteInto( + _smallMotionRenderer, + motionPicName, + value => motionLoaded = value, + warning); + if (!motionLoaded) + { + ClearSmallImmediate(); + yield break; + } + + _smallMotionRenderer.SetAlpha(1f); + } + else + { + var loaded = false; + yield return LoadDreamSpriteInto( + _spriteRenderer, + picName, + value => loaded = value, + warning); + if (!loaded) + { + ClearSmallImmediate(); + yield break; + } + } + + _spriteRenderer.SetAlpha(1f); + _currentSmallPicName = normalizedName; + _currentSmallUsesSplitLayers = usesSplitLayers; + } + + private IEnumerator RestoreLargeImmediate(string picName, Action warning) + { + if (_largeSpriteRenderer == null) yield break; + + _largeSpriteRenderer.gameObject.SetActive(true); + var loaded = false; + yield return LoadDreamSpriteInto( + _largeSpriteRenderer, + picName, + value => loaded = value, + warning); + if (!loaded) + { + ClearLargeImmediate(); + yield break; + } + + _largeSpriteRenderer.SetAlpha(1f); + _currentLargePicName = NormalizeDreamPicName(picName); + } + + public void ClearPresentationEffects() + { + ClearSmallImmediate(); + ClearLargeImmediate(); + ClearKiteImmediate(); + ResetDreamOverflow(); + + if (_backgroundSpriteRenderer != null) + { + _backgroundSpriteRenderer.DOKill(); + _backgroundSpriteRenderer.SetAlpha(0f); + _backgroundSpriteRenderer.gameObject.SetActive(false); + } + } + #region 风筝 Rig // Kite Camera(虚拟镜头完全补偿)→ Rig(高度+漂移)→ Sway(鱼式流体摆动)→ Accent(DOTween 顿挫) diff --git a/Assets/Scripts/UI/Panel/PlayToolPanel.cs b/Assets/Scripts/UI/Panel/PlayToolPanel.cs index 4aeabbe03..52acbe1a4 100644 --- a/Assets/Scripts/UI/Panel/PlayToolPanel.cs +++ b/Assets/Scripts/UI/Panel/PlayToolPanel.cs @@ -2,6 +2,8 @@ using System.Collections; using AibisDream.Framework; using AibisDream.Kit; +using AibisDream.SaveSystem; +using AibisDream.Utility; using DG.Tweening; using RainbowArt.CleanFlatUI; using UnityEngine; @@ -35,6 +37,12 @@ namespace AibisDream.UI private SignBoard _signBoard; private TimeTurner _timeTurner; private int _timeFlowsVersion; + private int _objOperationVersion; + private int _fullScreenOperationVersion; + private string _currentObjPicName; + private string _currentFullScreenPicName; + private bool _isObjVisible; + private bool _isFullScreenVisible; #endregion @@ -63,70 +71,197 @@ namespace AibisDream.UI : picName; } + private IEnumerator LoadObjSprite( + string picName, + Action completed, + Action warning = null) + { + var normalizedName = NormalizeObjPicName(picName); + var key = string.Format(ObjSpritePath, normalizedName); + var handle = ResourceSystem.LoadAsync(key); + yield return handle; + if (handle.Status == AsyncOperationStatus.Succeeded && handle.Result != null) + { + completed?.Invoke(handle.Result); + yield break; + } + + var message = $"[PlayToolPanel] Failed to load sprite: {key}"; + Debug.LogError(message); + warning?.Invoke(message); + completed?.Invoke(null); + } + + private void ApplyObjSprite(Sprite sprite) + { + var imageAspect = (float)sprite.texture.width / sprite.texture.height; + if (imageAspect > 1f) + { + _showObjImage.rectTransform.sizeDelta = new Vector2(objWidth, objWidth / imageAspect); + } + else + { + _showObjImage.rectTransform.sizeDelta = new Vector2(objHeight * imageAspect, objHeight); + } + + _showObjImage.sprite = sprite; + } + + private void ClearObjImmediate() + { + _objOperationVersion++; + _showObjImage.DOKill(); + _showObjImage.SetAlpha(0f); + _showObjPanel.SetActive(false); + _currentObjPicName = null; + _isObjVisible = false; + } + + private void ClearFullScreenImmediate() + { + _fullScreenOperationVersion++; + _fullScreenImage.DOKill(); + _fullScreenImage.SetAlpha(0f); + _fullScreenImage.gameObject.SetActive(false); + _currentFullScreenPicName = null; + _isFullScreenVisible = false; + } + /// /// 显示物体 /// /// 协程 public IEnumerator ShowObj(string picName, float duration = 1) { + var version = ++_objOperationVersion; + _showObjImage.DOKill(); _showObjPanel.gameObject.SetActive(true); - var key = string.Format(ObjSpritePath, NormalizeObjPicName(picName)); - var handle = ResourceSystem.LoadAsync(key); - yield return handle; - if (handle.Status != AsyncOperationStatus.Succeeded || handle.Result == null) + Sprite newSprite = null; + yield return LoadObjSprite(picName, value => newSprite = value); + if (version != _objOperationVersion) { - Debug.LogError($"[PlayToolPanel] Failed to load sprite: {key}"); yield break; } - var newSprite = handle.Result; - // 获取图片的长宽比 - var imageAspect = (float)newSprite.texture.width / newSprite.texture.height; - - // 计算新的尺寸 - if (imageAspect > 1) // 宽图 + if (newSprite == null) { - var newHeight = objWidth / imageAspect; - _showObjImage.rectTransform.sizeDelta = new Vector2(objWidth, newHeight); - } - else // 高图 - { - var newWidth = objHeight * imageAspect; - _showObjImage.rectTransform.sizeDelta = new Vector2(newWidth, objHeight); + ClearObjImmediate(); + yield break; } - // 淡入 - _showObjImage.sprite = newSprite; + ApplyObjSprite(newSprite); yield return _showObjImage.FadeInAsync(duration); + if (version != _objOperationVersion) yield break; + + _currentObjPicName = NormalizeObjPicName(picName); + _isObjVisible = true; } public IEnumerator HideObj(float duration = 1) { + var version = ++_objOperationVersion; + _currentObjPicName = null; + _isObjVisible = false; + _showObjImage.DOKill(); yield return _showObjImage.FadeOutAsync(duration); + if (version != _objOperationVersion) yield break; _showObjPanel.SetActive(false); } public IEnumerator ShowFullScreen(string picName, float duration = 1) { - var key = string.Format(ObjSpritePath, NormalizeObjPicName(picName)); - var handle = ResourceSystem.LoadAsync(key); - yield return handle; - if (handle.Status != AsyncOperationStatus.Succeeded || handle.Result == null) + var version = ++_fullScreenOperationVersion; + _fullScreenImage.DOKill(); + _fullScreenImage.gameObject.SetActive(true); + + Sprite newSprite = null; + yield return LoadObjSprite(picName, value => newSprite = value); + if (version != _fullScreenOperationVersion) { - Debug.LogError($"[PlayToolPanel] Failed to load sprite: {key}"); yield break; } - _fullScreenImage.sprite = handle.Result; + if (newSprite == null) + { + ClearFullScreenImmediate(); + yield break; + } + + _fullScreenImage.sprite = newSprite; - // 淡入 yield return _fullScreenImage.FadeInAsync(duration); + if (version != _fullScreenOperationVersion) yield break; + + _currentFullScreenPicName = NormalizeObjPicName(picName); + _isFullScreenVisible = true; } public IEnumerator HideFullScreen(float duration = 1) { + var version = ++_fullScreenOperationVersion; + _currentFullScreenPicName = null; + _isFullScreenVisible = false; + _fullScreenImage.DOKill(); yield return _fullScreenImage.FadeOutAsync(duration); + if (version != _fullScreenOperationVersion) yield break; + _fullScreenImage.gameObject.SetActive(false); + } + + public PlayToolSnapshotDto CaptureSnapshot() + { + return new PlayToolSnapshotDto + { + isObjVisible = _isObjVisible, + objPicName = _currentObjPicName, + isFullScreenVisible = _isFullScreenVisible, + fullScreenPicName = _currentFullScreenPicName + }; + } + + public IEnumerator RestoreSnapshotAsync(PlayToolSnapshotDto snapshot, Action warning = null) + { + ClearObjImmediate(); + ClearFullScreenImmediate(); + if (snapshot == null) yield break; + + if (snapshot.isObjVisible && !string.IsNullOrWhiteSpace(snapshot.objPicName)) + { + var version = ++_objOperationVersion; + Sprite sprite = null; + yield return LoadObjSprite(snapshot.objPicName, value => sprite = value, warning); + if (version == _objOperationVersion && sprite != null) + { + ApplyObjSprite(sprite); + _showObjPanel.SetActive(true); + _showObjImage.SetAlpha(1f); + _currentObjPicName = NormalizeObjPicName(snapshot.objPicName); + _isObjVisible = true; + } + } + else if (snapshot.isObjVisible) + { + warning?.Invoke("[PlayToolPanel] Obj snapshot is missing picName."); + } + + if (snapshot.isFullScreenVisible && !string.IsNullOrWhiteSpace(snapshot.fullScreenPicName)) + { + var version = ++_fullScreenOperationVersion; + Sprite sprite = null; + yield return LoadObjSprite(snapshot.fullScreenPicName, value => sprite = value, warning); + if (version == _fullScreenOperationVersion && sprite != null) + { + _fullScreenImage.sprite = sprite; + _fullScreenImage.gameObject.SetActive(true); + _fullScreenImage.SetAlpha(1f); + _currentFullScreenPicName = NormalizeObjPicName(snapshot.fullScreenPicName); + _isFullScreenVisible = true; + } + } + else if (snapshot.isFullScreenVisible) + { + warning?.Invoke("[PlayToolPanel] Full-screen snapshot is missing picName."); + } } public IEnumerator OpenProgressWindow(string title, string description, float duration) @@ -197,13 +332,8 @@ namespace AibisDream.UI // SceneLoader unloads scenes with DOTween.KillAll(). Do not rely on a // tween completion callback to deactivate these raycast targets: if // that tween is killed, an invisible full-screen image blocks the UI. - _fullScreenImage.DOKill(); - _fullScreenImage.color = Color.clear; - _fullScreenImage.gameObject.SetActive(false); - - _showObjImage.DOKill(); - _showObjImage.color = Color.clear; - _showObjPanel.SetActive(false); + ClearFullScreenImmediate(); + ClearObjImmediate(); _timeTurner.ForceStop(); }