0
\$\begingroup\$

I'm trying to achieve the effect of a dog sled for a small prototype. All my attempts failed until now.

I have the following code (inspired from this)

using UnityEngine;

public class BlendFollower : MonoBehaviour
{

    const int MAX_FPS = 30;

    public Transform leader;
    public float lagSeconds = 0.5f;

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

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

        _positionBuffer[0] = _positionBuffer[1] = leader.position;
        _rotationBuffer[0] = _rotationBuffer[1] = leader.transform.eulerAngles;
        _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;
        _rotationBuffer[_newestIndex] = leader.transform.eulerAngles;
        _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);
        transform.rotation = Quaternion.Lerp(Quaternion.Euler(_positionBuffer[_oldestIndex]), Quaternion.Euler(_positionBuffer[nextIndex]), progress);
    }    
}

It's "okayish" for the follow; but at some point it will catch the leader; as for a 'sled dog' I don't want the followers to catch the leader but keep the same distance/time delay behind the leader.

I've tried every variation but still stuck with it. Is this even the best approach? How would you do it?

Thanks!

\$\endgroup\$
4
  • \$\begingroup\$ Importantly, a dog sled does not follow the position and rotation of the dogs with a delay. It gets pulled when the dogs try to move further way from it than the length of the leads. That suggests a much simpler bit of vector math you can do to update the sled, with no position/rotation buffer needed. \$\endgroup\$
    – DMGregory
    Commented Dec 4, 2021 at 13:08
  • \$\begingroup\$ Interesting but I can't get my head around it; for me it 'has' to follow some kind of leader no ? Like a 'snake' but with rotation. \$\endgroup\$ Commented Dec 4, 2021 at 13:19
  • \$\begingroup\$ Right, but it does not do so by "remembering" where the leader was some amount of time before. It does it by getting pulled by a tensioned rope toward where the leader is now \$\endgroup\$
    – DMGregory
    Commented Dec 4, 2021 at 13:22
  • \$\begingroup\$ Like just checking the distance between leader and dog and if greater than X Lerp to leader's position ? \$\endgroup\$ Commented Dec 4, 2021 at 13:25

1 Answer 1

1
\$\begingroup\$

Here's a simple way to model an object towed from one end by a rope that can coil up shorter but never stretch longer than a given length.

enter image description here

public class SledFollow : MonoBehaviour {

    [SerializeField]
    [Tooltip("Object that pulls the sled.")]
    Transform _leader;

    [SerializeField]
    [Tooltip("Position on the sled's local z+ axis where its tow rope is attached")]
    float _attachmentDistance = 1;

    [SerializeField]
    [Tooltip("How far from the attachment point can the leader go before pulling?")]
    float _ropeLength = 1;


    Vector3 _attachmentPosition;

    void Start() {
        Vector3 localAttachment = new Vector3(0, 0, _attachmentDistance);
        _attachmentPosition = transform.TransformPoint(localAttachment);        
    }

    void LateUpdate() {

        // When the leader moves further from the attachment point
        // than the length of the rope, move the attachment point toward
        // the leader until the rope is perfectly taut, not over-stretched.
        Vector3 displacement = _attachmentPosition - _leader.position;
        Vector3 rope = Vector3.ClampMagnitude(displacement, _ropeLength);
        _attachmentPosition = _leader.position + rope;

        // If you never scale the sled, you can skip this line.
        float leverLength = _attachmentDistance * transform.localScale.z;

        // When the attachment point moves, move this object as though
        // connected to it by a rigid lever that can't lengthen OR contract.
        displacement = transform.position - _attachmentPosition;
        Vector3 lever = displacement.normalized * leverLength;
        transform.position = _attachmentPosition + lever;

        // Rotate so the attachment point is along our forward vector.
        transform.rotation = Quaternion.LookRotation(-displacement);
    }
}

You may need to make some adjustments if the sled and leader have different heights or go over uneven terrain (such as keeping the sled's up vector aligned with the plane normal, using this function instead of a raw LookRotation), but this gets you the basics.

\$\endgroup\$
1
  • \$\begingroup\$ Gosh, I think I got a brain damage. That was even simpler than expected ! Thank you so much, you saved me some hairs ! \$\endgroup\$ Commented Dec 4, 2021 at 14:12

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .