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

1066 lines
47 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)
{
// 轱辘:全力拉住——先被拽进受力姿态(只进一次),之后进入拔河式的角力拉锯。
// 拉锯循环见 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);
}
/// <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 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;
}
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);
// 不做淡入:背景是硬切的,风筝单独 fade 反而突兀,直接以目标透明度出现
_kiteBaseRenderer.SetAlpha(1f);
_kiteRenderer.SetAlpha(KiteAlpha(height));
}
/// <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>屏幕可读性补偿:高度层缩小时适度放大局部位移(开方衰减),避免远处几乎不动。</summary>
private float KiteSwayComp() =>
Mathf.Clamp(1f / Mathf.Sqrt(KiteScaleFactor(_kiteHeight)), 1f, 3f);
/// <summary>
/// 常驻风况循环。位移在高度层的子空间里会随缩放变小,用 comp 做部分补偿保证远处仍有可读的移动。
/// 平稳(乱度 0):沿 CatmullRom 曲线大而缓地漫游,绝不抖。
/// 乱流(乱度 &gt;0):转交 <see cref="PlayKiteGust"/>,每一阵的方向、力度、间隔都随机。
/// </summary>
private void PlayKiteSway(int wind)
{
_kiteSwaySequence?.Kill();
_kiteSwaySequence = null;
if (wind > 0 && _kiteTurbulence > 0.01f)
{
PlayKiteGust();
return;
}
var tr = _kiteSwayPivot;
var comp = KiteSwayComp();
// 幅度随高度补偿放大后,周期也同步拉长:漂移更明显,但速度不变快,保持轻松
var drift = 1f + (comp - 1f) * 0.4f;
var seq = DOTween.Sequence();
if (wind > 0)
{
// 稳定的风:顶着风侧倾,沿弧线缓慢巡游(位移为主,平稳无抖)
var path = new[]
{
new Vector3(0.26f, 0.15f, 0f) * comp,
new Vector3(0.46f, 0.05f, 0f) * comp,
new Vector3(0.33f, 0.24f, 0f) * comp,
new Vector3(0.11f, 0.11f, 0f) * comp,
};
var dur = 9.2f * drift;
seq.Append(tr.DOLocalPath(path, dur, PathType.CatmullRom).SetEase(Ease.InOutSine));
seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, -7.5f), dur / 4f).SetEase(Ease.InOutSine)
.SetLoops(4, LoopType.Yoyo));
}
else if (wind == 0)
{
// 平稳无风:自由自在地沿曲线漫游,幅度加大、节奏放缓
var path = new[]
{
new Vector3(0.3f, 0.16f, 0f) * comp,
new Vector3(0.08f, 0.36f, 0f) * comp,
new Vector3(-0.28f, 0.24f, 0f) * comp,
new Vector3(-0.38f, 0.03f, 0f) * comp,
};
var dur = 12f * drift;
seq.Append(tr.DOLocalPath(path, dur, PathType.CatmullRom).SetEase(Ease.InOutSine));
seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, 4f), dur / 4f).SetEase(Ease.InOutSine)
.SetLoops(4, LoopType.Yoyo));
}
else
{
// 风弱:沿弧线缓缓向下飘
var path = new[]
{
new Vector3(0.12f, -0.14f, 0f) * comp,
new Vector3(0.03f, -0.28f, 0f) * comp,
new Vector3(-0.17f, -0.21f, 0f) * comp,
};
var dur = 7.8f * drift;
seq.Append(tr.DOLocalPath(path, dur, PathType.CatmullRom).SetEase(Ease.InOutSine));
seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, 3f), dur / 3f).SetEase(Ease.InOutSine)
.SetLoops(3, LoopType.Yoyo));
}
seq.SetLoops(-1, LoopType.Yoyo);
_kiteSwaySequence = seq;
}
/// <summary>
/// 单次阵风,播完通过 OnComplete 用一组新的随机参数接下一阵——节奏永不重复。
/// 猛拽段 X 快、Y 慢半拍(不同时长+缓动),走出弧线而不是直线。
/// 乱度越高越狂:间隔与荡回时间大幅压缩、拽得更快更斜,
/// 甩过头弹回之外,还有概率来不及回稳就被反向再抽一鞭。
/// </summary>
private void PlayKiteGust()
{
if (!IsKiteShowing) return;
_kiteSwaySequence?.Kill();
var tr = _kiteSwayPivot;
var storm = _kiteTurbulence;
var amp = KiteSwayComp() * (1f + storm * 1.15f);
// 高乱度整体提速:猛拽更快、几乎没有喘息
var frantic = Mathf.Lerp(1f, 0.7f, storm);
// 阵风方向:多数顺着主风向,偶尔反向兜一下;上下随机
var side = UnityEngine.Random.value < 0.72f ? 1f : -1f;
var jerk = new Vector3(
UnityEngine.Random.Range(0.34f, 0.58f) * side,
UnityEngine.Random.Range(-0.1f, 0.3f),
0f) * amp;
var jerkDur = UnityEngine.Random.Range(0.14f, 0.26f) * frantic;
var rot = (14f + storm * 18f) * UnityEngine.Random.Range(0.7f, 1.15f) * -side;
var settle = jerk * UnityEngine.Random.Range(0.15f, 0.4f);
var settleDur = UnityEngine.Random.Range(0.7f, 1.2f) * Mathf.Lerp(1f, 0.45f, storm);
var rest = Mathf.Lerp(
UnityEngine.Random.Range(0.35f, 0.75f),
UnityEngine.Random.Range(0.02f, 0.1f),
storm);
var seq = DOTween.Sequence();
seq.AppendInterval(rest);
// 猛地被拽走:X 快 Y 慢,轨迹成弧
seq.Append(tr.DOLocalMoveX(jerk.x, jerkDur).SetEase(Ease.OutCubic));
seq.Join(tr.DOLocalMoveY(jerk.y, jerkDur * UnityEngine.Random.Range(1.4f, 1.9f)).SetEase(Ease.OutSine));
seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, rot), jerkDur * 1.2f).SetEase(Ease.OutCubic));
// 失控:甩过头再弹回来一下
if (storm > 0.7f && UnityEngine.Random.value < 0.6f)
{
seq.Append(tr.DOLocalRotate(new Vector3(0f, 0f, -rot * 0.65f), 0.22f).SetEase(Ease.InOutSine));
seq.Join(tr.DOLocalMoveX(jerk.x * 0.7f, 0.22f).SetEase(Ease.InOutSine));
}
// 更失控:来不及回稳,就被反方向再抽一鞭
if (storm > 0.6f && UnityEngine.Random.value < 0.5f)
{
var whipDur = UnityEngine.Random.Range(0.12f, 0.2f);
seq.Append(tr.DOLocalMoveX(-jerk.x * UnityEngine.Random.Range(0.5f, 0.9f), whipDur)
.SetEase(Ease.OutCubic));
seq.Join(tr.DOLocalMoveY(UnityEngine.Random.Range(-0.16f, 0.2f) * amp, whipDur * 1.5f)
.SetEase(Ease.OutSine));
seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, -rot * UnityEngine.Random.Range(0.7f, 1.1f)), whipDur * 1.3f)
.SetEase(Ease.OutCubic));
}
// 荡回来(X/Y 不同步 → 回程也是弧线;高乱度下这一步也短促)
seq.Append(tr.DOLocalMoveX(settle.x, settleDur).SetEase(Ease.InOutSine));
seq.Join(tr.DOLocalMoveY(settle.y, settleDur * UnityEngine.Random.Range(0.7f, 0.95f)).SetEase(Ease.InOutSine));
seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, rot * UnityEngine.Random.Range(-0.3f, 0.15f)), settleDur)
.SetEase(Ease.InOutSine));
seq.OnComplete(PlayKiteGust);
_kiteSwaySequence = seq;
}
/// <summary>
/// 一次性顿挫动效,叠加在 Accent 层上,结束回到原位,不打断高度/风况补间。
/// 支持:lift(上冲回落)/ sink(下坠沉降)/ taut(前扯抖动)/ drop(猛地下坠)/ flutter(原地挣扎扑腾)
/// / wrench(侧面兜住猛甩,最强档)。
/// </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 "wrench":
seq.Append(tr.DOLocalMove(new Vector3(-0.11f, 0.04f, 0f), 0.09f).SetEase(Ease.OutQuad));
seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, 4f), 0.09f).SetEase(Ease.OutQuad));
seq.Append(tr.DOLocalMove(new Vector3(0.7f, 0.2f, 0f), 0.16f).SetEase(Ease.OutCubic));
seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, -24f), 0.16f).SetEase(Ease.OutCubic));
seq.Append(tr.DOLocalMove(new Vector3(0.45f, 0.09f, 0f), 0.2f).SetEase(Ease.InOutSine));
seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, -10f), 0.2f).SetEase(Ease.InOutSine));
seq.Append(tr.DOLocalMove(new Vector3(0.92f, 0.3f, 0f), 0.15f).SetEase(Ease.OutCubic));
seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, -30f), 0.15f).SetEase(Ease.OutCubic));
seq.AppendInterval(0.25f);
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。与显示一致不做淡出,直接消失(过渡由外层屏幕 fade 承担)。
/// </summary>
/// <param name="duration">保留参数以兼容既有调用,实际忽略</param>
public IEnumerator HideKite(float duration = 1)
{
if (!IsKiteShowing) yield break;
KillKiteTweens();
_kiteRenderer.SetAlpha(0);
_kiteBaseRenderer.SetAlpha(0);
_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));
}
}
}