1563 lines
60 KiB
C#
1563 lines
60 KiB
C#
using System;
|
||
using System.Collections;
|
||
using AibisDream.Framework;
|
||
using AibisDream.Kit;
|
||
using AibisDream.SaveSystem;
|
||
using AibisDream.Utility;
|
||
using DG.Tweening;
|
||
using UnityEngine;
|
||
using UnityEngine.ResourceManagement.AsyncOperations;
|
||
|
||
namespace AibisDream
|
||
{
|
||
public class SpriteShowcase : Singleton<SpriteShowcase>
|
||
{
|
||
/// <summary>Addressable Key 模板,与 Scene_Dream 中梦境图 Address 一致。</summary>
|
||
private const string DreamSpritePath = "Sprite/梦境/{0}.png";
|
||
private const string KiteBaseSuffix = "_底图";
|
||
private const string KiteMotionSuffix = "_风筝层";
|
||
/// <summary>轱辘拉锯整体倍速;>1 更快。结尾全力拉住时用偏高倍速表现用力带来的身体微颤。</summary>
|
||
private const float ReelMotionSpeedMultiplier = 1.7f;
|
||
/// <summary>轱辘拉锯位移/旋转幅度倍率;偏小以贴近发力颤抖而非大幅晃动。</summary>
|
||
private const float ReelMotionAmplitude = 0.3f;
|
||
|
||
[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;
|
||
private string _currentLargePicName;
|
||
private bool _currentSmallUsesSplitLayers;
|
||
|
||
private DG.Tweening.Sequence _largePresentationSequence;
|
||
private Vector3 _largeBaseLocalPosition;
|
||
private Quaternion _largeBaseLocalRotation;
|
||
private Vector3 _largeBaseLocalScale;
|
||
private float _largeBaseAlpha;
|
||
private Material _largeBaseMaterial;
|
||
private MaterialPropertyBlock _largePropertyBlock;
|
||
private Coroutine _largeSpriteAnimationCoroutine;
|
||
|
||
private static readonly int BlurAmountId = Shader.PropertyToID("_BlurAmount");
|
||
private static readonly int BlurSizeId = Shader.PropertyToID("_BlurSize");
|
||
private static readonly int UseSoftFocusId = Shader.PropertyToID("_UseSoftFocus");
|
||
|
||
// 通过 activeSelf 判断当前状态
|
||
private bool IsSmallShowing => _spriteRenderer.gameObject.activeSelf;
|
||
private bool IsLargeShowing => _largeSpriteRenderer != null && _largeSpriteRenderer.gameObject.activeSelf;
|
||
|
||
public override void OnSingletonInit()
|
||
{
|
||
CaptureSmallBaseTransform();
|
||
CaptureLargeBaseTransform();
|
||
|
||
// 初始状态:隐藏
|
||
_spriteRenderer.gameObject.SetActive(false);
|
||
_spriteRenderer.SetAlpha(0);
|
||
|
||
// 大图初始状态:隐藏
|
||
if (_largeSpriteRenderer != null)
|
||
{
|
||
_largeSpriteRenderer.gameObject.SetActive(false);
|
||
_largeSpriteRenderer.SetAlpha(0);
|
||
}
|
||
}
|
||
|
||
private void OnDisable()
|
||
{
|
||
HideLargePresentationImmediate();
|
||
}
|
||
|
||
public override void OnSingletonDestroy()
|
||
{
|
||
HideLargePresentationImmediate();
|
||
base.OnSingletonDestroy();
|
||
}
|
||
|
||
private void CaptureSmallBaseTransform()
|
||
{
|
||
if (_spriteRenderer == null) return;
|
||
|
||
var tr = _spriteRenderer.transform;
|
||
_smallBaseLocalPosition = tr.localPosition;
|
||
_smallBaseLocalRotation = tr.localRotation;
|
||
_smallBaseLocalScale = tr.localScale;
|
||
}
|
||
|
||
private void CaptureLargeBaseTransform()
|
||
{
|
||
if (_largeSpriteRenderer == null) return;
|
||
|
||
var tr = _largeSpriteRenderer.transform;
|
||
_largeBaseLocalPosition = tr.localPosition;
|
||
_largeBaseLocalRotation = tr.localRotation;
|
||
_largeBaseLocalScale = tr.localScale;
|
||
_largeBaseAlpha = _largeSpriteRenderer.color.a;
|
||
_largeBaseMaterial = _largeSpriteRenderer.sharedMaterial;
|
||
}
|
||
|
||
private void StopLargeSpriteAnimation()
|
||
{
|
||
if (_largeSpriteAnimationCoroutine == null) return;
|
||
|
||
StopCoroutine(_largeSpriteAnimationCoroutine);
|
||
_largeSpriteAnimationCoroutine = null;
|
||
}
|
||
|
||
private IEnumerator LoopLargeSpriteAnimation(Sprite[] frames, float secondsPerFrame)
|
||
{
|
||
var frameIndex = 0;
|
||
var wait = new WaitForSeconds(Mathf.Max(0.01f, secondsPerFrame));
|
||
|
||
while (frames.Length > 0)
|
||
{
|
||
if (_largeSpriteRenderer != null)
|
||
{
|
||
_largeSpriteRenderer.sprite = frames[frameIndex];
|
||
}
|
||
|
||
frameIndex = (frameIndex + 1) % frames.Length;
|
||
yield return wait;
|
||
}
|
||
}
|
||
|
||
private void ApplyLargeSpriteBlur(float amount, float blurSize)
|
||
{
|
||
if (_largeSpriteRenderer == null) return;
|
||
|
||
amount = Mathf.Clamp01(amount);
|
||
if (amount <= 0.001f)
|
||
{
|
||
_largeSpriteRenderer.SetPropertyBlock(null);
|
||
_largeSpriteRenderer.sharedMaterial = _largeBaseMaterial;
|
||
return;
|
||
}
|
||
|
||
if (_kiteFocusMaterial == null)
|
||
{
|
||
Debug.LogWarning("[SpriteShowcase] 未配置全屏图片可用的对焦材质。", this);
|
||
return;
|
||
}
|
||
|
||
_largeSpriteRenderer.sharedMaterial = _kiteFocusMaterial;
|
||
_largePropertyBlock ??= new MaterialPropertyBlock();
|
||
_largeSpriteRenderer.GetPropertyBlock(_largePropertyBlock);
|
||
_largePropertyBlock.SetFloat(BlurAmountId, amount);
|
||
_largePropertyBlock.SetFloat(BlurSizeId, Mathf.Max(0f, blurSize));
|
||
_largePropertyBlock.SetFloat(UseSoftFocusId, 1f);
|
||
_largeSpriteRenderer.SetPropertyBlock(_largePropertyBlock);
|
||
}
|
||
|
||
public void SetLargeSpriteBlur(float amount, float blurSize = 0.012f)
|
||
{
|
||
ApplyLargeSpriteBlur(amount, blurSize);
|
||
}
|
||
|
||
public IEnumerator TweenLargeSpriteBlur(float targetAmount, float duration, float blurSize = 0.012f)
|
||
{
|
||
if (_largeSpriteRenderer == null) yield break;
|
||
|
||
_largePropertyBlock ??= new MaterialPropertyBlock();
|
||
_largeSpriteRenderer.GetPropertyBlock(_largePropertyBlock);
|
||
var startAmount = _largeSpriteRenderer.sharedMaterial == _kiteFocusMaterial
|
||
? _largePropertyBlock.GetFloat(BlurAmountId)
|
||
: 0f;
|
||
targetAmount = Mathf.Clamp01(targetAmount);
|
||
duration = Mathf.Max(0f, duration);
|
||
|
||
if (duration <= 0f)
|
||
{
|
||
ApplyLargeSpriteBlur(targetAmount, blurSize);
|
||
yield break;
|
||
}
|
||
|
||
var elapsed = 0f;
|
||
while (elapsed < duration)
|
||
{
|
||
elapsed += Time.deltaTime;
|
||
var t = Mathf.Clamp01(elapsed / duration);
|
||
t = t * t * (3f - 2f * t);
|
||
ApplyLargeSpriteBlur(Mathf.Lerp(startAmount, targetAmount, t), blurSize);
|
||
yield return null;
|
||
}
|
||
|
||
ApplyLargeSpriteBlur(targetAmount, blurSize);
|
||
}
|
||
|
||
public IEnumerator AnimateLargePresentation(float scaleMultiplier, float targetAlpha, float duration)
|
||
{
|
||
if (_largeSpriteRenderer == null) yield break;
|
||
|
||
KillLargePresentationTweens();
|
||
_largeSpriteRenderer.gameObject.SetActive(true);
|
||
|
||
scaleMultiplier = Mathf.Max(0.01f, scaleMultiplier);
|
||
targetAlpha = Mathf.Clamp01(targetAlpha);
|
||
duration = Mathf.Max(0f, duration);
|
||
|
||
var targetScale = new Vector3(
|
||
_largeBaseLocalScale.x * scaleMultiplier,
|
||
_largeBaseLocalScale.y * scaleMultiplier,
|
||
_largeBaseLocalScale.z);
|
||
|
||
if (duration <= 0f)
|
||
{
|
||
_largeSpriteRenderer.transform.localPosition = _largeBaseLocalPosition;
|
||
_largeSpriteRenderer.transform.localRotation = _largeBaseLocalRotation;
|
||
_largeSpriteRenderer.transform.localScale = targetScale;
|
||
_largeSpriteRenderer.SetAlpha(targetAlpha);
|
||
yield break;
|
||
}
|
||
|
||
_largePresentationSequence = DOTween.Sequence();
|
||
_largePresentationSequence.Join(
|
||
_largeSpriteRenderer.transform.DOScale(targetScale, duration).SetEase(Ease.InOutSine));
|
||
_largePresentationSequence.Join(
|
||
_largeSpriteRenderer.transform.DOLocalMove(_largeBaseLocalPosition, duration).SetEase(Ease.InOutSine));
|
||
_largePresentationSequence.Join(
|
||
_largeSpriteRenderer.DOFade(targetAlpha, duration).SetEase(Ease.InOutSine));
|
||
|
||
yield return _largePresentationSequence.WaitForCompletion();
|
||
_largePresentationSequence = null;
|
||
}
|
||
|
||
public void SetLargePresentationImmediate(float scaleMultiplier, float alpha)
|
||
{
|
||
if (_largeSpriteRenderer == null) return;
|
||
|
||
KillLargePresentationTweens();
|
||
scaleMultiplier = Mathf.Max(0.01f, scaleMultiplier);
|
||
|
||
_largeSpriteRenderer.transform.localPosition = _largeBaseLocalPosition;
|
||
_largeSpriteRenderer.transform.localRotation = _largeBaseLocalRotation;
|
||
_largeSpriteRenderer.transform.localScale = new Vector3(
|
||
_largeBaseLocalScale.x * scaleMultiplier,
|
||
_largeBaseLocalScale.y * scaleMultiplier,
|
||
_largeBaseLocalScale.z);
|
||
_largeSpriteRenderer.SetAlpha(Mathf.Clamp01(alpha));
|
||
}
|
||
|
||
public void StopLargePresentationAnimation()
|
||
{
|
||
KillLargePresentationTweens();
|
||
}
|
||
|
||
public void ResetLargePresentationImmediate()
|
||
{
|
||
if (_largeSpriteRenderer == null) return;
|
||
|
||
KillLargePresentationTweens();
|
||
_largeSpriteRenderer.transform.localPosition = _largeBaseLocalPosition;
|
||
_largeSpriteRenderer.transform.localRotation = _largeBaseLocalRotation;
|
||
_largeSpriteRenderer.transform.localScale = _largeBaseLocalScale;
|
||
_largeSpriteRenderer.SetAlpha(_largeBaseAlpha);
|
||
}
|
||
|
||
public void HideLargePresentationImmediate()
|
||
{
|
||
if (_largeSpriteRenderer == null) return;
|
||
|
||
StopLargeSpriteAnimation();
|
||
ApplyLargeSpriteBlur(0f, 0f);
|
||
ResetLargePresentationImmediate();
|
||
_largeSpriteRenderer.gameObject.SetActive(false);
|
||
}
|
||
|
||
private void KillLargePresentationTweens()
|
||
{
|
||
_largePresentationSequence?.Kill();
|
||
_largePresentationSequence = null;
|
||
|
||
if (_largeSpriteRenderer == null) return;
|
||
DOTween.Kill(_largeSpriteRenderer);
|
||
DOTween.Kill(_largeSpriteRenderer.transform);
|
||
}
|
||
|
||
private static string NormalizeDreamPicName(string picName)
|
||
{
|
||
if (string.IsNullOrEmpty(picName)) return picName;
|
||
return picName.EndsWith(".png", StringComparison.OrdinalIgnoreCase)
|
||
? picName.Substring(0, picName.Length - 4)
|
||
: picName;
|
||
}
|
||
|
||
private IEnumerator LoadDreamSpriteInto(
|
||
SpriteRenderer target,
|
||
string picName,
|
||
Action<bool> completed = null,
|
||
Action<string> warning = null)
|
||
{
|
||
var key = string.Format(DreamSpritePath, NormalizeDreamPicName(picName));
|
||
var handle = ResourceSystem.LoadAsync<Sprite>(key);
|
||
yield return handle;
|
||
if (handle.Status == AsyncOperationStatus.Succeeded && handle.Result != null)
|
||
{
|
||
target.sprite = handle.Result;
|
||
completed?.Invoke(true);
|
||
}
|
||
else
|
||
{
|
||
var message = $"[SpriteShowcase] Failed to load sprite: {key}";
|
||
Debug.LogError(message);
|
||
warning?.Invoke(message);
|
||
completed?.Invoke(false);
|
||
}
|
||
}
|
||
|
||
private void EnsureSmallMotionRenderer()
|
||
{
|
||
if (_smallMotionRenderer != null || _spriteRenderer == null) return;
|
||
|
||
var motionObject = new GameObject($"{_spriteRenderer.name} Motion");
|
||
motionObject.transform.SetParent(_spriteRenderer.transform.parent);
|
||
_smallMotionRenderer = motionObject.AddComponent<SpriteRenderer>();
|
||
CopySmallRendererSettings(_smallMotionRenderer, 1, SpriteMaskInteraction.VisibleInsideMask);
|
||
_smallMotionRenderer.gameObject.SetActive(false);
|
||
_smallMotionRenderer.SetAlpha(0);
|
||
}
|
||
|
||
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 static bool IsReelPic(string picName)
|
||
{
|
||
var normalized = NormalizeDreamPicName(picName);
|
||
return normalized == "D2S轱辘" || normalized == "D2S轱辘没线";
|
||
}
|
||
|
||
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 (IsReelPic(normalized))
|
||
{
|
||
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 * ReelMotionAmplitude, -0.32f * ReelMotionAmplitude), 0.38f)
|
||
.SetEase(Ease.OutCubic));
|
||
seq.Join(tr.DOLocalRotate(
|
||
new Vector3(0f, 0f, -2.8f * ReelMotionAmplitude), 0.38f)
|
||
.SetEase(Ease.OutCubic));
|
||
seq.OnComplete(() => PlayReelHold(tr));
|
||
break;
|
||
|
||
default:
|
||
seq.Kill();
|
||
return;
|
||
}
|
||
|
||
seq.timeScale = ReelMotionSpeedMultiplier;
|
||
_smallMotionSequence = seq;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 轱辘“拔河角力”常驻循环:一场正在输掉的拔河。
|
||
/// strain 随回合递增(0→1):节奏越来越快、姿态被拽得越来越深、猛夺越来越频繁;
|
||
/// 往回挣不再是平滑回弹,而是“向下被拖住→抬回一点→又沉下去→再咬住”的三段拉锯。
|
||
/// 每回合参数随机,OnComplete 续下一回合;用纵向位移和停顿表达强拉,避免机械抖动。
|
||
/// </summary>
|
||
private void PlayReelHold(Transform tr, float strain = 0f)
|
||
{
|
||
_smallMotionSequence?.Kill();
|
||
|
||
// 姿态整体随 strain 加深;时长随 strain 略收紧。旋转只做小角度,避免贴图边缘穿帮。
|
||
// deep*/rotBias 先按原尺度算,最后统一乘 amp,避免叠乘。
|
||
var amp = ReelMotionAmplitude;
|
||
var deepX = strain * 0.03f;
|
||
var deepY = strain * 0.07f;
|
||
// strain 越高越快,但收紧幅度比原先更克制,避免后期抖得诡异。
|
||
var speed = Mathf.Lerp(1f, 0.78f, strain);
|
||
var rotBias = strain * 0.35f;
|
||
|
||
var seq = DOTween.Sequence();
|
||
|
||
// 对面加力:被一点点拽深
|
||
var strainDur = UnityEngine.Random.Range(0.55f, 0.9f) * speed;
|
||
seq.Append(tr.DOLocalMove(
|
||
BaseOffset((UnityEngine.Random.Range(0.17f, 0.23f) + deepX) * amp,
|
||
(UnityEngine.Random.Range(-0.42f, -0.34f) - deepY) * amp),
|
||
strainDur)
|
||
.SetEase(Ease.InOutSine));
|
||
seq.Join(tr.DOLocalRotate(
|
||
new Vector3(0f, 0f, (UnityEngine.Random.Range(-3.6f, -2.4f) - rotBias) * amp), strainDur)
|
||
.SetEase(Ease.InOutSine));
|
||
|
||
// 猛夺一小截:概率与深度都随 strain 提升;夺完手上发麻——
|
||
// 不是连续 shake,而是被向下拽住、短暂停顿,再勉强抬回一点。
|
||
if (UnityEngine.Random.value < 0.4f + strain * 0.25f)
|
||
{
|
||
var yankDur = UnityEngine.Random.Range(0.18f, 0.28f) * speed;
|
||
seq.Append(tr.DOLocalMove(
|
||
BaseOffset((UnityEngine.Random.Range(0.25f, 0.34f) + deepX) * amp,
|
||
(UnityEngine.Random.Range(-0.62f, -0.5f) - deepY) * amp),
|
||
yankDur)
|
||
.SetEase(Ease.OutCubic));
|
||
seq.Join(tr.DOLocalRotate(
|
||
new Vector3(0f, 0f, (UnityEngine.Random.Range(-4.8f, -3.4f) - rotBias) * amp), yankDur)
|
||
.SetEase(Ease.OutCubic));
|
||
seq.AppendInterval(UnityEngine.Random.Range(0.1f, 0.18f) * speed);
|
||
var biteDur = UnityEngine.Random.Range(0.2f, 0.32f) * speed;
|
||
seq.Append(tr.DOLocalMove(
|
||
BaseOffset((UnityEngine.Random.Range(0.19f, 0.27f) + deepX) * amp,
|
||
(UnityEngine.Random.Range(-0.48f, -0.4f) - deepY) * amp),
|
||
biteDur)
|
||
.SetEase(Ease.OutCubic));
|
||
seq.Join(tr.DOLocalRotate(
|
||
new Vector3(0f, 0f, (UnityEngine.Random.Range(-3.4f, -2.2f) - rotBias) * amp), biteDur)
|
||
.SetEase(Ease.OutCubic));
|
||
}
|
||
|
||
// 往回挣(一):咬牙拉回一点
|
||
var recoverDur = UnityEngine.Random.Range(0.28f, 0.42f) * speed;
|
||
seq.Append(tr.DOLocalMove(
|
||
BaseOffset((UnityEngine.Random.Range(0.12f, 0.16f) + deepX) * amp,
|
||
(UnityEngine.Random.Range(-0.32f, -0.26f) - deepY * 0.55f) * amp),
|
||
recoverDur)
|
||
.SetEase(Ease.OutCubic));
|
||
seq.Join(tr.DOLocalRotate(
|
||
new Vector3(0f, 0f, (UnityEngine.Random.Range(-2.3f, -1.3f) - rotBias * 0.7f) * amp), recoverDur)
|
||
.SetEase(Ease.OutCubic));
|
||
// 滑走半截:手上又被抽走一段
|
||
var slipDur = UnityEngine.Random.Range(0.16f, 0.26f) * speed;
|
||
seq.Append(tr.DOLocalMove(
|
||
BaseOffset((UnityEngine.Random.Range(0.2f, 0.27f) + deepX) * amp,
|
||
(UnityEngine.Random.Range(-0.5f, -0.4f) - deepY) * amp),
|
||
slipDur)
|
||
.SetEase(Ease.OutQuad));
|
||
seq.Join(tr.DOLocalRotate(
|
||
new Vector3(0f, 0f, (UnityEngine.Random.Range(-4.1f, -2.8f) - rotBias) * amp), slipDur)
|
||
.SetEase(Ease.OutQuad));
|
||
// 三成回合再缓缓向下沉半口气又咬住,避免碎步太密显得机械。
|
||
if (UnityEngine.Random.value < 0.3f)
|
||
{
|
||
var dipDur = UnityEngine.Random.Range(0.14f, 0.22f) * speed;
|
||
seq.Append(tr.DOLocalMove(
|
||
BaseOffset((UnityEngine.Random.Range(0.21f, 0.28f) + deepX) * amp,
|
||
(UnityEngine.Random.Range(-0.56f, -0.47f) - deepY) * amp),
|
||
dipDur)
|
||
.SetEase(Ease.OutQuad));
|
||
seq.Join(tr.DOLocalRotate(
|
||
new Vector3(0f, 0f, (UnityEngine.Random.Range(-4.4f, -3f) - rotBias) * amp), dipDur)
|
||
.SetEase(Ease.OutQuad));
|
||
var braceDur = UnityEngine.Random.Range(0.18f, 0.28f) * speed;
|
||
seq.Append(tr.DOLocalMove(
|
||
BaseOffset((UnityEngine.Random.Range(0.16f, 0.22f) + deepX) * amp,
|
||
(UnityEngine.Random.Range(-0.4f, -0.33f) - deepY) * amp),
|
||
braceDur)
|
||
.SetEase(Ease.OutCubic));
|
||
seq.Join(tr.DOLocalRotate(
|
||
new Vector3(0f, 0f, (UnityEngine.Random.Range(-3f, -1.8f) - rotBias) * amp), braceDur)
|
||
.SetEase(Ease.OutCubic));
|
||
}
|
||
// 往回挣(二):再咬住,但回不到最初的姿态
|
||
var pullDur = UnityEngine.Random.Range(0.4f, 0.58f) * speed;
|
||
seq.Append(tr.DOLocalMove(
|
||
BaseOffset((UnityEngine.Random.Range(0.13f, 0.18f) + deepX) * amp,
|
||
(UnityEngine.Random.Range(-0.35f, -0.28f) - deepY * 0.75f) * amp),
|
||
pullDur)
|
||
.SetEase(Ease.InOutSine));
|
||
seq.Join(tr.DOLocalRotate(
|
||
new Vector3(0f, 0f, (UnityEngine.Random.Range(-2.8f, -1.6f) - rotBias) * amp), pullDur)
|
||
.SetEase(Ease.InOutSine));
|
||
// 偶尔结尾再沉一下,像手腕被拖住,不做高频震动。
|
||
if (UnityEngine.Random.value < 0.25f)
|
||
{
|
||
var sagDur = UnityEngine.Random.Range(0.2f, 0.32f) * speed;
|
||
seq.Append(tr.DOLocalMove(
|
||
BaseOffset((UnityEngine.Random.Range(0.18f, 0.24f) + deepX) * amp,
|
||
(UnityEngine.Random.Range(-0.46f, -0.38f) - deepY) * amp),
|
||
sagDur)
|
||
.SetEase(Ease.InOutSine));
|
||
seq.Join(tr.DOLocalRotate(
|
||
new Vector3(0f, 0f, (UnityEngine.Random.Range(-3.5f, -2.2f) - rotBias) * amp), sagDur)
|
||
.SetEase(Ease.InOutSine));
|
||
}
|
||
|
||
// strain 爬升更慢,后期不会很快进入“狂抖”
|
||
seq.OnComplete(() => PlayReelHold(tr, Mathf.Min(1f, strain + 0.12f)));
|
||
seq.timeScale = ReelMotionSpeedMultiplier;
|
||
_smallMotionSequence = seq;
|
||
}
|
||
|
||
public void PlayCurrentMotion(string motionProfile)
|
||
{
|
||
if (_spriteRenderer == null || !IsSmallShowing) return;
|
||
PlaySmallMotion(motionProfile);
|
||
}
|
||
|
||
private void ClearSmallImmediate()
|
||
{
|
||
KillSmallTweenState();
|
||
ResetSmallTransform();
|
||
|
||
if (_spriteRenderer != null)
|
||
{
|
||
_spriteRenderer.DOKill();
|
||
_spriteRenderer.SetAlpha(0f);
|
||
_spriteRenderer.gameObject.SetActive(false);
|
||
}
|
||
|
||
_currentSmallPicName = null;
|
||
_currentSmallUsesSplitLayers = false;
|
||
|
||
if (_smallMotionRenderer != null)
|
||
{
|
||
_smallMotionRenderer.DOKill();
|
||
_smallMotionRenderer.SetAlpha(0f);
|
||
_smallMotionRenderer.gameObject.SetActive(false);
|
||
_smallMotionRenderer.maskInteraction = SpriteMaskInteraction.None;
|
||
}
|
||
|
||
if (_smallMotionMask != null)
|
||
{
|
||
_smallMotionMask.enabled = false;
|
||
}
|
||
}
|
||
|
||
private void ClearLargeImmediate()
|
||
{
|
||
StopLargeSpriteAnimation();
|
||
ApplyLargeSpriteBlur(0f, 0f);
|
||
|
||
if (_largeSpriteRenderer != null)
|
||
{
|
||
_largeSpriteRenderer.DOKill();
|
||
_largeSpriteRenderer.SetAlpha(0f);
|
||
_largeSpriteRenderer.gameObject.SetActive(false);
|
||
}
|
||
|
||
_currentLargePicName = null;
|
||
}
|
||
|
||
private void ClearKiteImmediate()
|
||
{
|
||
KillKiteTweens();
|
||
if (_kiteRenderer != null)
|
||
{
|
||
_kiteRenderer.SetAlpha(0f);
|
||
}
|
||
|
||
if (_kiteBaseRenderer != null)
|
||
{
|
||
_kiteBaseRenderer.SetAlpha(0f);
|
||
_kiteBaseRenderer.gameObject.SetActive(false);
|
||
}
|
||
|
||
if (_kiteCameraNode != null)
|
||
{
|
||
_kiteCameraNode.gameObject.SetActive(false);
|
||
}
|
||
|
||
if (_kiteMask != null)
|
||
{
|
||
_kiteMask.enabled = false;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 显示物体(小图)
|
||
/// </summary>
|
||
/// <param name="picName">图片名(无扩展名,与 Yarn 中一致;Addressable Key 由 <see cref="DreamSpritePath"/> 拼接)</param>
|
||
/// <param name="duration">淡入时长</param>
|
||
/// <returns>协程</returns>
|
||
public IEnumerator ShowObj(string picName, float duration = 1)
|
||
{
|
||
// 互斥:如果大图正在显示,先隐藏
|
||
if (IsLargeShowing)
|
||
{
|
||
yield return HideLargeObj(duration);
|
||
}
|
||
|
||
// 互斥:风筝 Rig 正在显示时先隐藏
|
||
if (IsKiteShowing)
|
||
{
|
||
yield return HideKite(Mathf.Min(duration, 0.25f));
|
||
}
|
||
|
||
KillSmallTweenState();
|
||
ResetSmallTransform();
|
||
|
||
if (_smallMotionRenderer != null)
|
||
{
|
||
_smallMotionRenderer.gameObject.SetActive(false);
|
||
_smallMotionRenderer.SetAlpha(0);
|
||
_smallMotionRenderer.maskInteraction = SpriteMaskInteraction.None;
|
||
}
|
||
|
||
if (_smallMotionMask != null)
|
||
{
|
||
_smallMotionMask.enabled = false;
|
||
}
|
||
|
||
_spriteRenderer.gameObject.SetActive(true);
|
||
|
||
var loaded = false;
|
||
yield return LoadDreamSpriteInto(_spriteRenderer, picName, value => loaded = value);
|
||
if (!loaded)
|
||
{
|
||
ClearSmallImmediate();
|
||
yield break;
|
||
}
|
||
|
||
_currentSmallPicName = NormalizeDreamPicName(picName);
|
||
_currentSmallUsesSplitLayers = false;
|
||
|
||
yield return _spriteRenderer.DOFade(1, duration).WaitForCompletion();
|
||
}
|
||
|
||
public IEnumerator ShowObjWithMotion(string picName, string motionProfile, float duration)
|
||
{
|
||
// 互斥:如果大图正在显示,先隐藏
|
||
if (IsLargeShowing)
|
||
{
|
||
yield return HideLargeObj(duration);
|
||
}
|
||
|
||
// 互斥:风筝 Rig 正在显示时先隐藏
|
||
if (IsKiteShowing)
|
||
{
|
||
yield return HideKite(Mathf.Min(duration, 0.25f));
|
||
}
|
||
|
||
var normalizedName = NormalizeDreamPicName(picName);
|
||
var useSplitMotion = TryGetSplitMotionPicNames(picName, out var basePicName, out var motionPicName);
|
||
var isSamePic = string.Equals(_currentSmallPicName, normalizedName, StringComparison.OrdinalIgnoreCase)
|
||
&& _currentSmallUsesSplitLayers == useSplitMotion;
|
||
_smallMotionBaseOffset = IsReelPic(normalizedName) ? new Vector3(0f, -0.32f, 0f) : Vector3.zero;
|
||
|
||
KillSmallTweenState();
|
||
|
||
ResetSmallTransform(resetMotionLayer: !isSamePic);
|
||
_spriteRenderer.gameObject.SetActive(true);
|
||
|
||
if (!isSamePic || _spriteRenderer.sprite == null)
|
||
{
|
||
var baseLoaded = false;
|
||
yield return LoadDreamSpriteInto(
|
||
_spriteRenderer,
|
||
useSplitMotion ? basePicName : picName,
|
||
value => baseLoaded = value);
|
||
if (!baseLoaded)
|
||
{
|
||
ClearSmallImmediate();
|
||
yield break;
|
||
}
|
||
|
||
_currentSmallPicName = normalizedName;
|
||
}
|
||
_spriteRenderer.SetAlpha(1f);
|
||
|
||
if (useSplitMotion)
|
||
{
|
||
EnsureSmallMotionRenderer();
|
||
_smallMotionRenderer.gameObject.SetActive(true);
|
||
CopySmallRendererSettings(_smallMotionRenderer, 1, SpriteMaskInteraction.VisibleInsideMask);
|
||
ConfigureSmallMotionMask();
|
||
ResetSmallTransform(resetMotionLayer: !isSamePic);
|
||
if (!isSamePic || _smallMotionRenderer.sprite == null)
|
||
{
|
||
var motionLoaded = false;
|
||
yield return LoadDreamSpriteInto(
|
||
_smallMotionRenderer,
|
||
motionPicName,
|
||
value => motionLoaded = value);
|
||
if (!motionLoaded)
|
||
{
|
||
ClearSmallImmediate();
|
||
yield break;
|
||
}
|
||
}
|
||
_smallMotionRenderer.SetAlpha(1f);
|
||
}
|
||
else if (_smallMotionRenderer != null)
|
||
{
|
||
_smallMotionRenderer.gameObject.SetActive(false);
|
||
_smallMotionRenderer.SetAlpha(0);
|
||
_smallMotionRenderer.maskInteraction = SpriteMaskInteraction.None;
|
||
|
||
if (_smallMotionMask != null)
|
||
{
|
||
_smallMotionMask.enabled = false;
|
||
}
|
||
}
|
||
|
||
_currentSmallPicName = normalizedName;
|
||
_currentSmallUsesSplitLayers = useSplitMotion;
|
||
PlaySmallMotion(motionProfile);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 显示大图
|
||
/// </summary>
|
||
/// <param name="picName">图片名(无扩展名,与 Yarn 中一致;Addressable Key 由 <see cref="DreamSpritePath"/> 拼接)</param>
|
||
/// <param name="duration">淡入时长</param>
|
||
/// <returns>协程</returns>
|
||
public IEnumerator ShowLargeObj(string picName, float duration = 1)
|
||
{
|
||
StopLargeSpriteAnimation();
|
||
|
||
// 互斥:小图立刻关掉,避免风筝层与底图不同步淡出造成的怪 fade
|
||
if (IsSmallShowing)
|
||
{
|
||
yield return HideObj(0f);
|
||
}
|
||
|
||
if (IsKiteShowing)
|
||
{
|
||
yield return HideKite(0f);
|
||
}
|
||
|
||
if (_largeSpriteRenderer == null) yield break;
|
||
|
||
ResetLargePresentationImmediate();
|
||
_largeSpriteRenderer.gameObject.SetActive(true);
|
||
_largeSpriteRenderer.SetAlpha(0f);
|
||
|
||
var loaded = false;
|
||
yield return LoadDreamSpriteInto(_largeSpriteRenderer, picName, value => loaded = value);
|
||
if (!loaded)
|
||
{
|
||
ClearLargeImmediate();
|
||
yield break;
|
||
}
|
||
|
||
_currentLargePicName = NormalizeDreamPicName(picName);
|
||
if (duration <= 0f)
|
||
{
|
||
_largeSpriteRenderer.SetAlpha(1f);
|
||
}
|
||
else
|
||
{
|
||
yield return _largeSpriteRenderer.DOFade(1, duration).WaitForCompletion();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 按“前缀1.png … 前缀N.png”加载并循环播放全屏序列帧。
|
||
/// </summary>
|
||
public IEnumerator ShowLargeSpriteAnimation(
|
||
string picNamePrefix,
|
||
int frameCount,
|
||
float secondsPerFrame = 0.2f,
|
||
float duration = 1f)
|
||
{
|
||
if (_largeSpriteRenderer == null || frameCount <= 0) yield break;
|
||
|
||
StopLargeSpriteAnimation();
|
||
|
||
if (IsSmallShowing)
|
||
{
|
||
yield return HideObj(0f);
|
||
}
|
||
|
||
if (IsKiteShowing)
|
||
{
|
||
yield return HideKite(0f);
|
||
}
|
||
|
||
var frames = new Sprite[frameCount];
|
||
for (var i = 0; i < frameCount; i++)
|
||
{
|
||
var picName = $"{NormalizeDreamPicName(picNamePrefix)}{i + 1}";
|
||
var key = string.Format(DreamSpritePath, picName);
|
||
var handle = ResourceSystem.LoadAsync<Sprite>(key);
|
||
yield return handle;
|
||
|
||
if (handle.Status != AsyncOperationStatus.Succeeded || handle.Result == null)
|
||
{
|
||
Debug.LogError($"[SpriteShowcase] Failed to load animation frame: {key}");
|
||
ClearLargeImmediate();
|
||
yield break;
|
||
}
|
||
|
||
frames[i] = handle.Result;
|
||
}
|
||
|
||
ResetLargePresentationImmediate();
|
||
_largeSpriteRenderer.gameObject.SetActive(true);
|
||
_largeSpriteRenderer.sprite = frames[0];
|
||
_largeSpriteRenderer.SetAlpha(0f);
|
||
_currentLargePicName = $"{NormalizeDreamPicName(picNamePrefix)}1";
|
||
_largeSpriteAnimationCoroutine = StartCoroutine(
|
||
LoopLargeSpriteAnimation(frames, secondsPerFrame));
|
||
|
||
if (duration <= 0f)
|
||
{
|
||
_largeSpriteRenderer.SetAlpha(1f);
|
||
}
|
||
else
|
||
{
|
||
yield return _largeSpriteRenderer.DOFade(1f, duration).WaitForCompletion();
|
||
}
|
||
}
|
||
|
||
public void SetLargeSpriteImmediate(Sprite sprite)
|
||
{
|
||
if (_largeSpriteRenderer != null && sprite != null)
|
||
_largeSpriteRenderer.sprite = sprite;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 隐藏物体(小图)
|
||
/// </summary>
|
||
/// <param name="duration">淡出时长</param>
|
||
/// <returns>协程</returns>
|
||
public IEnumerator HideObj(float duration = 1)
|
||
{
|
||
yield return HideObj(force: false, duration);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 隐藏物体(小图)
|
||
/// </summary>
|
||
/// <param name="force">是否强制隐藏</param>
|
||
/// <param name="duration">淡出时长</param>
|
||
/// <returns>协程</returns>
|
||
public IEnumerator HideObj(bool force, float duration = 1)
|
||
{
|
||
// 风筝 Rig 视作小图的一种,hide_sprite 时一并隐藏
|
||
if (IsKiteShowing)
|
||
{
|
||
yield return HideKite(duration);
|
||
}
|
||
|
||
// force=false 时检查状态,未显示则直接返回
|
||
if (!force && !IsSmallShowing) yield break;
|
||
|
||
KillSmallTweenState();
|
||
ResetSmallTransform();
|
||
|
||
var motionActive = _smallMotionRenderer != null && _smallMotionRenderer.gameObject.activeSelf;
|
||
if (duration <= 0f)
|
||
{
|
||
_spriteRenderer.SetAlpha(0);
|
||
if (motionActive)
|
||
_smallMotionRenderer.SetAlpha(0);
|
||
}
|
||
else
|
||
{
|
||
var fade = _spriteRenderer.DOFade(0, duration);
|
||
if (motionActive)
|
||
_smallMotionRenderer.DOFade(0, duration);
|
||
yield return fade.WaitForCompletion();
|
||
}
|
||
|
||
_spriteRenderer.gameObject.SetActive(false);
|
||
_currentSmallPicName = null;
|
||
_currentSmallUsesSplitLayers = false;
|
||
|
||
if (_smallMotionRenderer != null)
|
||
{
|
||
_smallMotionRenderer.gameObject.SetActive(false);
|
||
_smallMotionRenderer.SetAlpha(0);
|
||
_smallMotionRenderer.maskInteraction = SpriteMaskInteraction.None;
|
||
}
|
||
|
||
if (_smallMotionMask != null)
|
||
{
|
||
_smallMotionMask.enabled = false;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 隐藏大图
|
||
/// </summary>
|
||
/// <param name="force">是否强制隐藏</param>
|
||
/// <param name="duration">淡出时长</param>
|
||
/// <returns>协程</returns>
|
||
public IEnumerator HideLargeObj(float duration = 1)
|
||
{
|
||
if (_largeSpriteRenderer == null) yield break;
|
||
|
||
StopLargeSpriteAnimation();
|
||
KillLargePresentationTweens();
|
||
if (duration <= 0f)
|
||
{
|
||
_largeSpriteRenderer.SetAlpha(0f);
|
||
}
|
||
else
|
||
{
|
||
yield return _largeSpriteRenderer.DOFade(0, duration).WaitForCompletion();
|
||
}
|
||
|
||
_largeSpriteRenderer.gameObject.SetActive(false);
|
||
_currentLargePicName = null;
|
||
ApplyLargeSpriteBlur(0f, 0f);
|
||
ResetLargePresentationImmediate();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 隐藏所有(小图和大图)
|
||
/// </summary>
|
||
/// <param name="duration">淡出时长</param>
|
||
/// <returns>协程</returns>
|
||
public IEnumerator HideAll(float duration = 1)
|
||
{
|
||
if (IsSmallShowing || IsKiteShowing)
|
||
yield return HideObj(duration);
|
||
|
||
if (IsLargeShowing)
|
||
yield return HideLargeObj(duration);
|
||
}
|
||
|
||
public ShowcaseSnapshotDto CaptureSnapshot()
|
||
{
|
||
if (IsKiteShowing)
|
||
{
|
||
return HiddenShowcaseSnapshot();
|
||
}
|
||
|
||
if (IsSmallShowing && !string.IsNullOrEmpty(_currentSmallPicName))
|
||
{
|
||
return new ShowcaseSnapshotDto
|
||
{
|
||
displayMode = nameof(ShowcaseDisplayMode.Small),
|
||
picName = _currentSmallPicName,
|
||
usesSplitLayers = _currentSmallUsesSplitLayers
|
||
};
|
||
}
|
||
|
||
if (IsLargeShowing && !string.IsNullOrEmpty(_currentLargePicName))
|
||
{
|
||
return new ShowcaseSnapshotDto
|
||
{
|
||
displayMode = nameof(ShowcaseDisplayMode.Large),
|
||
picName = _currentLargePicName
|
||
};
|
||
}
|
||
|
||
return HiddenShowcaseSnapshot();
|
||
}
|
||
|
||
private static ShowcaseSnapshotDto HiddenShowcaseSnapshot()
|
||
{
|
||
return new ShowcaseSnapshotDto
|
||
{
|
||
displayMode = nameof(ShowcaseDisplayMode.None)
|
||
};
|
||
}
|
||
|
||
public IEnumerator RestoreSnapshotAsync(ShowcaseSnapshotDto snapshot, Action<string> warning = null)
|
||
{
|
||
ClearPresentationEffects();
|
||
if (snapshot == null) yield break;
|
||
|
||
if (!Enum.TryParse(snapshot.displayMode, true, out ShowcaseDisplayMode mode))
|
||
{
|
||
warning?.Invoke($"[SpriteShowcase] Unknown display mode: {snapshot.displayMode}");
|
||
yield break;
|
||
}
|
||
|
||
switch (mode)
|
||
{
|
||
case ShowcaseDisplayMode.None:
|
||
yield break;
|
||
|
||
case ShowcaseDisplayMode.Small:
|
||
if (string.IsNullOrWhiteSpace(snapshot.picName))
|
||
{
|
||
warning?.Invoke("[SpriteShowcase] Small snapshot is missing picName.");
|
||
yield break;
|
||
}
|
||
|
||
yield return RestoreSmallImmediate(snapshot.picName, snapshot.usesSplitLayers, warning);
|
||
yield break;
|
||
|
||
case ShowcaseDisplayMode.Large:
|
||
if (string.IsNullOrWhiteSpace(snapshot.picName))
|
||
{
|
||
warning?.Invoke("[SpriteShowcase] Large snapshot is missing picName.");
|
||
yield break;
|
||
}
|
||
|
||
yield return RestoreLargeImmediate(snapshot.picName, warning);
|
||
yield break;
|
||
}
|
||
}
|
||
|
||
private IEnumerator RestoreSmallImmediate(string picName, bool usesSplitLayers, Action<string> warning)
|
||
{
|
||
if (_spriteRenderer == null) yield break;
|
||
|
||
var normalizedName = NormalizeDreamPicName(picName);
|
||
_smallMotionBaseOffset = IsReelPic(normalizedName) ? new Vector3(0f, -0.32f, 0f) : Vector3.zero;
|
||
_spriteRenderer.gameObject.SetActive(true);
|
||
|
||
if (usesSplitLayers)
|
||
{
|
||
if (!TryGetSplitMotionPicNames(picName, out var basePicName, out var motionPicName))
|
||
{
|
||
warning?.Invoke($"[SpriteShowcase] Split-layer snapshot is unsupported: {picName}");
|
||
ClearSmallImmediate();
|
||
yield break;
|
||
}
|
||
|
||
var baseLoaded = false;
|
||
yield return LoadDreamSpriteInto(
|
||
_spriteRenderer,
|
||
basePicName,
|
||
value => baseLoaded = value,
|
||
warning);
|
||
if (!baseLoaded)
|
||
{
|
||
ClearSmallImmediate();
|
||
yield break;
|
||
}
|
||
|
||
EnsureSmallMotionRenderer();
|
||
_smallMotionRenderer.gameObject.SetActive(true);
|
||
CopySmallRendererSettings(_smallMotionRenderer, 1, SpriteMaskInteraction.VisibleInsideMask);
|
||
ConfigureSmallMotionMask();
|
||
ResetSmallTransform();
|
||
|
||
var motionLoaded = false;
|
||
yield return LoadDreamSpriteInto(
|
||
_smallMotionRenderer,
|
||
motionPicName,
|
||
value => motionLoaded = value,
|
||
warning);
|
||
if (!motionLoaded)
|
||
{
|
||
ClearSmallImmediate();
|
||
yield break;
|
||
}
|
||
|
||
_smallMotionRenderer.SetAlpha(1f);
|
||
}
|
||
else
|
||
{
|
||
var loaded = false;
|
||
yield return LoadDreamSpriteInto(
|
||
_spriteRenderer,
|
||
picName,
|
||
value => loaded = value,
|
||
warning);
|
||
if (!loaded)
|
||
{
|
||
ClearSmallImmediate();
|
||
yield break;
|
||
}
|
||
}
|
||
|
||
_spriteRenderer.SetAlpha(1f);
|
||
_currentSmallPicName = normalizedName;
|
||
_currentSmallUsesSplitLayers = usesSplitLayers;
|
||
}
|
||
|
||
private IEnumerator RestoreLargeImmediate(string picName, Action<string> warning)
|
||
{
|
||
if (_largeSpriteRenderer == null) yield break;
|
||
|
||
_largeSpriteRenderer.gameObject.SetActive(true);
|
||
var loaded = false;
|
||
yield return LoadDreamSpriteInto(
|
||
_largeSpriteRenderer,
|
||
picName,
|
||
value => loaded = value,
|
||
warning);
|
||
if (!loaded)
|
||
{
|
||
ClearLargeImmediate();
|
||
yield break;
|
||
}
|
||
|
||
_largeSpriteRenderer.SetAlpha(1f);
|
||
_currentLargePicName = NormalizeDreamPicName(picName);
|
||
}
|
||
|
||
public void ClearPresentationEffects()
|
||
{
|
||
ClearSmallImmediate();
|
||
ClearLargeImmediate();
|
||
ClearKiteImmediate();
|
||
|
||
if (_backgroundSpriteRenderer != null)
|
||
{
|
||
_backgroundSpriteRenderer.DOKill();
|
||
_backgroundSpriteRenderer.SetAlpha(0f);
|
||
_backgroundSpriteRenderer.gameObject.SetActive(false);
|
||
}
|
||
}
|
||
|
||
#region 风筝 Rig
|
||
|
||
// Kite Camera(虚拟镜头完全补偿)→ Rig(高度+漂移)→ Sway(鱼式流体摆动)→ Accent(DOTween 顿挫)
|
||
// 底图 / Mask 保持静态,不进镜头节点。
|
||
private const string KiteSpritePicName = "D2S风筝1_风筝层";
|
||
private const string KiteBasePicName = "D2S风筝1_底图";
|
||
private const float KiteMaxHeight = 13f;
|
||
|
||
[Header("风筝 / 对焦材质")]
|
||
[SerializeField] private Material _kiteFocusMaterial;
|
||
[SerializeField] private KiteWindSettings _kiteWindSettings = new KiteWindSettings();
|
||
[SerializeField] private KiteCameraSettings _kiteCameraSettings = new KiteCameraSettings();
|
||
[SerializeField] private KiteFocusSettings _kiteFocusSettings = new KiteFocusSettings();
|
||
|
||
private Transform _kiteCameraNode;
|
||
private Transform _kiteRootPivot;
|
||
private Transform _kiteSwayPivot;
|
||
private SpriteRenderer _kiteRenderer;
|
||
private SpriteRenderer _kiteBaseRenderer;
|
||
private SpriteMask _kiteMask;
|
||
private KiteRigDriver _kiteDriver;
|
||
private DG.Tweening.Sequence _kiteAccentSequence;
|
||
private bool _kiteSpriteLoaded;
|
||
private bool _kiteBaseSpriteLoaded;
|
||
|
||
private bool IsKiteShowing =>
|
||
_kiteCameraNode != null && _kiteCameraNode.gameObject.activeSelf;
|
||
|
||
private void EnsureKiteRig()
|
||
{
|
||
if (_kiteRootPivot != null || _spriteRenderer == null) return;
|
||
|
||
var parent = _spriteRenderer.transform.parent;
|
||
|
||
var cameraObject = new GameObject("Kite Camera");
|
||
cameraObject.transform.SetParent(parent);
|
||
cameraObject.transform.localPosition = _smallBaseLocalPosition;
|
||
cameraObject.transform.localRotation = Quaternion.identity;
|
||
cameraObject.transform.localScale = Vector3.one;
|
||
|
||
var root = new GameObject("Kite Rig");
|
||
root.transform.SetParent(cameraObject.transform, false);
|
||
root.transform.localPosition = Vector3.zero;
|
||
root.transform.localRotation = _smallBaseLocalRotation;
|
||
root.transform.localScale = _smallBaseLocalScale;
|
||
|
||
var sway = new GameObject("Kite Sway");
|
||
sway.transform.SetParent(root.transform, false);
|
||
|
||
var accent = new GameObject("Kite Accent");
|
||
accent.transform.SetParent(sway.transform, false);
|
||
|
||
// 白色底图:独立静态节点,不随镜头/高度缩放
|
||
var baseObject = new GameObject("Kite Base");
|
||
baseObject.transform.SetParent(parent);
|
||
baseObject.transform.localPosition = _smallBaseLocalPosition;
|
||
baseObject.transform.localRotation = _smallBaseLocalRotation;
|
||
baseObject.transform.localScale = _smallBaseLocalScale;
|
||
|
||
// 遮罩:把风筝层限制在底图(天空)范围内
|
||
var maskObject = new GameObject("Kite Mask");
|
||
maskObject.transform.SetParent(parent);
|
||
maskObject.transform.localPosition = _smallBaseLocalPosition;
|
||
maskObject.transform.localRotation = _smallBaseLocalRotation;
|
||
maskObject.transform.localScale = _smallBaseLocalScale;
|
||
|
||
_kiteCameraNode = cameraObject.transform;
|
||
_kiteRootPivot = root.transform;
|
||
_kiteSwayPivot = sway.transform;
|
||
_kiteRenderer = accent.AddComponent<SpriteRenderer>();
|
||
CopySmallRendererSettings(_kiteRenderer, 1, SpriteMaskInteraction.VisibleInsideMask);
|
||
_kiteBaseRenderer = baseObject.AddComponent<SpriteRenderer>();
|
||
CopySmallRendererSettings(_kiteBaseRenderer, 0, SpriteMaskInteraction.None);
|
||
_kiteMask = maskObject.AddComponent<SpriteMask>();
|
||
_kiteMask.alphaCutoff = 0.01f;
|
||
_kiteMask.isCustomRangeActive = true;
|
||
_kiteMask.enabled = false;
|
||
|
||
EnsureKiteFocusMaterial();
|
||
_kiteDriver = cameraObject.AddComponent<KiteRigDriver>();
|
||
_kiteDriver.Configure(
|
||
_kiteCameraNode,
|
||
_kiteRootPivot,
|
||
_kiteSwayPivot,
|
||
_kiteRenderer,
|
||
_smallBaseLocalPosition,
|
||
_smallBaseLocalRotation,
|
||
_smallBaseLocalScale,
|
||
_kiteWindSettings,
|
||
_kiteCameraSettings,
|
||
_kiteFocusSettings,
|
||
_kiteFocusMaterial);
|
||
_kiteDriver.enabled = false;
|
||
|
||
cameraObject.SetActive(false);
|
||
baseObject.SetActive(false);
|
||
_kiteRenderer.SetAlpha(0);
|
||
_kiteBaseRenderer.SetAlpha(0);
|
||
}
|
||
|
||
private void EnsureKiteFocusMaterial()
|
||
{
|
||
if (_kiteFocusMaterial != null) return;
|
||
|
||
var shader = Shader.Find("AIBIS/KiteFocusSprite");
|
||
if (shader == null)
|
||
{
|
||
Debug.LogWarning("[SpriteShowcase] 找不到 AIBIS/KiteFocusSprite,风筝虚焦不可用。", this);
|
||
return;
|
||
}
|
||
|
||
_kiteFocusMaterial = new Material(shader) { name = "KiteFocusSprite (Runtime)" };
|
||
}
|
||
|
||
private void KillKiteTweens()
|
||
{
|
||
_kiteAccentSequence?.Kill();
|
||
_kiteAccentSequence = null;
|
||
_kiteDriver?.ResetAndSleep();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 显示风筝 Rig。已显示时等价于 <see cref="SetKiteState"/>(幂等)。
|
||
/// </summary>
|
||
public IEnumerator ShowKite(float height, int wind, float duration = 0.35f)
|
||
{
|
||
if (IsLargeShowing)
|
||
{
|
||
yield return HideLargeObj(duration);
|
||
}
|
||
|
||
if (IsSmallShowing)
|
||
{
|
||
yield return HideObj(Mathf.Min(duration, 0.25f));
|
||
}
|
||
|
||
EnsureKiteRig();
|
||
if (_kiteRenderer == null || _kiteDriver == null) yield break;
|
||
|
||
height = Mathf.Clamp(height, 0f, KiteMaxHeight);
|
||
|
||
if (IsKiteShowing)
|
||
{
|
||
SetKiteState(height, wind, duration);
|
||
yield break;
|
||
}
|
||
|
||
KillKiteTweens();
|
||
_kiteCameraNode.gameObject.SetActive(true);
|
||
_kiteBaseRenderer.gameObject.SetActive(true);
|
||
|
||
if (!_kiteSpriteLoaded)
|
||
{
|
||
yield return LoadDreamSpriteInto(_kiteRenderer, KiteSpritePicName);
|
||
_kiteSpriteLoaded = _kiteRenderer.sprite != null;
|
||
// 贴图加载后重新赋 focus 材质(Load 可能换过 material)
|
||
EnsureKiteFocusMaterial();
|
||
if (_kiteFocusMaterial != null)
|
||
{
|
||
_kiteRenderer.material = _kiteFocusMaterial;
|
||
}
|
||
}
|
||
|
||
if (!_kiteBaseSpriteLoaded)
|
||
{
|
||
yield return LoadDreamSpriteInto(_kiteBaseRenderer, KiteBasePicName);
|
||
_kiteBaseSpriteLoaded = _kiteBaseRenderer.sprite != null;
|
||
}
|
||
|
||
if (_kiteBaseSpriteLoaded)
|
||
{
|
||
_kiteMask.sprite = _kiteBaseRenderer.sprite;
|
||
_kiteMask.frontSortingLayerID = _kiteRenderer.sortingLayerID;
|
||
_kiteMask.backSortingLayerID = _kiteRenderer.sortingLayerID;
|
||
_kiteMask.frontSortingOrder = _spriteRenderer.sortingOrder + 2;
|
||
_kiteMask.backSortingOrder = _spriteRenderer.sortingOrder;
|
||
_kiteMask.enabled = true;
|
||
}
|
||
|
||
_kiteDriver.Wake(height, wind);
|
||
_kiteBaseRenderer.SetAlpha(1f);
|
||
_kiteRenderer.SetAlpha(KiteRigDriver.Alpha(height));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 高度 SmoothDamp + 切换风向。乱度粘滞由 driver 保留。
|
||
/// </summary>
|
||
public void SetKiteState(float height, int wind, float duration = 0.9f)
|
||
{
|
||
if (!IsKiteShowing || _kiteDriver == null) return;
|
||
|
||
height = Mathf.Clamp(height, 0f, KiteMaxHeight);
|
||
_kiteDriver.SetHeight(height, duration);
|
||
_kiteDriver.SetWind(wind);
|
||
}
|
||
|
||
/// <summary>仅切换风况(选择前 telegraph 风向)。</summary>
|
||
/// <param name="turbulence">乱度 0~1;负数或省略表示平稳(清零)。设定后粘滞,kite_state 沿用。</param>
|
||
public void SetKiteWind(int wind, float turbulence = -1f)
|
||
{
|
||
if (!IsKiteShowing || _kiteDriver == null) return;
|
||
// 与旧语义对齐:省略/负数 → 平稳(乱度归零);显式传入 ≥0 则设定并粘滞
|
||
float t = turbulence >= 0f ? Mathf.Clamp01(turbulence) : 0f;
|
||
_kiteDriver.SetWind(wind, t);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 一次性顿挫动效,叠加在 Accent 层上,结束回到原位。
|
||
/// 镜头采样合成位姿,会自然追着 accent 跑。
|
||
/// </summary>
|
||
public void PlayKiteAccent(string profile)
|
||
{
|
||
if (!IsKiteShowing) return;
|
||
|
||
_kiteAccentSequence?.Kill();
|
||
_kiteAccentSequence = null;
|
||
|
||
var tr = _kiteRenderer.transform;
|
||
var seq = DOTween.Sequence();
|
||
|
||
switch (NormalizeMotionProfile(profile))
|
||
{
|
||
case "lift":
|
||
seq.Append(tr.DOLocalPath(new[]
|
||
{
|
||
new Vector3(0.03f, 0.07f, 0f),
|
||
new Vector3(0.11f, 0.17f, 0f),
|
||
new Vector3(0.12f, 0.24f, 0f),
|
||
}, 0.75f, PathType.CatmullRom).SetEase(Ease.InOutSine));
|
||
seq.Join(tr.DOLocalRotate(new Vector3(2f, -1.5f, -2f), 0.62f).SetEase(Ease.InOutSine));
|
||
break;
|
||
|
||
case "sink":
|
||
seq.Append(tr.DOLocalPath(new[]
|
||
{
|
||
new Vector3(0.03f, -0.06f, 0f),
|
||
new Vector3(-0.02f, -0.17f, 0f),
|
||
new Vector3(-0.08f, -0.24f, 0f),
|
||
}, 0.82f, PathType.CatmullRom).SetEase(Ease.InOutSine));
|
||
seq.Join(tr.DOLocalRotate(new Vector3(-2f, 1.5f, 2f), 0.68f).SetEase(Ease.InOutSine));
|
||
break;
|
||
|
||
case "taut":
|
||
seq.Append(tr.DOLocalPath(new[]
|
||
{
|
||
new Vector3(0.08f, 0.01f, 0f),
|
||
new Vector3(0.2f, 0.07f, 0f),
|
||
new Vector3(0.26f, 0.05f, 0f),
|
||
}, 0.62f, PathType.CatmullRom).SetEase(Ease.OutSine));
|
||
seq.Join(tr.DOLocalRotate(new Vector3(0f, -2f, -3f), 0.62f).SetEase(Ease.OutSine));
|
||
seq.AppendInterval(0.32f);
|
||
break;
|
||
|
||
case "drop":
|
||
seq.Append(tr.DOLocalPath(new[]
|
||
{
|
||
new Vector3(0.06f, -0.08f, 0f),
|
||
new Vector3(0.08f, -0.24f, 0f),
|
||
new Vector3(0.01f, -0.36f, 0f),
|
||
}, 0.92f, PathType.CatmullRom).SetEase(Ease.InSine));
|
||
seq.Join(tr.DOLocalRotate(new Vector3(-2.5f, 1f, 3f), 0.86f).SetEase(Ease.InOutSine));
|
||
break;
|
||
|
||
case "wrench":
|
||
seq.Append(tr.DOLocalPath(new[]
|
||
{
|
||
new Vector3(-0.06f, 0.03f, 0f),
|
||
new Vector3(0.3f, 0.18f, 0f),
|
||
new Vector3(0.64f, 0.12f, 0f),
|
||
new Vector3(0.78f, 0.24f, 0f),
|
||
}, 1.15f, PathType.CatmullRom).SetEase(Ease.InOutSine));
|
||
seq.Join(tr.DOLocalRotate(new Vector3(1.5f, -3f, -6f), 0.92f).SetEase(Ease.InOutSine));
|
||
seq.AppendInterval(0.42f);
|
||
break;
|
||
|
||
case "flutter":
|
||
{
|
||
float side = UnityEngine.Random.value < 0.5f ? -1f : 1f;
|
||
seq.Append(tr.DOLocalPath(new[]
|
||
{
|
||
new Vector3(0.05f * side, 0.04f, 0f),
|
||
new Vector3(0.16f * side, 0.11f, 0f),
|
||
new Vector3(0.24f * side, 0.07f, 0f),
|
||
}, 1.05f, PathType.CatmullRom).SetEase(Ease.InOutSine));
|
||
seq.Join(tr.DOLocalRotate(new Vector3(1.5f, -2.5f * side, -3f * side), 0.9f)
|
||
.SetEase(Ease.InOutSine));
|
||
seq.AppendInterval(0.22f);
|
||
break;
|
||
}
|
||
|
||
default:
|
||
seq.Kill();
|
||
return;
|
||
}
|
||
|
||
seq.Append(tr.DOLocalMove(Vector3.zero, 0.82f).SetEase(Ease.InOutSine));
|
||
seq.Join(tr.DOLocalRotate(Vector3.zero, 0.82f).SetEase(Ease.InOutSine));
|
||
_kiteAccentSequence = seq;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 隐藏风筝 Rig。与显示一致不做淡出,直接消失。
|
||
/// </summary>
|
||
public IEnumerator HideKite(float duration = 1)
|
||
{
|
||
if (!IsKiteShowing) yield break;
|
||
|
||
KillKiteTweens();
|
||
|
||
_kiteRenderer.SetAlpha(0);
|
||
_kiteBaseRenderer.SetAlpha(0);
|
||
|
||
_kiteCameraNode.gameObject.SetActive(false);
|
||
_kiteBaseRenderer.gameObject.SetActive(false);
|
||
if (_kiteMask != null)
|
||
{
|
||
_kiteMask.enabled = false;
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
/// <summary>
|
||
/// 淡入(异步协程版本)
|
||
/// </summary>
|
||
/// <param name="duration">淡入时长</param>
|
||
/// <returns>协程</returns>
|
||
public IEnumerator FadeInAsync(float duration = 1)
|
||
{
|
||
if (_backgroundSpriteRenderer == null) yield break;
|
||
|
||
_backgroundSpriteRenderer.gameObject.SetActive(true);
|
||
yield return _backgroundSpriteRenderer.DOFade(1, duration).WaitForCompletion();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 淡出(异步协程版本)
|
||
/// </summary>
|
||
/// <param name="duration">淡出时长</param>
|
||
/// <returns>协程</returns>
|
||
public IEnumerator FadeOutAsync(float duration = 1)
|
||
{
|
||
if (_backgroundSpriteRenderer == null) yield break;
|
||
|
||
_backgroundSpriteRenderer.gameObject.SetActive(true);
|
||
yield return _backgroundSpriteRenderer.DOFade(0, duration).WaitForCompletion();
|
||
_backgroundSpriteRenderer.gameObject.SetActive(false);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 淡入(同步版本,启动协程)
|
||
/// </summary>
|
||
/// <param name="duration">淡入时长</param>
|
||
public void FadeInSync(float duration = 1)
|
||
{
|
||
StartCoroutine(FadeInAsync(duration));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 淡出(同步版本,启动协程)
|
||
/// </summary>
|
||
/// <param name="duration">淡出时长</param>
|
||
public void FadeOutSync(float duration = 1)
|
||
{
|
||
StartCoroutine(FadeOutAsync(duration));
|
||
}
|
||
}
|
||
}
|