I am making a 2D platformer game where you need to find and collect allies throughout the levels. When you collect and ally, it starts following you through the level. After a lot of brainstorming, I managed to create this time-based (hopefully) script that makes the ally follow a trail behind you. My problem is that it doesn’t feel very smooth and the ally gets “shaky”/glitchy sometimes. How can I improve the following code?
using UnityEngine;
using System.Collections.Generic;
using System.Collections;
public class FollowPlayer : MonoBehaviour
{
[SerializeField] private GameObject player;
[SerializeField] private float speed = 6f;
private List<Vector3> storedPositions;
[SerializeField] private float timeToSetPosition = 0.1f, distance = 4f;
private float countdown;
private bool shouldMove;
void Awake()
{
storedPositions = new List<Vector3>(); //create a blank list
countdown = timeToSetPosition;
}
void LateUpdate()
{
Vector3 playerPos = player.transform.position;
if (storedPositions.Count == 0) //check if the list is empty
{
storedPositions.Add(playerPos); //store the players currect position
StartCoroutine(MoveToPosition());
Debug.Log("Started the first coroutine!");
return;
}
countdown -= Time.deltaTime;
if (countdown <= 0 && storedPositions[storedPositions.Count - 1] != playerPos) //add a pos if the player has moved and the countdown is ready
{
countdown = timeToSetPosition;
storedPositions.Add(playerPos);
Debug.Log("Set a position!");
}
if(transform.position == storedPositions[0] && storedPositions.Count > 1) //remove a pos when the ally has reached it
{
storedPositions.RemoveAt(0);
Debug.Log("Deleted a position!");
shouldMove = true;
}
if(transform.position.y != playerPos.y && shouldMove) //move the aly anyway so that he doesn't get left behind when jumping over gaps
{
StartCoroutine(MoveToPosition());
Debug.Log("Started a coroutine because of the y difference!");
shouldMove = false;
}
else if (storedPositions.Count > 1 && shouldMove && Vector3.Distance(transform.position, playerPos) > distance)
{
StartCoroutine(MoveToPosition());
Debug.Log("Started a regular coroutine!");
shouldMove = false;
}
}
private IEnumerator MoveToPosition()
{
float t = 0;
Vector3 currentPos = transform.position;
while (t < 1)
{
t += Time.deltaTime * speed;
transform.position = Vector3.Lerp(currentPos, storedPositions[0], t);
yield return null;
}
}
}
Interesting approach. I like it. It’s nice and simple. You can smooth it out a bit by having a master speed control that slews up from 0 to the NPC normal walk speed, and does not return to zero until the NPC is caught up to the player.
ALSO, the Vector3.Lerp() function isn’t quite what you want in this case: V3.Lerp will give you a traverse that for a given speed covers the distance between any two points in the same amount of time. This will cause wild variations in the NPC motion depending on the spacing of the sampled player positions.
What you really want is probably more of a “turn towards” (using a float heading and a .LerpAngle() call with a maximum turn rate), then a .MoveTowards in that given direction but at the exact desired speed. And again you can slew the speed up and down smoothly as the NPC starts and finishes.
One approach I like to use for feeding player position points into such a system is not to feed the player’s actual location in, but rather feed a point a few units to the right of wherever the player is facing. This will tend to bring the NPC “abeam” the player.
Also, if the new position you calculate is close enough to where the NPC already is, it might be nice to just have the NPC not do anything, as in, “I’m close enough for now, I’ll wait until you move further.”
Interesting ideas. I’ll try them out when I get home! One question, though. What do you mean exactly by “master speed”? Could you give a more detailed example?
This approach will always be more or less shaky. Even if you will use MoveTowards method as suggested by Kurt-Dekker, the last step before reaching target point will produce imperfect movement, and this will happen almost every frame. I gave you the only (probably) correct solution in your other thread .
Remember, for the smooth movement, the follower MUST move every frame by its speed multiplied by Time.deltaTime.
Good catch @Fido789 I was looking at the code and wasn’t sure whether to post this or not but you beat me to it, Time.deltaTime is absolutely necessary for smooth movement
Fido, I didn’t try to neglect your suggestions, but truth is I have no idea how to even start implementing your idea through code xD. I am still a novice when it comes to Unity/C# scripting, so your idea, while seeming efficient, is useless to me, since I don’t know how to translate it into code
Movement animations on a float with smooth transitions from idle to walk to run on the same values as your player. Use Mathf.Smoothstep or something similar to fade off the start and stop. So its not so much like a trained soldier and more like your buddy or puppy following you around. Use some low random values on your target position calculation so like the earlier person suggested you will have not so rigid of a patern, itll be a bit more random and lifelike.
Time.deltaTime has nothing to do with telling the time if you really don’t know that, what it does is smooth out movement frame by frame whereas if you just leave the code as is if there’s a differentiation in the way your computer is rendering frames the engine will pick that up without it. At least, that’s how I understand it.
Best Answer Answer by tanoshimi · Aug 18, 2016 at 07:31 PM
If you want framerate-independent kinematic movement (i.e. you’re moving the object, via Translate() or explicitly setting the value of transform.position) then multiplying by Time.deltaTime is essential.
But in this example, you’re not moving the object; you’re setting the rigidbody’s velocity and letting the Physx engine determine how far the object should move in each frame accordingly. It doesn’t make any difference if you set a velocity of 20 once a second or 50 times a second - it’s still going to be 20 in every single frame when the physics calculation is performed, and that’s why you’ve not noticed a difference in this case.