Files
aibis-dream/Assets/Scripts/SceneManagement/SpriteShowcase.cs
T
dingyuntian a5e417eeae feat(save): 新增梦境演出图与物品图的存档快照
新增 showcase(order 69) 与 playTool(order 70) 两个快照 section,
分别持久化 SpriteShowcase 小图/大图与 PlayToolPanel 物品图/全屏图
的终态,读档时直接还原静止画面。SpriteShowcase 与 PlayToolPanel
补充快照捕获/还原与立即清理逻辑,GameManager 在卸载场景前清理
演出效果。screen 还原顺序顺延至 80。
2026-07-18 23:59:52 +08:00

1393 lines
53 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<SpriteShowcase>
{
/// <summary>Addressable Key 模板,与 Scene_Dream 中梦境图 Address 一致。</summary>
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();
}
}
/// <summary>
/// Drives the local overexposure/glow effect used by dream sprites.
/// The effect is applied through MaterialPropertyBlock so no material instances are created.
/// </summary>
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<bool> completed = null,
Action<string> warning = null)
{
var key = string.Format(DreamSpritePath, NormalizeDreamPicName(picName));
var handle = ResourceSystem.LoadAsync<Sprite>(key);
yield return handle;
if (handle.Status == AsyncOperationStatus.Succeeded && handle.Result != null)
{
target.sprite = handle.Result;
completed?.Invoke(true);
}
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<SpriteRenderer>();
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<SpriteMask>();
_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;
}
/// <summary>
/// 轱辘“拔河角力”常驻循环:一场正在输掉的拔河。
/// strain 随回合递增(0→1):节奏越来越快、姿态被拽得越来越深、猛夺越来越频繁;
/// 往回挣不再是平滑回弹,而是“向下被拖住→抬回一点→又沉下去→再咬住”的三段拉锯。
/// 每回合参数随机,OnComplete 续下一回合;用纵向位移和停顿表达强拉,避免机械抖动。
/// </summary>
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;
}
}
/// <summary>
/// 显示物体(小图)
/// </summary>
/// <param name="picName">图片名(无扩展名,与 Yarn 中一致;Addressable Key 由 <see cref="DreamSpritePath"/> 拼接)</param>
/// <param name="duration">淡入时长</param>
/// <returns>协程</returns>
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);
}
/// <summary>
/// 显示大图
/// </summary>
/// <param name="picName">图片名(无扩展名,与 Yarn 中一致;Addressable Key 由 <see cref="DreamSpritePath"/> 拼接)</param>
/// <param name="duration">淡入时长</param>
/// <returns>协程</returns>
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();
}
/// <summary>
/// 隐藏物体(小图)
/// </summary>
/// <param name="duration">淡出时长</param>
/// <returns>协程</returns>
public IEnumerator HideObj(float duration = 1)
{
yield return HideObj(force: false, duration);
}
/// <summary>
/// 隐藏物体(小图)
/// </summary>
/// <param name="force">是否强制隐藏</param>
/// <param name="duration">淡出时长</param>
/// <returns>协程</returns>
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;
}
}
/// <summary>
/// 隐藏大图
/// </summary>
/// <param name="force">是否强制隐藏</param>
/// <param name="duration">淡出时长</param>
/// <returns>协程</returns>
public IEnumerator HideLargeObj(float duration = 1)
{
if (_largeSpriteRenderer == null) yield break;
yield return _largeSpriteRenderer.DOFade(0, duration).WaitForCompletion();
_largeSpriteRenderer.gameObject.SetActive(false);
_currentLargePicName = null;
}
/// <summary>
/// 隐藏所有(小图和大图)
/// </summary>
/// <param name="duration">淡出时长</param>
/// <returns>协程</returns>
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<string> 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<string> 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<string> 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(鱼式流体摆动)→ AccentDOTween 顿挫)
// 底图 / 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<SpriteRenderer>();
CopySmallRendererSettings(_kiteRenderer, 1, SpriteMaskInteraction.VisibleInsideMask);
_kiteBaseRenderer = baseObject.AddComponent<SpriteRenderer>();
CopySmallRendererSettings(_kiteBaseRenderer, 0, SpriteMaskInteraction.None);
_kiteMask = maskObject.AddComponent<SpriteMask>();
_kiteMask.alphaCutoff = 0.01f;
_kiteMask.isCustomRangeActive = true;
_kiteMask.enabled = false;
EnsureKiteFocusMaterial();
_kiteDriver = cameraObject.AddComponent<KiteRigDriver>();
_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();
}
/// <summary>
/// 显示风筝 Rig。已显示时等价于 <see cref="SetKiteState"/>(幂等)。
/// </summary>
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));
}
/// <summary>
/// 高度 SmoothDamp + 切换风向。乱度粘滞由 driver 保留。
/// </summary>
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);
}
/// <summary>仅切换风况(选择前 telegraph 风向)。</summary>
/// <param name="turbulence">乱度 0~1;负数或省略表示平稳(清零)。设定后粘滞,kite_state 沿用。</param>
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);
}
/// <summary>
/// 一次性顿挫动效,叠加在 Accent 层上,结束回到原位。
/// 镜头采样合成位姿,会自然追着 accent 跑。
/// </summary>
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;
}
/// <summary>
/// 隐藏风筝 Rig。与显示一致不做淡出,直接消失。
/// </summary>
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
/// <summary>
/// 淡入(异步协程版本)
/// </summary>
/// <param name="duration">淡入时长</param>
/// <returns>协程</returns>
public IEnumerator FadeInAsync(float duration = 1)
{
if (_backgroundSpriteRenderer == null) yield break;
_backgroundSpriteRenderer.gameObject.SetActive(true);
yield return _backgroundSpriteRenderer.DOFade(1, duration).WaitForCompletion();
}
/// <summary>
/// 淡出(异步协程版本)
/// </summary>
/// <param name="duration">淡出时长</param>
/// <returns>协程</returns>
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);
}
/// <summary>
/// 淡入(同步版本,启动协程)
/// </summary>
/// <param name="duration">淡入时长</param>
public void FadeInSync(float duration = 1)
{
StartCoroutine(FadeInAsync(duration));
}
/// <summary>
/// 淡出(同步版本,启动协程)
/// </summary>
/// <param name="duration">淡出时长</param>
public void FadeOutSync(float duration = 1)
{
StartCoroutine(FadeOutAsync(duration));
}
}
}