How to "snap" the player into a position to perform an action

Hello. In my third person player project, my character need to perform interactions with some objects in the environment. What i have so far it’s working. I can reach a certain distance from the object and start the interaction, but i can’t properly “snap” my character into a “null object” that hold the position where i have to move in order to perform the action, right in front of the object.

If i do a very rude:

public void MoveToPosition (Transform targetPos)
{
      transform.position = targetPos.position;
}

it work always. The character snaps right into place every time but it’s very brutal and very ugly to watch. But if i try something more appropriate like this:

public void moveTowardsPoint(Vector3 targetPos, Vector3 rotationDir, float moveSpeed, float rotationSpeed)
    {
        animator.SetBool("walking", true);
        transform.position = Vector3.MoveTowards(transform.position, targetPos, moveSpeed * Time.deltaTime);
        transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(rotationDir), Time.deltaTime * rotationSpeed);
    }

it never reach the exact center of the target position (the null object). It always stop much before, so the position is never the same, and i need to be in that exact point to perfom the animation correctly.

Here is a practical example of what i need.

How do i solve this? I tried some ThirdPerson Character Controllers on the asset store that does something like this, but i don't want to customize a pre-made solution. I want to learn how to properly do stuff.

Somebody help?

Don’t forget to disable any user input or operation that changes the transform.positionand transform.rotation values while the coroutine is running.

private IEnumerator _snapToCoroutine;

// Call this method, it makes sure you don't create multiple coroutines that fight for the current transform.position, only one instance of the coroutine runs
public void SnapTo(Vector3 snapPosition, Quaternion snapRotation, float duration) {
    if(_snapToCoroutine != null) StopCoroutine(_snapToCoroutine);
    _snapToCoroutine = SnapToCoroutine(snapPosition, snapRotation, duration);
    StartCoroutine(_snapToCoroutine);
}

// Stops the running snap coroutine, only if a snap coroutine is running
public void CancelSnap() {
    if(_snapToCoroutine != null) StopCoroutine(_snapToCoroutine);
}

private IEnumerator SnapToCoroutine(Vector3 snapPosition, Quaternion snapRotation, float duration) {
    // Store the start time and position, we require these for the linear interpolation
    float startTime = Time.time;
    Vector3 startPosition = transform.position;
    Quaternion startRotation = transform.rotation;

    // While the duration is not reached, move the position towards the snap position, increasing the value in equal steps for the given duration, every frame
    while(Time.time - startTime < duration) {
        // Gives us a value starting from 0 and ending in 1 as time progresses towards our duration, we will feed this 0-1 value into our linear interpolation
        float t = (Time.time - startTime) / duration;

        // This method performs faster than manually assigning transform.position and transform.rotation
        transform.SetPositionAndRotation(Vector3.Lerp(startPosition, snapPosition, t), Quaternion.Lerp(startRotation, snapRotation, t));

        yield return null;
    }

    // And finally snap to the desired position to eliminate any tiny floating point differences due to timing above
    transform.SetPositionAndRotation(snapPosition, snapRotation);
}

Hey @SarperS now it works also with the Rotation.

public void SnapTo(Vector3 snapPosition, Quaternion snapRotation, float duration)
	{
		if (_snapToCoroutine != null) StopCoroutine(_snapToCoroutine);
		_snapToCoroutine = SnapToCoroutine(snapPosition, snapRotation, duration);
		StartCoroutine(_snapToCoroutine);
	}

	public void StopSnapTo()
    {
		StopCoroutine("SnapToCoroutine");
    }

	private IEnumerator SnapToCoroutine(Vector3 snapPosition, Quaternion snapRotation, float duration)
	{
		// Store the start time and position, we require these for the linear interpolation
		float startTime = Time.time;
		Vector3 startPosition = transform.position;
		Quaternion startRotation = transform.rotation;

		// While the duration is not reached, move the position towards the snap position, increasing the value in equal steps for the given duration, every frame
		while (Time.time - startTime < duration)
		{
			transform.position = Vector3.Lerp(startPosition, snapPosition, Time.time - startTime / duration);
			transform.rotation = Quaternion.Lerp(startRotation, snapRotation, Time.time - startTime / duration);
			yield return null;
		}

		// And finally snap to the desired position to eliminate any tiny floating point differences due to timing above
		transform.position = snapPosition;
		transform.rotation = snapRotation;
	}

The only thing i notice is that if try to increase the speed of the snap (at 1.0f is very slow), it snaps immediately like before, like the Lerp isn’t working. Even if try small increments like 1.1f or something like it, it snaps immediately. Maybe because of the nature of Time.time? Should i try a different calculation in timing?

I’ve found another solution that i think it’s even more “elegant”. It requires the DOTween library but i think it worth it. Same method as before with the coroutine but with DOTween you can give that “ease” curve to the movement that makes it a bit more nicer to look at.

using DG.Tweening;

public Ease ease; // Choose an ease curve from the inspector
private IEnumerator _moveToPosCoroutine;

// Call this method first, it makes sure you don't create multiple coroutines that fight for the current transform.position, only one instance of the coroutine runs
    public void MoveToPosition(Vector3 snapPosition, Quaternion snapRotation, float duration)
    {
        if (_moveToPosCoroutine != null) StopCoroutine(_moveToPosCoroutine);
        _moveToPosCoroutine = MoveToPositionCoroutine(pos01.transform.position, pos01.transform.rotation, duration);
        StartCoroutine(_moveToPosCoroutine);
    }

// Stops the running snap coroutine, only if a snap coroutine is running
    public void CancelMoveToPos()
    {
        if (_moveToPosCoroutine != null) StopCoroutine(_moveToPosCoroutine);
    }

private IEnumerator MoveToPositionCoroutine(Vector3 snapPosition, Quaternion snapRotation, float duration)
    {
        // Store the start time and position, we require these for the linear interpolation
        float startTime = Time.time;

        // While the duration is not reached, move the position towards the snap position, increasing the value in equal steps for the given duration, every frame
        while (Time.time - startTime < duration)
        {
            // Gives us a value starting from 0 and ending in 1 as time progresses towards our duration, we will feed this 0-1 value into our linear interpolation
            float t = (Time.time - startTime) / duration;

            // Transform position and rotation with DOTween library with ease curve.
            transform.DOMove(snapPosition, 0.05f, false).SetEase(ease);
            transform.DORotateQuaternion(snapRotation, 0.05f);

            yield return new WaitForSeconds(t);
        }
    }