RankUpAnimator.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  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. // --- Animation Step 1: 快速循环滚动阶段 ---
  80. animationSequence.AppendCallback(() =>
  81. {
  82. StartCoroutine(ScrollAndStop(targetLocalPosition));
  83. });
  84. // --- Animation Step 2: 自身向上冲动画 ---
  85. float totalMoveDuration = scrollDuration - hangTime;
  86. float fastUpDuration = totalMoveDuration * 0.2f; // 前段快速线性
  87. float floatUpDuration = totalMoveDuration * 0.8f; // 后段缓慢升空
  88. Vector3 startPos = targetItem.localPosition;
  89. Vector3 endPos = new Vector3(startPos.x, targetLocalPosition.y, startPos.z);
  90. Vector3 midPos = Vector3.Lerp(startPos, endPos, 0.4f); // 中间位置点(快速上升到一半)
  91. // 快速线性上升阶段
  92. animationSequence.Append(DOTween.To(() => 0f, t =>
  93. {
  94. float progress = t / fastUpDuration;
  95. targetItem.localPosition = Vector3.Lerp(startPos, midPos, progress);
  96. }, fastUpDuration, fastUpDuration).SetEase(Ease.Linear));
  97. // 缓慢升空 + 缩放曲线阶段
  98. animationSequence.Append(DOTween.To(() => 0f, t =>
  99. {
  100. float progress = t / floatUpDuration;
  101. float easedT = moveEase != Ease.Unset ? DOVirtual.EasedValue(0, 1, progress, moveEase) : progress;
  102. targetItem.localPosition = Vector3.Lerp(midPos, endPos, easedT);
  103. //但要确保 t + fastUpDuration <= scrollDuration,否则 Evaluate 超出曲线长度可能导致异常值
  104. float curveTime = Mathf.Clamp(t + fastUpDuration, 0f, scrollDuration);
  105. float scaleFactor = scaleCurve.Evaluate(t + fastUpDuration); // 补正时间轴
  106. targetItem.localScale = originalScale * scaleFactor;
  107. }, floatUpDuration, floatUpDuration));
  108. // --- Animation Step 3: 滞空阶段 ---
  109. // 滞空 -> 落地缓冲效果
  110. animationSequence.AppendInterval(hangTime); // 滞空 x 秒
  111. // 落地阶段(快速砸下来)
  112. animationSequence.Append(targetItem.DOLocalMove(targetLocalPosition, 0.25f)
  113. .SetEase(Ease.InQuad)); // 快速落地
  114. // 缩放恢复阶段
  115. // 能避免 DOTween 残留的内部插值。
  116. animationSequence.Join(DOTween.To(() => targetItem.localScale, s => targetItem.localScale = s, originalScale, 0.25f)
  117. .SetEase(Ease.OutQuad));
  118. // --- Animation Step 4: 滚动到目标名次 ---
  119. // AnimateRankChange(
  120. // targetItem.gameObject,
  121. // fromRank,
  122. // toRank,
  123. // scrollDuration
  124. // );
  125. // Step 4. 动画完成,归位
  126. animationSequence.OnComplete(() =>
  127. {
  128. //targetItem.gameObject.GetComponent<Outline>().enabled = false; // 关闭轮廓
  129. ResetItem();
  130. isAnimating = false;
  131. onAnimationComplete?.Invoke();
  132. });
  133. }
  134. /**
  135. * 无限滚动逻辑
  136. * 1. 先让Content向上滚动
  137. * 2. 当Content滚动到一定高度时,重置Content位置
  138. * 3. 等待一段时间后,停止滚动
  139. */
  140. IEnumerator ScrollAndStop(Vector3 targetPos)
  141. {
  142. float elapsed = 0f;
  143. while (elapsed < scrollDuration)
  144. {
  145. content.anchoredPosition += Vector2.up * scrollSpeed * Time.deltaTime;
  146. // 无限滚动逻辑
  147. if (content.anchoredPosition.y >= contentResetThreshold)
  148. {
  149. content.anchoredPosition -= Vector2.up * contentResetThreshold;
  150. }
  151. elapsed += Time.deltaTime;
  152. yield return null;
  153. }
  154. yield return new WaitForSeconds(stopDelay);
  155. }
  156. private Vector3 CalculateTargetLocalPosition(int targetRank)
  157. {
  158. float itemHeight = targetItem.rect.height;
  159. VerticalLayoutGroup layoutGroup = content.GetComponent<VerticalLayoutGroup>();
  160. float spacing = layoutGroup != null ? layoutGroup.spacing : 0f;
  161. float topPadding = layoutGroup != null ? layoutGroup.padding.top : 0f;
  162. float totalHeightPerItem = itemHeight + spacing;
  163. float contentY = -(targetRank - 1) * totalHeightPerItem - topPadding;
  164. float viewportHeight = viewport.rect.height;
  165. float contentHeight = content.rect.height;
  166. float offsetY = 0f; // 你可以自定义偏移
  167. float viewportY = contentY + contentHeight - viewportHeight + offsetY;
  168. return new Vector3(targetItem.localPosition.x, viewportY, 0f);
  169. }
  170. private GameObject targetClone;
  171. private void ResetItem()
  172. {
  173. if (targetClone != null)
  174. {
  175. Destroy(targetClone); // 删除动画用副本
  176. targetClone = null;
  177. }
  178. if (targetItem != null)
  179. {
  180. // 把item归回原来的Content下
  181. targetItem.SetParent(content, false);
  182. // 移动到正确的位置
  183. targetItem.SetSiblingIndex(targetRankIndex - 1);
  184. // 恢复初始缩放
  185. targetItem.localScale = originalScale;
  186. // 保证在Layout下重新排列
  187. LayoutRebuilder.ForceRebuildLayoutImmediate(content.GetComponent<RectTransform>());
  188. }
  189. }
  190. public void StopAnimation()
  191. {
  192. if (animationSequence != null && animationSequence.IsPlaying())
  193. {
  194. animationSequence.Kill();
  195. ResetItem();
  196. isAnimating = false;
  197. }
  198. }
  199. /// <summary>
  200. /// 平滑地将排名数字从旧值变更为新值
  201. /// </summary>
  202. /// <param name="targetGO">包含 Text 或 TMP_Text 的 GameObject</param>
  203. /// <param name="fromRank">起始名次(通常是旧排名 +1)</param>
  204. /// <param name="toRank">目标名次(通常是当前排名 +1)</param>
  205. /// <param name="duration">变化时间</param>
  206. /// <param name="ease">插值曲线,默认线性</param>
  207. public void AnimateRankChange(GameObject targetGO, int fromRank, int toRank, float duration, Ease ease = Ease.Linear)
  208. {
  209. var tmp = targetGO.GetComponentInChildren<TMP_Text>();
  210. var text = targetGO.GetComponentInChildren<Text>();
  211. if (tmp == null && text == null)
  212. {
  213. Debug.LogWarning("未找到 TMP_Text 或 Text 组件!");
  214. return;
  215. }
  216. DOTween.To(() => fromRank, value =>
  217. {
  218. if (tmp != null)
  219. tmp.text = value.ToString();
  220. else if (text != null)
  221. text.text = value.ToString();
  222. }, toRank, duration).SetEase(ease);
  223. }
  224. }
  225. }