0
\$\begingroup\$

I want a function, which lets me follow a vector with a bit offset or delay, like it's done in the TrailRenderer. I tried to achieve this with the Lerp function, but that didn't work for me like I want, since I had to wait for a specific time until the lerp was done and then updated my targetVector of the Lerp again, which led to a bad result.

Now a possible solution to my problem would be a List, which holds all Positions of my Vector from the past 50 frames or so and I assign the same values to my other Vector with a specific offset. But I was wondering if there is some built in method in Unity, which could achieve this for me.

Edit: This is about moving a Canvas

\$\endgroup\$
  • \$\begingroup\$ With the limited information about what you want to do I would also use a List. But depending on why you need this, there might be a better solution. \$\endgroup\$ – Philipp Apr 20 '16 at 12:15
  • \$\begingroup\$ Okay the idea is to make my canvas a bit slower than my head is moving with my AR-Glasses, so that the UI is "swimming" a bit. So I take the actual position of the device and then trail this position with like 1 second delay. \$\endgroup\$ – TobiasW Apr 20 '16 at 12:18
  • \$\begingroup\$ Is it just rotational information, or are you getting actual positional data from the glasses (e.g. GPS)? If it's just rotational, you could possibly constantly SLerp between the canvas orientation and the head orientation each frame (much like the common follow camera script does with Lerp). \$\endgroup\$ – XNargaHuntress Apr 20 '16 at 14:27
4
\$\begingroup\$

What you describe, in terms of following a position's exact path with a time delay, might be both more complicated and less satisfying than what you want.

Here's a mock-up of two possibilities:

Animated example of two following modes

The object labeled "Path" follows the exact path of the leader object, a constant amount of time behind it. This means:

  • When the leader starts moving, there is a delay before the follower moves. Likewise for stopping.

  • Any kinks or jitter in the leader's movement is repeated exactly by the follower.

The object labeled "Blend" mixes-together its current position with the leader's each frame, so that it attracts toward it asymptotically.

  • The follower always moves directly toward the leader, cutting corners tighter and smoothing out jitter to a degree.

  • The follower moves slower when it's close to the leader, and faster when far away, making its follow distance/delay non-constant (but for some applications this gives a pleasant ease-in and ease-out to its motion effectively "for free")

The great thing about the blend approach is how simple the code is:

public class BlendFollower : MonoBehaviour {

    public Transform leader;
    public float followSharpness = 0.05f;

    void LateUpdate () {
        transform.position += (leader.position - transform.position) * followSharpness;
    }
}

I use this about 80% of the time when I want some quantity to track toward another in games - it works for floats, positions, colours, rotations, etc. It's a super useful little trick.


Edit: for rotation, you'd use something like this:

transform.rotation = Quaternion.Lerp(
                          transform.rotation, 
                          leader.rotation, 
                          followSharpness);

Just ensure the Quaternions you start with are valid (not all zeroes or NaNs) or this will blow up. I often write a Quaternion.IsValid() extension method to check this if I'm working with a lot of computed rotations.


Note that since this is blending a constant amount per frame, so at higher framerates it will be sharper and at lower framerates, spongier. For purely visual effects this is often tolerable, but if gameplay outcomes depend on the follow rate then you'll either want to correct for this or move it to FixedUpdate so it's consistent.

Compare this to the path follow:

(Variable framerates account for some of this complexity - if you want to follow a constant number of frames behind, rather than seconds, then the code is a bit simpler. Also, I threw in my path-drawing debug gizmo code too.)

public class PathFollower : MonoBehaviour {

    const int MAX_FPS = 60;

    public Transform leader;
    public float lagSeconds =  0.5f;

    Vector3[] _positionBuffer;
    float[] _timeBuffer;
    int _oldestIndex;
    int _newestIndex;

    // Use this for initialization
    void Start () {
        int bufferLength = Mathf.CeilToInt(lagSeconds * MAX_FPS);
        _positionBuffer = new Vector3[bufferLength];
        _timeBuffer = new float[bufferLength];

        _positionBuffer[0] = _positionBuffer[1] = leader.position;
        _timeBuffer[0] = _timeBuffer[1] = Time.time;

        _oldestIndex = 0;
        _newestIndex = 1;
    }


    void LateUpdate () {
        // Insert newest position into our cache.
        // If the cache is full, overwrite the latest sample.
        int newIndex = (_newestIndex + 1) % _positionBuffer.Length;
        if (newIndex != _oldestIndex)
            _newestIndex = newIndex;

        _positionBuffer[_newestIndex] = leader.position;
        _timeBuffer[_newestIndex] = Time.time;

        // Skip ahead in the buffer to the segment containing our target time.
        float targetTime = Time.time - lagSeconds;
        int nextIndex;
        while (_timeBuffer[nextIndex = (_oldestIndex + 1) % _timeBuffer.Length] < targetTime)
            _oldestIndex = nextIndex;

        // Interpolate between the two samples on either side of our target time.
        float span = _timeBuffer[nextIndex] - _timeBuffer[_oldestIndex];
        float progress = 0f;
        if(span > 0f)
        {
            progress = (targetTime - _timeBuffer[_oldestIndex]) / span;
        }

        transform.position = Vector3.Lerp(_positionBuffer[_oldestIndex], _positionBuffer[nextIndex], progress);
    }

    void OnDrawGizmos()
    {
        if (_positionBuffer == null || _positionBuffer.Length == 0)
            return;

        Gizmos.color = Color.grey;

        Vector3 oldPosition = _positionBuffer[_oldestIndex];
        int next;
        for(int i = _oldestIndex; i != _newestIndex; i = next)
        {
            next = (i + 1) % _positionBuffer.Length;
            Vector3 newPosition = _positionBuffer[next];
            Gizmos.DrawLine(oldPosition, newPosition);
            oldPosition = newPosition;
        }
    }
}
\$\endgroup\$
  • \$\begingroup\$ This is an absolutely great answer! Exactly what I wanted and even more. If you have suggestions to improve my question, I will change it so that more people can find your answer :) \$\endgroup\$ – TobiasW Apr 21 '16 at 6:23
  • \$\begingroup\$ Would the same be possible to achieve with 2 rotations? I mean the BlendFollow :) \$\endgroup\$ – TobiasW Apr 22 '16 at 7:06
  • \$\begingroup\$ Quoting the above, "it works for floats, positions, colours, rotations, etc. It's a super useful little trick." although the syntax will be a little different. I'll pop it into an edit. \$\endgroup\$ – DMGregory Apr 22 '16 at 11:14
  • \$\begingroup\$ This script above is great for storing the positions of the leader. But now i want to do this exactly the same with rotations. The lerp example above is obviously not doing the same as with the positions. I have almost zero programming knowledge, so i hope someone can come up with something. Thanks! \$\endgroup\$ – Mathijs Brussaard Jul 25 at 10:07
  • \$\begingroup\$ "the lerp example above is obviously not doing the same as with the positions" is wrong. It is mathematically equivalent. new = lerp(current, target, sharpness), when we peek under the hood, reads new = (1 - sharpness) * current + sharpness * target (neglecting quaternion renormalization for the moment) which we can rearrange to new = current + (target - current) * sharpness which is exactly what we used for positions. Please actually try writing and testing the code before you claim an answer is "obviously" not what you want. \$\endgroup\$ – DMGregory Jul 25 at 12:48
1
\$\begingroup\$

Unity does not store this information, so there is no property or field that holds the old position of objects. Once a frame passes, the old positions of objects are gone.

You can get around this by storing the last few positions yourself and interpolating between them. Here's some code:

using UnityEngine;

public class Tracker : MonoBehaviour
{
    public int trailIntervalsCount = 5;
    public float trailIntervalDuration = .5f;
    Vector3[] pastPositions = new Vector3[0];
    int writeIndex = 0;
    float timer;

    public Vector3 GetPastPosition(float secondsAgo)
    {
        secondsAgo = Mathf.Min((trailIntervalsCount-1) * trailIntervalDuration, secondsAgo);
        float intervalsAgo = ((secondsAgo - timer) / trailIntervalDuration) + 1;
        Vector3 posAtFloor = getPositionByInterval(Mathf.FloorToInt(intervalsAgo));
        Vector3 posAtCeil = getPositionByInterval(Mathf.CeilToInt(intervalsAgo));
        float t = intervalsAgo - Mathf.Floor(intervalsAgo);
        return Vector3.Lerp(posAtFloor, posAtCeil, t);
    }

    void Start()
    {
        // Initialize the past positions buffer to our current position.
        FillPastPositions();
    }

    void FixedUpdate()
    {
        if (trailIntervalsCount != pastPositions.Length)
        {
            FillPastPositions();
        }
        timer += Time.fixedDeltaTime;
        if (timer > trailIntervalDuration && trailIntervalDuration > 0 && trailIntervalsCount > 0)
        {
            timer -= trailIntervalDuration;
            pastPositions[writeIndex++] = transform.position;
            writeIndex %= trailIntervalsCount;
        }
    }

    /// <summary>
    /// Fills the pastPositions buffer with the current position.
    /// </summary>
    private void FillPastPositions()
    {
        pastPositions = new Vector3[trailIntervalsCount];
        for (int i = 0; i < trailIntervalsCount; ++i)
        {
            pastPositions[i] = transform.position;
        }
        writeIndex = 0;
    }

    private Vector3 getPositionByInterval(int intervalsAgo)
    {
        // Interval 0 is our current position. BUT we have to offset it to where we "will be" at the end of the interval.
        if (intervalsAgo <= 0)
        {
            Vector3 currentPos = transform.position;
            Vector3 lastPos = getPositionByInterval(1);
            float percentOfInterval = (timer / trailIntervalDuration);
            if (percentOfInterval == 0)
                return currentPos;
            else
                return Vector3.LerpUnclamped(lastPos, currentPos, (1f / percentOfInterval));
        }
        // Interval 1 is our last written position, 2 is the one before that, etc.
        else if(intervalsAgo <= trailIntervalsCount)
        {
            int index = writeIndex - intervalsAgo;
            // Wrap around once
            if (index < 0)
            {
                index += trailIntervalsCount;
            }
            // If we would wrap around twice, clamp it to the end of the trail.
            if (index < 0)
            {
                index = writeIndex;
            }

            if (index < 0 || index >= pastPositions.Length)
            {
                Debug.LogFormat("{0}, {1}, {2}", index, writeIndex, intervalsAgo);
            }
            return pastPositions[index];
        }
        else
        {
            return getPositionByInterval(trailIntervalsCount);
        }
    }
}
\$\endgroup\$
0
\$\begingroup\$

Assuming you don't care about changes in direction you can multiply the velocity vector of the object by a negative amount, then add that negative vector to the position of the object you're trailing. I think it would be something like this:

var positionBehind = rigidbody.velocity * -1.0f;
var trailPosition = rigidbody.position + positionBehind;
\$\endgroup\$
  • \$\begingroup\$ Sorry, I want to move my canvas like this, and my canvas has no velocity \$\endgroup\$ – TobiasW Apr 20 '16 at 13:15
  • 1
    \$\begingroup\$ Oh I see. That makes this much more interesting. I haven't used the Canvas before, but I think you'd be able to do this by offsetting the canvas by the angular velocity (and I supposed regular velocity) of the camera. But I'd be surprised if the camera uses velocity to update its' position. I think I'd need to do some reading before I can help more. \$\endgroup\$ – Dar Brett Apr 20 '16 at 13:32
  • \$\begingroup\$ Thanks for your effort, but I guess there is no need since DMGregory made such a good answer :) \$\endgroup\$ – TobiasW Apr 21 '16 at 6:49

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.