feat(scene-mgmt): 实现风筝精灵分层动效系统

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-10 22:32:52 +08:00
co-authored by Cursor
parent a1bfa4c8b8
commit 18d6d970f9
@@ -13,17 +13,30 @@ namespace AibisDream
{
/// <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 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;
// 通过 activeSelf 判断当前状态
private bool IsSmallShowing => _spriteRenderer.gameObject.activeSelf;
private bool IsLargeShowing => _largeSpriteRenderer != null && _largeSpriteRenderer.gameObject.activeSelf;
public override void OnSingletonInit()
{
CaptureSmallBaseTransform();
// 初始状态:隐藏
_spriteRenderer.gameObject.SetActive(false);
_spriteRenderer.SetAlpha(0);
@@ -36,6 +49,16 @@ namespace AibisDream
}
}
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;
@@ -55,6 +78,246 @@ namespace AibisDream
Debug.LogError($"[SpriteShowcase] Failed to load sprite: {key}");
}
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);
}
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);
var isSplitMotionPic = !string.IsNullOrEmpty(normalized)
&& (normalized.StartsWith("D2S风筝", StringComparison.Ordinal)
|| normalized == "D2S轱辘")
&& !normalized.EndsWith(KiteBaseSuffix, StringComparison.Ordinal)
&& !normalized.EndsWith(KiteMotionSuffix, StringComparison.Ordinal);
if (isSplitMotionPic)
{
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)
{
// 近景微风:位移/倾角更明显,多段不对称节奏模拟阵风
case "kite_low_breeze":
seq.Append(tr.DOLocalMove(BaseOffset(0.32f, 0.21f), 2.8f).SetEase(Ease.InOutSine));
seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, 6.4f), 2.8f).SetEase(Ease.InOutSine));
seq.Append(tr.DOLocalMove(BaseOffset(-0.13f, 0.06f), 1.35f).SetEase(Ease.InOutSine));
seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, -3.6f), 1.35f).SetEase(Ease.InOutSine));
seq.Append(tr.DOLocalMove(BaseOffset(0.25f, 0.17f), 2.45f).SetEase(Ease.InOutSine));
seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, 4.8f), 2.45f).SetEase(Ease.InOutSine));
seq.Append(tr.DOLocalMove(BaseOffset(0.06f, 0.12f), 0.95f).SetEase(Ease.OutSine));
seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, 1.2f), 0.95f).SetEase(Ease.OutSine));
seq.SetLoops(-1, LoopType.Yoyo);
break;
case "kite_mid_breeze":
seq.Append(tr.DOLocalMove(BaseOffset(0.3f, 0.19f), 2.35f).SetEase(Ease.InOutSine));
seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, 6f), 2.35f).SetEase(Ease.InOutSine));
seq.Append(tr.DOLocalMove(BaseOffset(-0.14f, 0.07f), 1.18f).SetEase(Ease.InOutSine));
seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, -3.5f), 1.18f).SetEase(Ease.InOutSine));
seq.Append(tr.DOLocalMove(BaseOffset(0.27f, 0.16f), 2.05f).SetEase(Ease.InOutSine));
seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, 4.8f), 2.05f).SetEase(Ease.InOutSine));
seq.Append(tr.DOLocalMove(BaseOffset(0.02f, 0.1f), 0.82f).SetEase(Ease.OutSine));
seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, -0.8f), 0.82f).SetEase(Ease.OutSine));
seq.SetLoops(-1, LoopType.Yoyo);
break;
case "kite_far_breeze":
seq.Append(tr.DOLocalMove(BaseOffset(0.2f, 0.12f), 3.2f).SetEase(Ease.InOutSine));
seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, 4f), 3.2f).SetEase(Ease.InOutSine));
seq.Append(tr.DOLocalMove(BaseOffset(-0.08f, 0.04f), 2.2f).SetEase(Ease.InOutSine));
seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, -2.2f), 2.2f).SetEase(Ease.InOutSine));
seq.SetLoops(-1, LoopType.Yoyo);
break;
case "kite_tension":
seq.Append(tr.DOLocalMove(BaseOffset(0.48f, 0.15f), 0.92f).SetEase(Ease.InOutSine));
seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, -9f), 0.92f).SetEase(Ease.InOutSine));
seq.Append(tr.DOLocalMove(BaseOffset(0.1f, -0.13f), 0.58f).SetEase(Ease.InOutSine));
seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, 4.2f), 0.58f).SetEase(Ease.InOutSine));
seq.Append(tr.DOLocalMove(BaseOffset(0.39f, 0.11f), 0.9f).SetEase(Ease.InOutSine));
seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, -6.6f), 0.9f).SetEase(Ease.InOutSine));
seq.SetLoops(-1, LoopType.Yoyo);
break;
case "kite_drop":
seq.Append(tr.DOLocalMove(BaseOffset(0.1f, -0.34f), 1.55f).SetEase(Ease.InOutSine));
seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, -5.5f), 1.55f).SetEase(Ease.InOutSine));
seq.Append(tr.DOLocalMove(BaseOffset(-0.08f, -0.18f), 1.2f).SetEase(Ease.InOutSine));
seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, 2.8f), 1.2f).SetEase(Ease.InOutSine));
seq.SetLoops(-1, LoopType.Yoyo);
break;
case "kite_breakaway":
seq.Append(tr.DOLocalMove(BaseOffset(0.58f, 0.2f), 0.58f).SetEase(Ease.OutSine));
seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, -12f), 0.58f).SetEase(Ease.OutSine));
seq.Append(tr.DOShakePosition(0.28f, new Vector3(0.075f, 0.035f, 0f), 10, 78f, false, false)
.SetEase(Ease.InOutSine));
seq.Append(tr.DOLocalMove(BaseOffset(0.14f, -0.16f), 0.46f).SetEase(Ease.InOutSine));
seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, 5.8f), 0.46f).SetEase(Ease.InOutSine));
seq.Append(tr.DOLocalMove(BaseOffset(0.5f, 0.16f), 0.66f).SetEase(Ease.InOutSine));
seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, -9f), 0.66f).SetEase(Ease.InOutSine));
seq.SetLoops(-1, LoopType.Yoyo);
break;
case "reel_tension":
seq.Append(tr.DOLocalMove(BaseOffset(0.14f, -0.18f), 0.5f).SetEase(Ease.InOutSine));
seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, -3.8f), 0.5f).SetEase(Ease.InOutSine));
seq.Append(tr.DOShakePosition(0.22f, new Vector3(0.04f, 0.02f, 0f), 8, 82f, false, false)
.SetEase(Ease.InOutSine));
seq.Append(tr.DOLocalMove(BaseOffset(-0.06f, -0.06f), 0.7f).SetEase(Ease.InOutSine));
seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, 1.6f), 0.7f).SetEase(Ease.InOutSine));
seq.Append(tr.DOLocalMove(BaseOffset(0.08f, -0.14f), 0.92f).SetEase(Ease.InOutSine));
seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, -2.2f), 0.92f).SetEase(Ease.InOutSine));
seq.SetLoops(-1, LoopType.Yoyo);
break;
case "kite_lift":
seq.Append(tr.DOLocalMove(BaseOffset(0.28f, 0.34f), 0.68f).SetEase(Ease.OutSine));
seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, -6.4f), 0.68f).SetEase(Ease.OutSine));
seq.Append(tr.DOLocalMove(BaseOffset(0.16f, 0.22f), 0.78f).SetEase(Ease.InOutSine));
seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, -3.2f), 0.78f).SetEase(Ease.InOutSine));
break;
case "kite_sink":
seq.Append(tr.DOLocalMove(BaseOffset(-0.14f, -0.34f), 0.76f).SetEase(Ease.InOutSine));
seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, 5.2f), 0.76f).SetEase(Ease.InOutSine));
seq.Append(tr.DOLocalMove(BaseOffset(-0.06f, -0.22f), 0.62f).SetEase(Ease.InOutSine));
seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, 2.4f), 0.62f).SetEase(Ease.InOutSine));
break;
case "kite_taut":
seq.Append(tr.DOLocalMove(BaseOffset(0.4f, 0.08f), 0.42f).SetEase(Ease.OutSine));
seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, -8f), 0.42f).SetEase(Ease.OutSine));
seq.Append(tr.DOShakePosition(0.24f, new Vector3(0.06f, 0.025f, 0f), 8, 76f, false, false)
.SetEase(Ease.InOutSine));
seq.Append(tr.DOLocalMove(BaseOffset(0.24f, 0.03f), 0.62f).SetEase(Ease.InOutSine));
seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, -4.2f), 0.62f).SetEase(Ease.InOutSine));
break;
default:
seq.Kill();
return;
}
_smallMotionSequence = seq;
}
public void PlayCurrentMotion(string motionProfile)
{
if (_spriteRenderer == null || !IsSmallShowing) return;
PlaySmallMotion(motionProfile);
}
/// <summary>
/// 显示物体(小图)
/// </summary>
@@ -69,13 +332,82 @@ namespace AibisDream
yield return HideLargeObj(duration);
}
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);
yield return LoadDreamSpriteInto(_spriteRenderer, picName);
_currentSmallPicName = NormalizeDreamPicName(picName);
yield return _spriteRenderer.DOFade(1, duration).WaitForCompletion();
}
public IEnumerator ShowObjWithMotion(string picName, string motionProfile, float duration)
{
// 互斥:如果大图正在显示,先隐藏
if (IsLargeShowing)
{
yield return HideLargeObj(duration);
}
var normalizedName = NormalizeDreamPicName(picName);
var useSplitMotion = TryGetSplitMotionPicNames(picName, out var basePicName, out var motionPicName);
var isSamePic = string.Equals(_currentSmallPicName, normalizedName, StringComparison.OrdinalIgnoreCase);
_smallMotionBaseOffset = normalizedName == "D2S轱辘" ? new Vector3(0f, -0.32f, 0f) : Vector3.zero;
KillSmallTweenState();
ResetSmallTransform(resetMotionLayer: !isSamePic);
_spriteRenderer.gameObject.SetActive(true);
if (!isSamePic || _spriteRenderer.sprite == null)
{
yield return LoadDreamSpriteInto(_spriteRenderer, useSplitMotion ? basePicName : picName);
_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)
{
yield return LoadDreamSpriteInto(_smallMotionRenderer, motionPicName);
}
_smallMotionRenderer.SetAlpha(1f);
}
else if (_smallMotionRenderer != null)
{
_smallMotionRenderer.gameObject.SetActive(false);
_smallMotionRenderer.SetAlpha(0);
_smallMotionRenderer.maskInteraction = SpriteMaskInteraction.None;
if (_smallMotionMask != null)
{
_smallMotionMask.enabled = false;
}
}
PlaySmallMotion(motionProfile);
}
/// <summary>
/// 显示大图
/// </summary>
@@ -84,10 +416,10 @@ namespace AibisDream
/// <returns>协程</returns>
public IEnumerator ShowLargeObj(string picName, float duration = 1)
{
// 互斥:如果小图正在显示,先隐藏
// 互斥:小图立刻关掉,避免风筝层与底图不同步淡出造成的怪 fade
if (IsSmallShowing)
{
yield return HideObj(duration);
yield return HideObj(0f);
}
if (_largeSpriteRenderer == null) yield break;
@@ -120,8 +452,38 @@ namespace AibisDream
// force=false 时检查状态,未显示则直接返回
if (!force && !IsSmallShowing) yield break;
yield return _spriteRenderer.DOFade(0, duration).WaitForCompletion();
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;
if (_smallMotionRenderer != null)
{
_smallMotionRenderer.gameObject.SetActive(false);
_smallMotionRenderer.SetAlpha(0);
_smallMotionRenderer.maskInteraction = SpriteMaskInteraction.None;
}
if (_smallMotionMask != null)
{
_smallMotionMask.enabled = false;
}
}
/// <summary>