需求描述:实现一个获取金币动画,要求金币先在领取点四散爆开,然后沿着平滑曲线飞到目标点。

理清思路

需求

对策

金币先在领取点四散爆开

1、生成一定数量的图标对象

2、在以领取点为中心的指定半径的圆中随机一个目标点,使用Dotween的DOMove方法实现四散爆开的效果

从领取点到目标点生成一条平滑曲线路径

使用二阶贝塞尔曲线生成路径节点的数组

实现图标曲线移动

使用Dotween的DOPath实现曲线移动

生成图标对象数量较多时,可能影响性能

使用对象池进行优化

提高曲线路径随机性,优化显示效果

在每次生成路径前,随机更新贝塞尔路径参考点bezierControlPoint

 代码实现

using DG.Tweening;
using UnityEngine;
using UnityEngine.Pool;
using UnityEngine.UI;
using Random = UnityEngine.Random;

//资源获取时,飞入动画逻辑
public class FlyResManager : MonoBehaviour
{
    [SerializeField] private GameObject m_goMainRoot;
    [SerializeField] private GameObject m_goPoolRoot;//对象池节点
    [SerializeField] private GameObject m_goResObj;//图标预制体模板
    [SerializeField] private GameObject m_goStart;//领取点
    [SerializeField] private GameObject m_goEnd;//目标点
    
    //resolution为int类型,表示要取得路径点数量,值越大,取得的路径点越多,曲线最后越平滑
    private int resolution = 16;
    private float offset = 5;
    private int maxCount = 15; //最大显示图标数
    private int initCount = 30; //对象池初始化的数量
    
    private Transform mainTrans;
    private Transform poolTrans;

    private ObjectPool<GameObject> iconPool;
    private Vector2 bezierControlPoint;
    
    private void Start()
    {
        mainTrans = m_goMainRoot.transform;
        poolTrans = m_goPoolRoot.transform;
        InitPool();
    }

    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            SingleFly(1,10);
        }
    }
    
    //测试用
    public void SingleFly(int id, int count)
    {
        var startRect = m_goStart.GetComponent<RectTransform>();
        var endRect = m_goEnd.GetComponent<RectTransform>();
        // startRect.anchoredPosition = startPos;
        // endRect.anchoredPosition = endPos;
        BezierMove(id, count, m_goStart.transform.position, m_goEnd.transform.position);
    }
    
    /// <summary>
    /// 一种资源飞向目标
    /// </summary>
    /// <param name="id">资源id</param>
    /// <param name="count">资源数量</param>
    /// <param name="startPos">飞行开始位置</param>
    /// <param name="endPos">飞行结束位置</param>
    public void SingleFly(int id, int count, Vector2 startPos, Vector2 endPos)
    {
        var startRect = m_goStart.GetComponent<RectTransform>();
        var endRect = m_goEnd.GetComponent<RectTransform>();
        startRect.anchoredPosition = startPos;
        endRect.anchoredPosition = endPos;
        BezierMove(id, count, m_goStart.transform.position, m_goEnd.transform.position);
    }

    //贝塞尔曲线移动
    private void BezierMove(int id, int count, Vector3 startPos, Vector3 endPos)
    {
        CalculateBezierControlPoint(startPos, endPos);
        var showCount = count >= maxCount ? maxCount : count;
        var playerArray = new GameObject[showCount];
        var startPoint = startPos;
     
        var sequence = DOTween.Sequence();
        sequence.AppendCallback(() =>
        {
            for (var i = 0; i < showCount; i++)
            {
                var obj = iconPool.Get();
                var randomPos = Random.insideUnitCircle*20;
                var pos = new Vector3(startPoint.x + randomPos.x, startPoint.y + randomPos.y, startPoint.z);
                var trans = obj.transform;
                trans.position = startPoint;
                var img = obj.GetComponent<Image>();
                //此处处理图标更改加载逻辑
                trans.DOMove(pos, 0.4f).SetEase(Ease.OutCirc).SetDelay(i * 0.02f);
                playerArray[i] = obj;
            }
        });
        sequence.AppendInterval(showCount * 0.05f);
        sequence.AppendCallback(() =>
        {
            for (var i = 0; i < showCount; i++)
            {
                var trans = playerArray[i].transform;
                var delayTime = i * 0.05f;
                var path = GetBezierPath(trans.position, endPos);
                trans.DOPath(path, 0.8f)
                    .SetEase(Ease.InCubic)
                    .SetOptions(false, AxisConstraint.Z) // 锁定Z轴,强制在XY平面运动
                    .SetDelay(delayTime).OnComplete(() =>
                {
                    
                    iconPool.Release(trans.gameObject);
                });
            }
        });
    }

    //随机更新贝塞尔曲线参考点
    private void CalculateBezierControlPoint(Vector3 startPos, Vector3 endPos)
    {
        //随机生成[1,4)
        var value = Random.Range(1, 4);
        if (value == 1)
        {
            bezierControlPoint = (startPos + endPos) * 0.5f;
        }
        else if (value == 2)
        {
            bezierControlPoint = new Vector2(endPos.x, startPos.y);
        }
        else if (value == 3)
        {
            bezierControlPoint = new Vector2(startPos.x, endPos.y);
        }
    }

    /// <summary>
    /// 二阶贝塞尔曲线
    /// </summary>
    /// <param name="start">起点</param>
    /// <param name="center">控制点</param>
    /// <param name="end">终点</param>
    /// <param name="t">[0,1]</param>
    /// <returns></returns>
    private Vector3 GetBezierPoint(Vector3 start, Vector3 center, Vector3 end, float t)
    {
        var p0p1 = (1 - t) * start + t * center;
        var p1p2 = (1 - t) * center + t * end;
        return (1 - t) * p0p1 + t * p1p2;
    }

    /// <summary>
    /// 获取贝塞尔曲线路径点
    /// </summary>
    /// <returns></returns>
    private Vector3[] GetBezierPath(Vector3 startPos, Vector3 endPos)
    {
        var path = new Vector3[resolution];
        for (var i = 0; i < resolution; i++)
        {
            var t = (i + 1) / (float)resolution; //归化到0~1范围
            //使用贝塞尔曲线的公式取得t时的路径点
            var data = GetBezierPoint(startPos, bezierControlPoint, endPos, t);
            path[i] = data;
        }
        return path;
    }
    
    #region 对象池逻辑

    //初始化对象池
    private void InitPool()
    {
        iconPool = new ObjectPool<GameObject>(() =>
            {
                var obj = Instantiate(m_goResObj, poolTrans);
                obj.SetActive(false);
                obj.transform.localPosition = Vector3.zero;
                return obj;
            },
            (go) =>
            {
                go.SetActive(true);
                var trans = go.transform;
                trans.DOKill();
                trans.SetParent(mainTrans);
                trans.localPosition = Vector3.zero;
            },
            (go) =>
            {
                go.SetActive(false);
                var trans = go.transform;
                trans.SetParent(poolTrans);
                trans.localPosition = Vector3.zero;
            },
            (go) =>
            {
                Destroy(go);
            }, true, 20);
    }

    #endregion
}

踩坑提醒

由于本实现方案主要使用Dotween,在使用tween动画数量过多时,可能会报错和警告,或者导致动画停止。可能是Dotween默认有固定容量的序列和补间池,操作超过了Dotween的默认最大容量。

解决方案

1. 增加Dotween容量

在项目启动时(如Awake或Start方法中)增加容量:

using DG.Tweening;

void Awake()
{
    // 设置最大补间数量
    DOTween.SetTweensCapacity(2000, 100); // 第一个参数是最大补间数,第二个是最大序列数
}

2. 正确管理动画

在对象销毁时终止相关动画

void OnDestroy()
{
    transform.DOKill(); // 终止该transform上的所有Dotween动画
}
Logo

一起探索未来云端世界的核心,云原生技术专区带您领略创新、高效和可扩展的云计算解决方案,引领您在数字化时代的成功之路。

更多推荐