884 lines
38 KiB
C#
884 lines
38 KiB
C#
using System;
|
||
using System.Collections;
|
||
using AibisDream.Framework;
|
||
using AibisDream.Kit;
|
||
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 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);
|
||
|
||
// 大图初始状态:隐藏
|
||
if (_largeSpriteRenderer != null)
|
||
{
|
||
_largeSpriteRenderer.gameObject.SetActive(false);
|
||
_largeSpriteRenderer.SetAlpha(0);
|
||
}
|
||
}
|
||
|
||
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)
|
||
{
|
||
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;
|
||
else
|
||
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);
|
||
|
||
// 风筝图已改由风筝 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)
|
||
{
|
||
// 轱辘:持续拉扯里的几次失控顿挫。单向循环,避免 Yoyo 反放造成“被倒带”的怪感。
|
||
case "reel_tension":
|
||
seq.Append(tr.DOLocalMove(BaseOffset(0.03f, -0.05f), 0.28f).SetEase(Ease.InOutSine));
|
||
seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, -1.5f), 0.28f).SetEase(Ease.InOutSine));
|
||
seq.Append(tr.DOLocalMove(BaseOffset(0.13f, -0.18f), 0.16f).SetEase(Ease.OutCubic));
|
||
seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, -4.8f), 0.16f).SetEase(Ease.OutCubic));
|
||
seq.Append(tr.DOLocalMove(BaseOffset(0.08f, -0.11f), 0.48f).SetEase(Ease.InOutSine));
|
||
seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, -2.4f), 0.48f).SetEase(Ease.InOutSine));
|
||
seq.AppendInterval(0.12f);
|
||
seq.Append(tr.DOLocalMove(BaseOffset(0.19f, -0.25f), 0.13f).SetEase(Ease.OutCubic));
|
||
seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, -6.5f), 0.13f).SetEase(Ease.OutCubic));
|
||
seq.Append(tr.DOLocalMove(BaseOffset(0.11f, -0.15f), 0.62f).SetEase(Ease.InOutSine));
|
||
seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, -3.2f), 0.62f).SetEase(Ease.InOutSine));
|
||
seq.AppendInterval(0.2f);
|
||
seq.Append(tr.DOLocalMove(BaseOffset(0.15f, -0.21f), 0.18f).SetEase(Ease.OutCubic));
|
||
seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, -5.2f), 0.18f).SetEase(Ease.OutCubic));
|
||
seq.Append(tr.DOLocalMove(BaseOffset(0.06f, -0.09f), 0.75f).SetEase(Ease.InOutSine));
|
||
seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, -2f), 0.75f).SetEase(Ease.InOutSine));
|
||
seq.AppendInterval(0.18f);
|
||
seq.SetLoops(-1, LoopType.Restart);
|
||
break;
|
||
|
||
default:
|
||
seq.Kill();
|
||
return;
|
||
}
|
||
|
||
_smallMotionSequence = seq;
|
||
}
|
||
|
||
public void PlayCurrentMotion(string motionProfile)
|
||
{
|
||
if (_spriteRenderer == null || !IsSmallShowing) return;
|
||
PlaySmallMotion(motionProfile);
|
||
}
|
||
|
||
/// <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);
|
||
|
||
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);
|
||
}
|
||
|
||
// 互斥:风筝 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);
|
||
_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>
|
||
/// <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);
|
||
|
||
yield return LoadDreamSpriteInto(_largeSpriteRenderer, 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;
|
||
|
||
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);
|
||
}
|
||
|
||
/// <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);
|
||
}
|
||
|
||
#region 风筝 Rig
|
||
|
||
// 风筝段落弃用切图(D2S风筝1/2/3),改为单张风筝图 + 三层 Transform 连续驱动:
|
||
// Kite Rig(高度补间:缩放+位移)→ Sway(常驻风况循环)→ Accent(一次性顿挫 + Renderer)
|
||
// 三层各持有独立 Tween,互不覆盖。白色底图挂在独立的静态 renderer 上,不随高度缩放。
|
||
private const string KiteSpritePicName = "D2S风筝1_风筝层";
|
||
private const string KiteBasePicName = "D2S风筝1_底图";
|
||
private const float KiteMaxHeight = 13f;
|
||
/// <summary>每升高 1 点高度,缩放乘以该系数。</summary>
|
||
private const float KiteScalePerHeight = 0.84f;
|
||
/// <summary>缩放下限:最远处的风筝也不小于该比例。</summary>
|
||
private const float KiteMinScaleFactor = 0.12f;
|
||
/// <summary>高度最大时风筝相对基准位置的漂移量(向上飘 + 少量横移)。</summary>
|
||
private static readonly Vector3 KiteTopDrift = new Vector3(0.5f, 1.2f, 0f);
|
||
|
||
private Transform _kiteRootPivot;
|
||
private Transform _kiteSwayPivot;
|
||
private SpriteRenderer _kiteRenderer;
|
||
private SpriteRenderer _kiteBaseRenderer;
|
||
private SpriteMask _kiteMask;
|
||
private DG.Tweening.Sequence _kiteHeightSequence;
|
||
private DG.Tweening.Sequence _kiteSwaySequence;
|
||
private DG.Tweening.Sequence _kiteAccentSequence;
|
||
private Tween _kiteFadeTween;
|
||
private Tween _kiteBaseFadeTween;
|
||
private float _kiteHeight;
|
||
/// <summary>当前乱度 0~1,由 kite_wind 显式设定;kite_state 沿用上次的值(粘滞)。</summary>
|
||
private float _kiteTurbulence;
|
||
private bool _kiteSpriteLoaded;
|
||
private bool _kiteBaseSpriteLoaded;
|
||
|
||
private bool IsKiteShowing => _kiteRootPivot != null && _kiteRootPivot.gameObject.activeSelf;
|
||
|
||
private void EnsureKiteRig()
|
||
{
|
||
if (_kiteRootPivot != null || _spriteRenderer == null) return;
|
||
|
||
var root = new GameObject("Kite Rig");
|
||
root.transform.SetParent(_spriteRenderer.transform.parent);
|
||
root.transform.localPosition = _smallBaseLocalPosition;
|
||
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(_spriteRenderer.transform.parent);
|
||
baseObject.transform.localPosition = _smallBaseLocalPosition;
|
||
baseObject.transform.localRotation = _smallBaseLocalRotation;
|
||
baseObject.transform.localScale = _smallBaseLocalScale;
|
||
|
||
// 遮罩:把风筝层限制在底图(天空)范围内,不会飘出画框
|
||
var maskObject = new GameObject("Kite Mask");
|
||
maskObject.transform.SetParent(_spriteRenderer.transform.parent);
|
||
maskObject.transform.localPosition = _smallBaseLocalPosition;
|
||
maskObject.transform.localRotation = _smallBaseLocalRotation;
|
||
maskObject.transform.localScale = _smallBaseLocalScale;
|
||
|
||
_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;
|
||
|
||
root.SetActive(false);
|
||
baseObject.SetActive(false);
|
||
_kiteRenderer.SetAlpha(0);
|
||
_kiteBaseRenderer.SetAlpha(0);
|
||
}
|
||
|
||
private static float KiteScaleFactor(float height) =>
|
||
Mathf.Max(Mathf.Pow(KiteScalePerHeight, height), KiteMinScaleFactor);
|
||
|
||
private Vector3 KitePosition(float height) =>
|
||
_smallBaseLocalPosition + KiteTopDrift * (height / KiteMaxHeight);
|
||
|
||
private Vector3 KiteScale(float height) => _smallBaseLocalScale * KiteScaleFactor(height);
|
||
|
||
/// <summary>极高处降低不透明度,配合“几乎看不见”的文本。</summary>
|
||
private static float KiteAlpha(float height) =>
|
||
height <= 8f ? 1f : Mathf.Lerp(1f, 0.75f, (height - 8f) / (KiteMaxHeight - 8f));
|
||
|
||
private void KillKiteTweens()
|
||
{
|
||
_kiteHeightSequence?.Kill();
|
||
_kiteHeightSequence = null;
|
||
_kiteSwaySequence?.Kill();
|
||
_kiteSwaySequence = null;
|
||
_kiteAccentSequence?.Kill();
|
||
_kiteAccentSequence = null;
|
||
_kiteFadeTween?.Kill();
|
||
_kiteFadeTween = null;
|
||
_kiteBaseFadeTween?.Kill();
|
||
_kiteBaseFadeTween = null;
|
||
}
|
||
|
||
private void ApplyKiteStateImmediate(float height)
|
||
{
|
||
_kiteRootPivot.localPosition = KitePosition(height);
|
||
_kiteRootPivot.localRotation = _smallBaseLocalRotation;
|
||
_kiteRootPivot.localScale = KiteScale(height);
|
||
_kiteSwayPivot.localPosition = Vector3.zero;
|
||
_kiteSwayPivot.localRotation = Quaternion.identity;
|
||
_kiteRenderer.transform.localPosition = Vector3.zero;
|
||
_kiteRenderer.transform.localRotation = Quaternion.identity;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 显示风筝 Rig。已显示时等价于 <see cref="SetKiteState"/>(幂等)。
|
||
/// </summary>
|
||
/// <param name="height">风筝高度(对应 Yarn 变量 $h)</param>
|
||
/// <param name="wind">风向(对应 Yarn 变量 $wind:1 强 / 0 无 / -1 弱)</param>
|
||
/// <param name="duration">淡入时长</param>
|
||
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) yield break;
|
||
|
||
height = Mathf.Clamp(height, 0f, KiteMaxHeight);
|
||
|
||
if (IsKiteShowing)
|
||
{
|
||
SetKiteState(height, wind, duration);
|
||
yield break;
|
||
}
|
||
|
||
KillKiteTweens();
|
||
ApplyKiteStateImmediate(height);
|
||
_kiteHeight = height;
|
||
_kiteRootPivot.gameObject.SetActive(true);
|
||
_kiteBaseRenderer.gameObject.SetActive(true);
|
||
|
||
if (!_kiteSpriteLoaded)
|
||
{
|
||
yield return LoadDreamSpriteInto(_kiteRenderer, KiteSpritePicName);
|
||
_kiteSpriteLoaded = _kiteRenderer.sprite != null;
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
_kiteTurbulence = 0f;
|
||
PlayKiteSway(wind);
|
||
_kiteBaseFadeTween = _kiteBaseRenderer.DOFade(1f, duration);
|
||
_kiteFadeTween = _kiteRenderer.DOFade(KiteAlpha(height), duration);
|
||
yield return _kiteFadeTween.WaitForCompletion();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 高度补间(每次选择的主反馈)+ 切换风况循环。
|
||
/// </summary>
|
||
public void SetKiteState(float height, int wind, float duration = 0.9f)
|
||
{
|
||
if (!IsKiteShowing) return;
|
||
|
||
height = Mathf.Clamp(height, 0f, KiteMaxHeight);
|
||
_kiteHeight = height;
|
||
|
||
_kiteHeightSequence?.Kill();
|
||
var seq = DOTween.Sequence();
|
||
seq.Append(_kiteRootPivot.DOLocalMove(KitePosition(height), duration).SetEase(Ease.InOutSine));
|
||
seq.Join(_kiteRootPivot.DOScale(KiteScale(height), duration).SetEase(Ease.InOutSine));
|
||
_kiteHeightSequence = seq;
|
||
|
||
_kiteFadeTween?.Kill();
|
||
_kiteFadeTween = _kiteRenderer.DOFade(KiteAlpha(height), duration);
|
||
|
||
PlayKiteSway(wind);
|
||
}
|
||
|
||
/// <summary>仅切换风况循环(选择前 telegraph 风向)。</summary>
|
||
/// <param name="wind">风向:1 强 / 0 无 / -1 弱</param>
|
||
/// <param name="turbulence">乱度 0~1;负数或省略表示平稳。设定后粘滞,kite_state 沿用。</param>
|
||
public void SetKiteWind(int wind, float turbulence = -1f)
|
||
{
|
||
if (!IsKiteShowing) return;
|
||
_kiteTurbulence = turbulence >= 0f ? Mathf.Clamp01(turbulence) : 0f;
|
||
PlayKiteSway(wind);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 常驻风况循环。位移在高度层的子空间里会随缩放变小,用 comp 做部分补偿保证远处仍有可读的移动。
|
||
/// 平稳(乱度 0):大而缓的漫游位移,自由自在地飘,绝不抖。
|
||
/// 乱流(乱度 >0):一阵一阵——静一下,猛地被拽走一截(位移+旋转),慢慢荡回来,再来一阵。
|
||
/// </summary>
|
||
private void PlayKiteSway(int wind)
|
||
{
|
||
_kiteSwaySequence?.Kill();
|
||
_kiteSwaySequence = null;
|
||
|
||
var tr = _kiteSwayPivot;
|
||
var storm = _kiteTurbulence;
|
||
// 屏幕可读性补偿:高度层缩小时适度放大局部位移(开方衰减),避免远处几乎不动
|
||
var comp = Mathf.Clamp(1f / Mathf.Sqrt(KiteScaleFactor(_kiteHeight)), 1f, 2.2f);
|
||
var seq = DOTween.Sequence();
|
||
var loopType = LoopType.Yoyo;
|
||
|
||
if (wind > 0 && storm > 0.01f)
|
||
{
|
||
// 乱流:先被阵风突然带走,再慢慢回撑;间隔不等,让“失控”读出来但不变成抖屏。
|
||
loopType = LoopType.Restart;
|
||
var amp = comp * (0.95f + storm * 0.95f);
|
||
seq.AppendInterval(Mathf.Lerp(0.35f, 0.12f, storm));
|
||
seq.Append(tr.DOLocalMove(new Vector3(0.46f, 0.08f, 0f) * amp, 0.22f).SetEase(Ease.OutCubic));
|
||
seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, -13f - storm * 8f), 0.22f).SetEase(Ease.OutCubic));
|
||
seq.Append(tr.DOLocalMove(new Vector3(0.2f, -0.08f, 0f) * amp, 0.95f).SetEase(Ease.InOutSine));
|
||
seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, 5f + storm * 2f), 0.95f).SetEase(Ease.InOutSine));
|
||
seq.AppendInterval(0.08f);
|
||
seq.Append(tr.DOLocalMove(new Vector3(-0.3f, 0.16f, 0f) * amp, 0.18f).SetEase(Ease.OutCubic));
|
||
seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, 12f + storm * 7f), 0.18f).SetEase(Ease.OutCubic));
|
||
seq.Append(tr.DOLocalMove(new Vector3(-0.04f, 0.02f, 0f) * amp, 0.72f).SetEase(Ease.InOutSine));
|
||
seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, -2f), 0.72f).SetEase(Ease.InOutSine));
|
||
seq.AppendInterval(Mathf.Lerp(0.28f, 0.08f, storm));
|
||
seq.Append(tr.DOLocalMove(new Vector3(0.36f, 0.2f, 0f) * amp, 0.16f).SetEase(Ease.OutCubic));
|
||
seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, -10f - storm * 5f), 0.16f).SetEase(Ease.OutCubic));
|
||
seq.Append(tr.DOLocalMove(new Vector3(0.1f, 0.04f, 0f) * amp, 1.05f).SetEase(Ease.InOutSine));
|
||
seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, 2f), 1.05f).SetEase(Ease.InOutSine));
|
||
}
|
||
else if (wind > 0)
|
||
{
|
||
// 稳定的风:顶着风侧倾,大幅度缓慢巡游(位移为主,平稳无抖)
|
||
seq.Append(tr.DOLocalMove(new Vector3(0.3f, 0.18f, 0f) * comp, 2.6f).SetEase(Ease.InOutSine));
|
||
seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, -7f), 2.6f).SetEase(Ease.InOutSine));
|
||
seq.Append(tr.DOLocalMove(new Vector3(0.42f, 0.06f, 0f) * comp, 2.2f).SetEase(Ease.InOutSine));
|
||
seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, -4f), 2.2f).SetEase(Ease.InOutSine));
|
||
seq.Append(tr.DOLocalMove(new Vector3(0.12f, 0.12f, 0f) * comp, 2.8f).SetEase(Ease.InOutSine));
|
||
seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, -8.5f), 2.8f).SetEase(Ease.InOutSine));
|
||
}
|
||
else if (wind == 0)
|
||
{
|
||
// 平稳无风:自由自在地漫游,大而缓的位移
|
||
seq.Append(tr.DOLocalMove(new Vector3(0.26f, 0.14f, 0f) * comp, 3.2f).SetEase(Ease.InOutSine));
|
||
seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, 4f), 3.2f).SetEase(Ease.InOutSine));
|
||
seq.Append(tr.DOLocalMove(new Vector3(-0.2f, 0.24f, 0f) * comp, 3.6f).SetEase(Ease.InOutSine));
|
||
seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, -4.5f), 3.6f).SetEase(Ease.InOutSine));
|
||
seq.Append(tr.DOLocalMove(new Vector3(-0.3f, 0.04f, 0f) * comp, 3f).SetEase(Ease.InOutSine));
|
||
seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, 3f), 3f).SetEase(Ease.InOutSine));
|
||
}
|
||
else
|
||
{
|
||
// 风弱:缓慢的下坠漂移
|
||
seq.Append(tr.DOLocalMove(new Vector3(0.08f, -0.22f, 0f) * comp, 2.6f).SetEase(Ease.InOutSine));
|
||
seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, 3.5f), 2.6f).SetEase(Ease.InOutSine));
|
||
seq.Append(tr.DOLocalMove(new Vector3(-0.14f, -0.1f, 0f) * comp, 2.2f).SetEase(Ease.InOutSine));
|
||
seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, -2f), 2.2f).SetEase(Ease.InOutSine));
|
||
}
|
||
|
||
seq.SetLoops(-1, loopType);
|
||
_kiteSwaySequence = seq;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 一次性顿挫动效,叠加在 Accent 层上,结束回到原位,不打断高度/风况补间。
|
||
/// 支持:lift(上冲回落)/ sink(下坠沉降)/ taut(前扯抖动)/ drop(猛地下坠)/ flutter(原地挣扎扑腾)。
|
||
/// </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.DOLocalMove(new Vector3(0.14f, 0.3f, 0f), 0.35f).SetEase(Ease.OutQuad));
|
||
seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, -6f), 0.35f).SetEase(Ease.OutQuad));
|
||
break;
|
||
|
||
case "sink":
|
||
seq.Append(tr.DOLocalMove(new Vector3(-0.06f, -0.28f, 0f), 0.4f).SetEase(Ease.OutSine));
|
||
seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, 5f), 0.4f).SetEase(Ease.OutSine));
|
||
break;
|
||
|
||
// 绷紧:猛地被扯直,然后绷住缓慢微颤(小幅慢速拉锯,不是震动)
|
||
case "taut":
|
||
seq.Append(tr.DOLocalMove(new Vector3(0.3f, 0.06f, 0f), 0.2f).SetEase(Ease.OutCubic));
|
||
seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, -9f), 0.2f).SetEase(Ease.OutCubic));
|
||
seq.Append(tr.DOLocalMove(new Vector3(0.26f, 0.03f, 0f), 0.3f).SetEase(Ease.InOutSine));
|
||
seq.Append(tr.DOLocalMove(new Vector3(0.31f, 0.07f, 0f), 0.26f).SetEase(Ease.InOutSine));
|
||
seq.Append(tr.DOLocalMove(new Vector3(0.27f, 0.04f, 0f), 0.3f).SetEase(Ease.InOutSine));
|
||
break;
|
||
|
||
case "drop":
|
||
seq.Append(tr.DOLocalMove(new Vector3(0.04f, -0.42f, 0f), 0.55f).SetEase(Ease.InQuad));
|
||
seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, 7f), 0.55f).SetEase(Ease.InQuad));
|
||
break;
|
||
|
||
// 挣扎:左右交替的几下小拉扯,力度递减
|
||
case "flutter":
|
||
seq.Append(tr.DOLocalMove(new Vector3(0.1f, 0.14f, 0f), 0.2f).SetEase(Ease.OutCubic));
|
||
seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, -5f), 0.2f).SetEase(Ease.OutCubic));
|
||
seq.Append(tr.DOLocalMove(new Vector3(-0.08f, 0.06f, 0f), 0.26f).SetEase(Ease.InOutSine));
|
||
seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, 4.5f), 0.26f).SetEase(Ease.InOutSine));
|
||
seq.Append(tr.DOLocalMove(new Vector3(0.06f, 0.1f, 0f), 0.22f).SetEase(Ease.OutCubic));
|
||
seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, -3.5f), 0.22f).SetEase(Ease.OutCubic));
|
||
break;
|
||
|
||
default:
|
||
seq.Kill();
|
||
return;
|
||
}
|
||
|
||
// 回到原位,保证可与高度/风况补间叠加
|
||
seq.Append(tr.DOLocalMove(Vector3.zero, 0.5f).SetEase(Ease.InOutSine));
|
||
seq.Join(tr.DOLocalRotate(Vector3.zero, 0.5f).SetEase(Ease.InOutSine));
|
||
_kiteAccentSequence = seq;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 隐藏风筝 Rig
|
||
/// </summary>
|
||
/// <param name="duration">淡出时长</param>
|
||
public IEnumerator HideKite(float duration = 1)
|
||
{
|
||
if (!IsKiteShowing) yield break;
|
||
|
||
KillKiteTweens();
|
||
|
||
if (duration <= 0f)
|
||
{
|
||
_kiteRenderer.SetAlpha(0);
|
||
_kiteBaseRenderer.SetAlpha(0);
|
||
}
|
||
else
|
||
{
|
||
_kiteBaseFadeTween = _kiteBaseRenderer.DOFade(0, duration);
|
||
_kiteFadeTween = _kiteRenderer.DOFade(0, duration);
|
||
yield return _kiteFadeTween.WaitForCompletion();
|
||
}
|
||
|
||
_kiteRootPivot.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));
|
||
}
|
||
}
|
||
}
|