- Home /
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.
https://www.youtube.com/watch?v=8OZsM5n4B2I
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?
Answer by SarperS · Sep 30, 2020 at 05:30 PM
Don't forget to disable any user input or operation that changes the transform.position
and 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);
}
Thank you very much! I'm going to try it, and from the comments, that's explain a lot why my position was always insufficient to reach the right center of the null.
You are welcome. Oh by the way, it seems I forgot to interpolate the rotation, the idea is the same though. Just store the starting rotation and then Quaternion.Lerp it towards the snapRotation (add this as another parameter) .
Thank you! It works as intended! That's exactly what i needed!
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);
}
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 eli$$anonymous$$ate any tiny floating point differences due to ti$$anonymous$$g 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. $$anonymous$$aybe because of the nature of Time.time? Should i try a different calculation in ti$$anonymous$$g?
Heh, this is what happens when you post not compiled and not tested code :) The problem was very simple, I forgot to add parentheses to the below line so division was happening before the required subtraction.
Time.time - startTime / duration
should have been (Time.time - startTime) / duration
Anyways, I've updated the answer and cleaned up your code a little bit, and added new comments. Hope it helps improve your understanding. Take care.
Thank you sir! This is exactly what i needed, and i learned something! Thank you!
Answer by andreadoria · Oct 04, 2020 at 10:01 AM
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);
}
}
You don't need DOTween for that. You can achieve the same very easily like below:
First declare a public AnimationCurve you can play with in the inspector
public AnimationCurve SnapCurve;
Then change the line where we calculate the t
as such so that it uses the curve while calculating it's value.
float t = SnapCurve.Evaluate((Time.time - startTime) / duration);
That's it, now you can apply any kind of easing or funky effect you can think of.