RankUpAnimator.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  1. using UnityEngine;
  2. using UnityEngine.UI;
  3. using DG.Tweening;
  4. using System.Collections;
  5. using TMPro;
  6. namespace LocalRank
  7. {
  8. public class RankItemData
  9. {
  10. public int Rank;
  11. public string UserName;
  12. public int Score;
  13. public string AvatarUrl;
  14. public bool IsSelf;
  15. public int RankIndex; // 用于存储当前Item的索引
  16. public override string ToString()
  17. {
  18. return $"Rank: {Rank}, UserName: {UserName}, Score: {Score}, AvatarUrl: {AvatarUrl}, IsSelf: {IsSelf}, RankIndex: {RankIndex}";
  19. }
  20. }
  21. public class RankUpAnimator : MonoBehaviour
  22. {
  23. public ScrollRect scrollRect; // 排行榜 ScrollRect
  24. public RectTransform content; // 排行榜内容区域 (Item们的父节点)
  25. public RectTransform viewport; // ScrollRect的可视区域
  26. public RectTransform viewportParent; // targetItem的父节点
  27. public RectTransform targetItem; // 要冲榜的Item
  28. public int targetRankIndex; // 目标名次(比如第3名)
  29. [Header("滚动速度")]
  30. public float scrollSpeed = 2000f;
  31. [Header("滚动时间")]
  32. public float scrollDuration = 1.5f;
  33. [Header("冲榜上升缩放曲线 (时间轴: 0 ~ scrollDuration)")]
  34. public AnimationCurve scaleCurve = AnimationCurve.EaseInOut(0, 1.2f, 1.5f, 1.5f);
  35. [Header("冲榜结束时候滞空时间")]
  36. public float hangTime = 0.5f; // 滞空时间
  37. [Header("位移动画 Ease 类型")]
  38. public Ease moveEase = Ease.Linear;
  39. [Header("重置频率")]
  40. public float contentResetThreshold = 100f;
  41. public float stopDelay = 0.5f;
  42. private Transform originalParent; // 保存初始Parent
  43. private Vector3 originalPosition; // 保存初始局部位置
  44. private Vector3 originalScale; // 保存初始缩放
  45. private Sequence animationSequence; // DOTween动画序列
  46. private bool isAnimating = false; // 动画状态
  47. public System.Action onAnimationStart; // 动画开始回调
  48. public System.Action onAnimationComplete; // 动画结束回调
  49. public void StartRankUpAnimation(int fromRank, int toRank)
  50. {
  51. if (isAnimating) return;
  52. isAnimating = true;
  53. // 触发动画开始回调
  54. onAnimationStart?.Invoke();
  55. // 保存原始状态
  56. originalParent = targetItem.parent;
  57. originalPosition = targetItem.localPosition;
  58. //用于计算缩放曲线的初始值
  59. // 这里的 scaleCurve.Evaluate(0f) 是为了获取曲线在 t=0 时的值,通常是 1.0f
  60. // 设置统一缩放起始值(来自曲线)
  61. float startScale = scaleCurve.Evaluate(0f);
  62. originalScale = targetItem.localScale = Vector3.one * startScale;
  63. // 使用clone对象来避免原始对象被修改
  64. targetClone = Instantiate(targetItem.gameObject, targetItem.parent, true);
  65. targetClone.name = targetItem.name + "_Clone";
  66. targetClone.transform.localScale = originalScale;
  67. targetClone.GetComponent<CanvasGroup>().blocksRaycasts = false;
  68. targetClone.transform.SetSiblingIndex(targetItem.GetSiblingIndex()); // 确保在原始Item上面
  69. RankItemUI rankItemUI = targetClone.GetComponent<RankItemUI>();
  70. rankItemUI.SetOtherSprite(); // 设置为其他人的图标
  71. // Step 1. 脱离LayoutGroup控制,移动到Viewport上
  72. targetItem.SetParent(viewportParent, true); // true保持世界位置不变
  73. //targetItem.gameObject.GetComponent<Outline>().enabled = true; // 激活轮廓
  74. // Step 2. 准备目标位置(计算目标Item应该停在哪)
  75. Vector3 targetLocalPosition = CalculateTargetLocalPosition(targetRankIndex);
  76. // Step 3. 开始上升动画 + 底部滚动动画
  77. animationSequence = DOTween.Sequence();
  78. animationSequence.SetTarget(targetItem);
  79. animationSequence.SetUpdate(true); //整个序列使用 UnscaledTime
  80. // --- Animation Step 1: 快速循环滚动阶段 ---
  81. animationSequence.AppendCallback(() =>
  82. {
  83. StartCoroutine(ScrollAndStop(targetLocalPosition));
  84. });
  85. // --- Animation Step 2: 自身向上冲动画 ---
  86. float totalMoveDuration = scrollDuration - hangTime;
  87. float fastUpDuration = totalMoveDuration * 0.2f; // 前段快速线性
  88. float floatUpDuration = totalMoveDuration * 0.8f; // 后段缓慢升空
  89. Vector3 startPos = targetItem.localPosition;
  90. Vector3 endPos = new Vector3(startPos.x, targetLocalPosition.y, startPos.z);
  91. Vector3 midPos = Vector3.Lerp(startPos, endPos, 0.4f); // 中间位置点(快速上升到一半)
  92. // 快速线性上升阶段
  93. animationSequence.Append(DOTween.To(() => 0f, t =>
  94. {
  95. float progress = t / fastUpDuration;
  96. targetItem.localPosition = Vector3.Lerp(startPos, midPos, progress);
  97. }, fastUpDuration, fastUpDuration).SetEase(Ease.Linear));
  98. // 缓慢升空 + 缩放曲线阶段
  99. animationSequence.Append(DOTween.To(() => 0f, t =>
  100. {
  101. float progress = t / floatUpDuration;
  102. float easedT = moveEase != Ease.Unset ? DOVirtual.EasedValue(0, 1, progress, moveEase) : progress;
  103. targetItem.localPosition = Vector3.Lerp(midPos, endPos, easedT);
  104. //但要确保 t + fastUpDuration <= scrollDuration,否则 Evaluate 超出曲线长度可能导致异常值
  105. float curveTime = Mathf.Clamp(t + fastUpDuration, 0f, scrollDuration);
  106. float scaleFactor = scaleCurve.Evaluate(t + fastUpDuration); // 补正时间轴
  107. targetItem.localScale = originalScale * scaleFactor;
  108. }, floatUpDuration, floatUpDuration));
  109. // --- Animation Step 3: 滞空阶段 ---
  110. // 滞空 -> 落地缓冲效果
  111. animationSequence.AppendInterval(hangTime); // 滞空 x 秒
  112. // 落地阶段(快速砸下来)
  113. animationSequence.Append(targetItem.DOLocalMove(targetLocalPosition, 0.25f)
  114. .SetEase(Ease.InQuad)); // 快速落地
  115. // 缩放恢复阶段
  116. // 能避免 DOTween 残留的内部插值。
  117. animationSequence.Join(DOTween.To(() => targetItem.localScale, s => targetItem.localScale = s, originalScale, 0.25f)
  118. .SetEase(Ease.OutQuad));
  119. // --- Animation Step 4: 滚动到目标名次 ---
  120. // AnimateRankChange(
  121. // targetItem.gameObject,
  122. // fromRank,
  123. // toRank,
  124. // scrollDuration
  125. // );
  126. // Step 4. 动画完成,归位
  127. animationSequence.OnComplete(() =>
  128. {
  129. //targetItem.gameObject.GetComponent<Outline>().enabled = false; // 关闭轮廓
  130. ResetItem();
  131. isAnimating = false;
  132. onAnimationComplete?.Invoke();
  133. });
  134. }
  135. /**
  136. * 无限滚动逻辑
  137. * 1. 先让Content向上滚动
  138. * 2. 当Content滚动到一定高度时,重置Content位置
  139. * 3. 等待一段时间后,停止滚动
  140. */
  141. IEnumerator ScrollAndStop(Vector3 targetPos)
  142. {
  143. float elapsed = 0f;
  144. while (elapsed < scrollDuration)
  145. {
  146. content.anchoredPosition += Vector2.up * scrollSpeed * Time.unscaledDeltaTime;//* Time.deltaTime;
  147. // 无限滚动逻辑
  148. if (content.anchoredPosition.y >= contentResetThreshold)
  149. {
  150. content.anchoredPosition -= Vector2.up * contentResetThreshold;
  151. }
  152. elapsed += Time.unscaledDeltaTime;//Time.deltaTime;
  153. yield return null;
  154. }
  155. yield return new WaitForSecondsRealtime(stopDelay);
  156. }
  157. private Vector3 CalculateTargetLocalPosition(int targetRank)
  158. {
  159. float itemHeight = targetItem.rect.height;
  160. VerticalLayoutGroup layoutGroup = content.GetComponent<VerticalLayoutGroup>();
  161. float spacing = layoutGroup != null ? layoutGroup.spacing : 0f;
  162. float topPadding = layoutGroup != null ? layoutGroup.padding.top : 0f;
  163. float totalHeightPerItem = itemHeight + spacing;
  164. float contentY = -(targetRank - 1) * totalHeightPerItem - topPadding;
  165. float viewportHeight = viewport.rect.height;
  166. float contentHeight = content.rect.height;
  167. float offsetY = 0f; // 你可以自定义偏移
  168. float viewportY = contentY + contentHeight - viewportHeight + offsetY;
  169. return new Vector3(targetItem.localPosition.x, viewportY, 0f);
  170. }
  171. private GameObject targetClone;
  172. private void ResetItem()
  173. {
  174. if (targetClone != null)
  175. {
  176. Destroy(targetClone); // 删除动画用副本
  177. targetClone = null;
  178. }
  179. if (targetItem != null)
  180. {
  181. // 把item归回原来的Content下
  182. targetItem.SetParent(content, false);
  183. // 移动到正确的位置
  184. targetItem.SetSiblingIndex(targetRankIndex - 1);
  185. // 恢复初始缩放
  186. targetItem.localScale = originalScale;
  187. // 保证在Layout下重新排列
  188. LayoutRebuilder.ForceRebuildLayoutImmediate(content.GetComponent<RectTransform>());
  189. }
  190. }
  191. public void StopAnimation()
  192. {
  193. if (animationSequence != null && animationSequence.IsPlaying())
  194. {
  195. animationSequence.Kill();
  196. ResetItem();
  197. isAnimating = false;
  198. }
  199. }
  200. /// <summary>
  201. /// 平滑地将排名数字从旧值变更为新值
  202. /// </summary>
  203. /// <param name="targetGO">包含 Text 或 TMP_Text 的 GameObject</param>
  204. /// <param name="fromRank">起始名次(通常是旧排名 +1)</param>
  205. /// <param name="toRank">目标名次(通常是当前排名 +1)</param>
  206. /// <param name="duration">变化时间</param>
  207. /// <param name="ease">插值曲线,默认线性</param>
  208. public void AnimateRankChange(GameObject targetGO, int fromRank, int toRank, float duration, Ease ease = Ease.Linear)
  209. {
  210. var tmp = targetGO.GetComponentInChildren<TMP_Text>();
  211. var text = targetGO.GetComponentInChildren<Text>();
  212. if (tmp == null && text == null)
  213. {
  214. Debug.LogWarning("未找到 TMP_Text 或 Text 组件!");
  215. return;
  216. }
  217. DOTween.To(() => fromRank, value =>
  218. {
  219. if (tmp != null)
  220. tmp.text = value.ToString();
  221. else if (text != null)
  222. text.text = value.ToString();
  223. }, toRank, duration).SetEase(ease);
  224. }
  225. }
  226. }