using System; using System.Collections; using AibisDream.Framework; using AibisDream.Kit; 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 = "_风筝层"; [SerializeField] private SpriteRenderer _spriteRenderer; [SerializeField] private SpriteRenderer _backgroundSpriteRenderer; [SerializeField] private SpriteRenderer _largeSpriteRenderer; private struct DreamOverflowSettings { public float exposure; public float glow; public float chromatic; public float noise; public DreamOverflowSettings(float exposure, float glow, float chromatic, float noise) { this.exposure = exposure; this.glow = glow; this.chromatic = chromatic; this.noise = noise; } } private static readonly int OverflowIntensityId = Shader.PropertyToID("_OverflowIntensity"); private static readonly int OverflowExposureId = Shader.PropertyToID("_OverflowExposure"); private static readonly int OverflowGlowId = Shader.PropertyToID("_OverflowGlow"); private static readonly int OverflowChromaticId = Shader.PropertyToID("_OverflowChromatic"); private static readonly int OverflowNoiseId = Shader.PropertyToID("_OverflowNoise"); private MaterialPropertyBlock _dreamOverflowBlock; private DreamOverflowSettings _dreamOverflowSettings; private float _dreamOverflowIntensity; private int _dreamOverflowTransitionVersion; 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; // 通过 activeSelf 判断当前状态 private bool IsSmallShowing => _spriteRenderer.gameObject.activeSelf; private bool IsLargeShowing => _largeSpriteRenderer != null && _largeSpriteRenderer.gameObject.activeSelf; public override void OnSingletonInit() { _dreamOverflowSettings = EntryOverflowSettings(); ResetDreamOverflow(); CaptureSmallBaseTransform(); // 初始状态:隐藏 _spriteRenderer.gameObject.SetActive(false); _spriteRenderer.SetAlpha(0); // 大图初始状态:隐藏 if (_largeSpriteRenderer != null) { _largeSpriteRenderer.gameObject.SetActive(false); _largeSpriteRenderer.SetAlpha(0); } } private void OnDisable() { ResetDreamOverflow(); } public override void OnSingletonDestroy() { ResetDreamOverflow(); } private static DreamOverflowSettings EntryOverflowSettings() { return new DreamOverflowSettings( exposure: 1.45f, glow: 0.65f, chromatic: 0.006f, noise: 0.08f); } private static DreamOverflowSettings RuptureOverflowSettings() { return new DreamOverflowSettings( exposure: 2.4f, glow: 1.15f, chromatic: 0.018f, noise: 0.2f); } private static DreamOverflowSettings ResolveDreamOverflowSettings(string preset) { var normalized = string.IsNullOrWhiteSpace(preset) ? "entry" : preset.Trim().ToLowerInvariant(); switch (normalized) { case "entry": return EntryOverflowSettings(); case "rupture": return RuptureOverflowSettings(); default: Debug.LogWarning($"[SpriteShowcase] Unknown dream overflow preset '{preset}', using 'entry'."); return EntryOverflowSettings(); } } /// /// Drives the local overexposure/glow effect used by dream sprites. /// The effect is applied through MaterialPropertyBlock so no material instances are created. /// public IEnumerator SetDreamOverflow(string preset, float target, float duration) { if (!string.IsNullOrWhiteSpace(preset)) { _dreamOverflowSettings = ResolveDreamOverflowSettings(preset); } target = Mathf.Clamp01(target); duration = Mathf.Max(0f, duration); var version = ++_dreamOverflowTransitionVersion; var start = _dreamOverflowIntensity; if (duration <= 0f || !gameObject.activeInHierarchy) { _dreamOverflowIntensity = target; ApplyDreamOverflow(); yield break; } var elapsed = 0f; while (elapsed < duration) { if (version != _dreamOverflowTransitionVersion) { yield break; } elapsed += Time.deltaTime; var progress = Mathf.SmoothStep(0f, 1f, Mathf.Clamp01(elapsed / duration)); _dreamOverflowIntensity = Mathf.Lerp(start, target, progress); ApplyDreamOverflow(); yield return null; } if (version == _dreamOverflowTransitionVersion) { _dreamOverflowIntensity = target; ApplyDreamOverflow(); } } private void ResetDreamOverflow() { _dreamOverflowTransitionVersion++; _dreamOverflowIntensity = 0f; ApplyDreamOverflow(); } private void ApplyDreamOverflow() { ApplyDreamOverflow(_spriteRenderer); ApplyDreamOverflow(_largeSpriteRenderer); ApplyDreamOverflow(_smallMotionRenderer); } private void ApplyDreamOverflow(SpriteRenderer target) { if (target == null) return; if (_dreamOverflowBlock == null) { _dreamOverflowBlock = new MaterialPropertyBlock(); } target.GetPropertyBlock(_dreamOverflowBlock); _dreamOverflowBlock.SetFloat(OverflowIntensityId, _dreamOverflowIntensity); _dreamOverflowBlock.SetFloat(OverflowExposureId, _dreamOverflowSettings.exposure); _dreamOverflowBlock.SetFloat(OverflowGlowId, _dreamOverflowSettings.glow); _dreamOverflowBlock.SetFloat(OverflowChromaticId, _dreamOverflowSettings.chromatic); _dreamOverflowBlock.SetFloat(OverflowNoiseId, _dreamOverflowSettings.noise); target.SetPropertyBlock(_dreamOverflowBlock); _dreamOverflowBlock.Clear(); } private void CaptureSmallBaseTransform() { if (_spriteRenderer == null) return; var tr = _spriteRenderer.transform; _smallBaseLocalPosition = tr.localPosition; _smallBaseLocalRotation = tr.localRotation; _smallBaseLocalScale = tr.localScale; } private static string NormalizeDreamPicName(string picName) { if (string.IsNullOrEmpty(picName)) return picName; return picName.EndsWith(".png", StringComparison.OrdinalIgnoreCase) ? picName.Substring(0, picName.Length - 4) : 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 { var message = $"[SpriteShowcase] Failed to load sprite: {key}"; Debug.LogError(message); warning?.Invoke(message); completed?.Invoke(false); } } private void EnsureSmallMotionRenderer() { if (_smallMotionRenderer != null || _spriteRenderer == null) return; 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); ApplyDreamOverflow(_smallMotionRenderer); } 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 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 (normalized == "D2S轱辘") { 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, -0.32f), 0.22f).SetEase(Ease.OutCubic)); seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, -2.8f), 0.22f).SetEase(Ease.OutCubic)); seq.OnComplete(() => PlayReelHold(tr)); break; default: seq.Kill(); return; } _smallMotionSequence = seq; } /// /// 轱辘“拔河角力”常驻循环:一场正在输掉的拔河。 /// strain 随回合递增(0→1):节奏越来越快、姿态被拽得越来越深、猛夺越来越频繁; /// 往回挣不再是平滑回弹,而是“向下被拖住→抬回一点→又沉下去→再咬住”的三段拉锯。 /// 每回合参数随机,OnComplete 续下一回合;用纵向位移和停顿表达强拉,避免机械抖动。 /// private void PlayReelHold(Transform tr, float strain = 0f) { _smallMotionSequence?.Kill(); // 姿态整体随 strain 加深;时长随 strain 收紧。旋转只做小角度,避免贴图边缘穿帮。 var deepX = strain * 0.06f; var deepY = strain * 0.14f; var speed = Mathf.Lerp(1f, 0.62f, strain); var rotBias = strain * 0.8f; var seq = DOTween.Sequence(); // 对面加力:被一点点拽深 var strainDur = UnityEngine.Random.Range(0.35f, 0.6f) * speed; seq.Append(tr.DOLocalMove( BaseOffset(UnityEngine.Random.Range(0.17f, 0.23f) + deepX, UnityEngine.Random.Range(-0.42f, -0.34f) - deepY), strainDur) .SetEase(Ease.InOutSine)); seq.Join(tr.DOLocalRotate( new Vector3(0f, 0f, UnityEngine.Random.Range(-3.6f, -2.4f) - rotBias), strainDur) .SetEase(Ease.InOutSine)); // 猛夺一小截:概率与深度都随 strain 提升;夺完手上发麻—— // 不是连续 shake,而是被向下拽住、短暂停顿,再勉强抬回一点。 if (UnityEngine.Random.value < 0.55f + strain * 0.35f) { var yankDur = UnityEngine.Random.Range(0.1f, 0.16f) * speed; seq.Append(tr.DOLocalMove( BaseOffset(UnityEngine.Random.Range(0.25f, 0.34f) + deepX, UnityEngine.Random.Range(-0.62f, -0.5f) - deepY), yankDur) .SetEase(Ease.OutCubic)); seq.Join(tr.DOLocalRotate( new Vector3(0f, 0f, UnityEngine.Random.Range(-4.8f, -3.4f) - rotBias), yankDur) .SetEase(Ease.OutCubic)); seq.AppendInterval(UnityEngine.Random.Range(0.05f, 0.11f) * speed); var biteDur = UnityEngine.Random.Range(0.12f, 0.2f) * speed; seq.Append(tr.DOLocalMove( BaseOffset(UnityEngine.Random.Range(0.19f, 0.27f) + deepX, UnityEngine.Random.Range(-0.48f, -0.4f) - deepY), biteDur) .SetEase(Ease.OutCubic)); seq.Join(tr.DOLocalRotate( new Vector3(0f, 0f, UnityEngine.Random.Range(-3.4f, -2.2f) - rotBias), biteDur) .SetEase(Ease.OutCubic)); } // 往回挣(一):咬牙拉回一点 var recoverDur = UnityEngine.Random.Range(0.16f, 0.24f) * speed; seq.Append(tr.DOLocalMove( BaseOffset(UnityEngine.Random.Range(0.12f, 0.16f) + deepX, UnityEngine.Random.Range(-0.32f, -0.26f) - deepY * 0.55f), recoverDur) .SetEase(Ease.OutCubic)); seq.Join(tr.DOLocalRotate( new Vector3(0f, 0f, UnityEngine.Random.Range(-2.3f, -1.3f) - rotBias * 0.7f), recoverDur) .SetEase(Ease.OutCubic)); // 滑走半截:手上又被抽走一段 var slipDur = UnityEngine.Random.Range(0.09f, 0.15f) * speed; seq.Append(tr.DOLocalMove( BaseOffset(UnityEngine.Random.Range(0.2f, 0.27f) + deepX, UnityEngine.Random.Range(-0.5f, -0.4f) - deepY), slipDur) .SetEase(Ease.OutQuad)); seq.Join(tr.DOLocalRotate( new Vector3(0f, 0f, UnityEngine.Random.Range(-4.1f, -2.8f) - rotBias), slipDur) .SetEase(Ease.OutQuad)); // 四成回合再快速地向下沉半口气又咬住,上下拉锯的小碎步。 if (UnityEngine.Random.value < 0.45f) { var dipDur = UnityEngine.Random.Range(0.08f, 0.12f) * speed; seq.Append(tr.DOLocalMove( BaseOffset(UnityEngine.Random.Range(0.21f, 0.28f) + deepX, UnityEngine.Random.Range(-0.56f, -0.47f) - deepY), dipDur) .SetEase(Ease.OutQuad)); seq.Join(tr.DOLocalRotate( new Vector3(0f, 0f, UnityEngine.Random.Range(-4.4f, -3f) - rotBias), dipDur) .SetEase(Ease.OutQuad)); var braceDur = UnityEngine.Random.Range(0.11f, 0.16f) * speed; seq.Append(tr.DOLocalMove( BaseOffset(UnityEngine.Random.Range(0.16f, 0.22f) + deepX, UnityEngine.Random.Range(-0.4f, -0.33f) - deepY), braceDur) .SetEase(Ease.OutCubic)); seq.Join(tr.DOLocalRotate( new Vector3(0f, 0f, UnityEngine.Random.Range(-3f, -1.8f) - rotBias), braceDur) .SetEase(Ease.OutCubic)); } // 往回挣(二):再咬住,但回不到最初的姿态 var pullDur = UnityEngine.Random.Range(0.25f, 0.38f) * speed; seq.Append(tr.DOLocalMove( BaseOffset(UnityEngine.Random.Range(0.13f, 0.18f) + deepX, UnityEngine.Random.Range(-0.35f, -0.28f) - deepY * 0.75f), pullDur) .SetEase(Ease.InOutSine)); seq.Join(tr.DOLocalRotate( new Vector3(0f, 0f, UnityEngine.Random.Range(-2.8f, -1.6f) - rotBias), pullDur) .SetEase(Ease.InOutSine)); // 偶尔结尾再沉一下,像手腕被拖住,不做高频震动。 if (UnityEngine.Random.value < 0.35f) { var sagDur = UnityEngine.Random.Range(0.12f, 0.2f) * speed; seq.Append(tr.DOLocalMove( BaseOffset(UnityEngine.Random.Range(0.18f, 0.24f) + deepX, UnityEngine.Random.Range(-0.46f, -0.38f) - deepY), sagDur) .SetEase(Ease.InOutSine)); seq.Join(tr.DOLocalRotate( new Vector3(0f, 0f, UnityEngine.Random.Range(-3.5f, -2.2f) - rotBias), sagDur) .SetEase(Ease.InOutSine)); } seq.OnComplete(() => PlayReelHold(tr, Mathf.Min(1f, strain + 0.22f))); _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() { 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; } _spriteRenderer.gameObject.SetActive(true); 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(); } public IEnumerator ShowObjWithMotion(string picName, string motionProfile, float duration) { // 互斥:如果大图正在显示,先隐藏 if (IsLargeShowing) { yield return HideLargeObj(duration); } // 互斥:风筝 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 = normalizedName == "D2S轱辘" ? 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) { // 互斥:小图立刻关掉,避免风筝层与底图不同步淡出造成的怪 fade if (IsSmallShowing) { yield return HideObj(0f); } if (IsKiteShowing) { yield return HideKite(0f); } if (_largeSpriteRenderer == null) yield break; _largeSpriteRenderer.gameObject.SetActive(true); 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(); } /// /// 隐藏物体(小图) /// /// 淡出时长 /// 协程 public IEnumerator HideObj(float duration = 1) { 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 时检查状态,未显示则直接返回 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; } } /// /// 隐藏大图 /// /// 是否强制隐藏 /// 淡出时长 /// 协程 public IEnumerator HideLargeObj(float duration = 1) { if (_largeSpriteRenderer == null) yield break; yield return _largeSpriteRenderer.DOFade(0, duration).WaitForCompletion(); _largeSpriteRenderer.gameObject.SetActive(false); _currentLargePicName = null; } /// /// 隐藏所有(小图和大图) /// /// 淡出时长 /// 协程 public IEnumerator HideAll(float duration = 1) { if (IsSmallShowing || IsKiteShowing) yield return HideObj(duration); if (IsLargeShowing) 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 顿挫) // 底图 / 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) { if (_backgroundSpriteRenderer == null) yield break; _backgroundSpriteRenderer.gameObject.SetActive(true); yield return _backgroundSpriteRenderer.DOFade(1, duration).WaitForCompletion(); } /// /// 淡出(异步协程版本) /// /// 淡出时长 /// 协程 public IEnumerator FadeOutAsync(float duration = 1) { if (_backgroundSpriteRenderer == null) yield break; _backgroundSpriteRenderer.gameObject.SetActive(true); yield return _backgroundSpriteRenderer.DOFade(0, duration).WaitForCompletion(); _backgroundSpriteRenderer.gameObject.SetActive(false); } /// /// 淡入(同步版本,启动协程) /// /// 淡入时长 public void FadeInSync(float duration = 1) { StartCoroutine(FadeInAsync(duration)); } /// /// 淡出(同步版本,启动协程) /// /// 淡出时长 public void FadeOutSync(float duration = 1) { StartCoroutine(FadeOutAsync(duration)); } } }