Files
aibis-dream/Assets/Scripts/SceneManagement/SpriteShowcase.cs
T

884 lines
38 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.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 变量 $wind1 强 / 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):大而缓的漫游位移,自由自在地飘,绝不抖。
/// 乱流(乱度 &gt;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));
}
}
}