Efficient ways of handling bezier UI object movements on mobile?

Hi all -

I’m sure you’ve all seen this kind of effect in mobile games:

  • User does something (kills a monster, collects a treasure, opens a chest box)
  • User gets loot (coins, crystals, diamonds, etc)
  • The image of the loot “flies” towards its corresponding UI label (e.g., the gold coin flies towards the UI element showing the user’s current coin balance)
  • Upon reaching the label, the coin disappears

Usually, this movement is accomplished not in a straight line but along a Bezier curve, with easing and what not involved.

Now, I’ve been able to accomplish this using DOTween’s DoPath() method - but I’ve been told that it’s not and quite inefficient. Profiler data seems to agree - triggering such an animation “costs” 1.4kb of garbage right off the bat, plus an additional 0.5ms of processing per frame.

I will test this on my mobile later today to see if the performance impact of this is significant (assume that such flying animations are triggered by the user twice a second). I was, however, wondering if the above is as good as it gets or if there’s a better and more standard way that other mobile developers do this?

Your thoughts would be appreciated. For those so inclined, and in the spirit of sharing, here is the code (I suspect I might be recreating the Tween with each instance - might be worth investigating):

using UnityEngine;
using DG.Tweening;

public class MoveToCoin : MonoBehaviour
{

    public float duration;

    UI_InteractiveLabel target;
    Vector3 deviationPosition;
    Tween tween;

    private void OnEnable()
    {
        if (target == null) { target = UI_Controller.Instance.topLabelCoin.GetComponent<UI_InteractiveLabel>(); }
        deviationPosition = new Vector3(transform.position.x + Random.Range(-1, 1), transform.position.y + Random.Range(0.1f, 2f));

            
            transform.DOPath(new Vector3[] { deviationPosition, UI_Controller.Instance.topLabelCoin.position }, duration, PathType.CatmullRom, PathMode.Ignore, 5, null)
            .OnStart(() => { transform.DOPunchScale(new Vector3(0.75f, 0.75f, 0), duration, 0, 0); transform.DORotate(new Vector3(0, 0, 360), 1f, RotateMode.FastBeyond360); })
            .OnComplete(() => { target.Bounce(); gameObject.SetActive(false); })
            .SetEase(Ease.InOutCirc)
            .SetAutoKill(false);
    }

    private void OnDisable()
    {
        transform.rotation = new Quaternion(0, 0, 0, 0);
        transform.localScale = new Vector3(0.5f, 0.5f, 1);
    }
}

I haven’t used Dotween. You could try leantween to see if you get better results. Just as another testing option.

1 Like

When in doubt, use an animation curve. Lerp towards your target position with an animation curve acting as an offset for the height to create a curved path. I made an example for another poster here . No garbage created if you do it yourself.

2 Likes

Bezier compuations are pretty expensive. What do I do is I am replacing bezier curve with a rough polyline (connected straight lines). Then I adjust rotation of moving object by Mathf.LerpAngle between precomputed waypoints angles. Visual difference between this and moving along actual bezier curve is unnoticeable and performance is many times higher.

1 Like

Just to let you know that I’m looking into your solution - I haven’t quite managed to make the curve work (even out of the box, it just moves in a straight line - but I’ll get there).

Anyway, the good news is that, compared to my original solution, your generates only 4.4kb of garbage when I trigger the movement of 44 sprites simultaneously! In contrast, using DOTween generated 67kb of garbage.

Thanks ever so much for suggesting this!

Whipped up a quick example for you (label got a little small at 1080p):

public class CoinTween : MonoBehaviour
{
   public RectTransform target;
   public AnimationCurve animationCurve;

   private RectTransform rectTransform;
   private Vector3 start;
   private Vector3 end;

   private Coroutine coroutine;      

   private void Start()
   {
       rectTransform = GetComponent<RectTransform>();
       start = rectTransform.position;
       end = target.position + new Vector3(target.rect.width / 2f, target.rect.height / 2f, 0f);
   }

   private void Update()
   {
       if(coroutine == null && Input.GetKeyDown(KeyCode.Space) == true)
       {
           coroutine = StartCoroutine(MoveToTarget());
       }
   }

   private IEnumerator MoveToTarget()
   {
       float duration = 0.60f;
       float time = 0f;

       while(time <= duration)
       {
           time += Time.deltaTime;

           float linearT = time / duration;
           float heightT = animationCurve.Evaluate(linearT);

           float height = Mathf.Lerp(0f, 600.0f, heightT); // you'll want the height based on screen size not just a flat 600

           rectTransform.position = Vector3.Lerp(start, end, linearT) + new Vector3(0f, height, 0f);

           yield return null;
       }

       coroutine = null;
   }
}

The curve I used:

1 Like

Woohoo! It works magnificently! Thank you so much!

1 Like

@GroZZleR - you’re a genius - I’m able to have 60+ objects with zero garbage or performance overhead:

using System.Collections;
using UnityEngine;

public class MoveToCoin : MonoBehaviour
{
    public AnimationCurve animationCurve;
    public Transform destination;

    private Coroutine coroutine;
    private Vector3 start;
    private float duration;

    private void OnEnable()
    {
        start = transform.position;
        duration = Random.Range(0.6f, 0.9f);
        transform.localScale = Vector3.one * Random.Range(0.7f, 1.2f);
        if (coroutine == null) { coroutine = StartCoroutine(Move()); }
        animationCurve.AddKey(new Keyframe(0.5f, Random.Range(-1f, 1f)));
    }

    IEnumerator Move()
    {
        float time = 0f;

        Vector2 end = destination.position;

        while (time < duration)
        {

            time += Time.deltaTime;
            float normalizedTimeOnCurve = time / duration;
            float yValueOfCurve = animationCurve.Evaluate(normalizedTimeOnCurve);

            transform.position = Vector2.Lerp(start, end, normalizedTimeOnCurve) + new Vector2(yValueOfCurve, 0f);
            yield return null;
        }

        coroutine = null;
        gameObject.SetActive(false);

    }

    private void OnDisable()
    {
        transform.position = Vector2.zero;
        animationCurve.RemoveKey(1);
    }
}
1 Like