Moving Multiple Objects With Vector2.MoveTowards Not Consistent

Good Afternoon,

I'm trying to move three objects from their current position to a shared target position based on speed. Once they reach the target position, they move to a shared respawn location and then start their journey over again.

The objects move to the target position and restart their journey successfully. However, the problem is that the distance between each object starts to gradually change. Eventually, the spacing/gap between objects becomes bigger or smaller. I'm honestly not certain if this an issue caused by framerate changing, or if my logic is simply off.

Here is an example of how the three objects look at the start. They're evenly spaced out by an x value of 340:

• buttons0 (Left Square) Pos X 340
• buttons1 (Middle Triangle) Pos X 680
• buttons2 (Right Triangle) Pos X 1020
• targetPos (The position they're traveling to) Pos X -340
• RespawnPos (Where they restart) Pos X 680

And here is one example of what eventually happened to the spacing after a short period of time:

Here is the code I am currently using:

public IEnumerator MoveTheButtons()
{
var i = 0;

while(needsToMove)
{

while (i < 3)
{
buttons[i].MyRectTransform.position = Vector2.MoveTowards(buttons[i].MyRectTransform.position, targetPos.position, buttonTravelSpeed * Time.deltaTime);

if (buttons[i].MyRectTransform.position.x <= targetPos.position.x)
{
buttons[i].MyRectTransform.position = RespawnPos.position;

}

i++;

}

if (i == buttons.Count)
{
i = 0;
}

yield return null;

}

}


Additionally, here is the code calling the MoveTheButtons() Coroutine. I made sure that it's only being called once:

private IEnumerator BeginActionCommand()
{
xxxtestSkill.needsToMove = true;

// Start moving the buttons
StartCoroutine(xxxtestSkill.MoveTheButtons());

var isComplete = false;

while (!isComplete)
{
if (xxx.myInputActions.Minigame.Any.WasPressedThisFrame())
{
if(correctButtonPressed && currentButton.canBePressed)
{
Debug.Log("Correct Button Pressed!");
Debug.Log("Timed Correctly!");
}
else
{
Debug.Log("WRONG Button Pressed!");
}
}

if (xxx.myInputActions.Minigame.EndButton.WasPressedThisFrame())
{
isComplete = true;
yield break;
}

yield return null;

}

}


I'd like to keep this as a Coroutine and have one method move all three buttons, rather than have each object move on their own in their individual Update() methods.

I hope this is clear enough. Thanks so much for taking the time!😊

• Normaly you can get the amount to move by using functions like timeGettime() at cpu level that give you a relative time value independant of the framerate. But with unity i don't know how you define a timer. Oct 4, 2023 at 16:30
• @philB that's what the Time.deltaTime in the code above is doing, accounting for how much real world time elapsed since the previous frame. Oct 4, 2023 at 17:45
• Can you include the logic as well that is starting MoveTheButtons and the code for the previous movement? There might be some leftover or multiple calls error Oct 4, 2023 at 18:13
• Certainly, I'll add it to the first post. Though, I did make sure that MoveTheButtons() is only being called once. There's also no previous movement as I'm calling it once and stopping the game after the spacing changes. It is worth mentioning that this issue consistently happens when there is a spike in framerate. It doesn't make any sense to me as I'm using speed * Time.deltaTime. So, I'm not sure why framerate would be an issue, but it most definitely is impacting the spacing. Oct 4, 2023 at 19:08

Your framerate will very rarely work out such that your code executes at the exact moment the button hits the target. Usually you'll run a little early, and the button hasn't hit the target yet, or a little late, and the button should have hit the target a little in the past.

Mathf.MoveTowards removes any overshoot, so you don't see it go past the target. Then the respawn code puts them back exactly at the respawn point. That means if the button should have arrived at the target, say, 0.001 seconds ago, you've discarded 0.001 seconds' worth of its motion. It should have had time to move from the respawn point, but you discarded any "loose change" from overshooting the target instead of applying it to the next pass. This slightly shifts the position of the teleported button relative to its peers.

If you throw out enough of this loose change across several cycles, you begin to significantly distort the spacing between buttons.

We can fix that in a couple of ways. The most direct is this:

public IEnumerator MoveTheButtons() {
while(needsToMove) {

// No need for separate declaration/test/increment lines when 'for' does all that.
for (int i = 0; i < 3; i++) {
// We'll work on a temporary - cheaper than poking Transform every time.
Vector3 pos = buttons[i].MyRectTransform.position;

// Slide left and allow overshoot.
pos.x -= buttonTravelSpeed * Time.deltaTime;

// If we hit OR PASSED the target, warp back to respawn,
// preserving any overshoot as movement post-respawn.
if (pos.x <= targetPos.position.x)  {
pos.x += RespawnPos.position.x - targetPos.position.x;
}

// Assign just once.
buttons[i].MyRectTransform.position = pos;
}

// No need to compare i to count; for loop starts over next frame.
yield return null;
}
}


This preserves the loose change we have from overshooting the target... at least to within float rounding precision. You can still get accumulating error from this, but if you're only ever going through dozens of cycles, not hundreds or thousands, it probably won't be noticeable.

If you need something like this that's bulletproof, even if we leave the game cycling continuously for a week, we can go with a more over-engineered solution:

public IEnumerator MoveTheButtons() {
// We're going to model our cycle as a "phase" number that just
// continuously counts down from 1 to 0 and repeats forever.
float phase = 1f;

// Distance covered in one cycle (scale factor between phase and position)
float span = RespawnPos.position.x - targetPos.position.x;

// Time scale factor converting from seconds to cycles.
float phaseSpeed = buttonTravelSpeed / span;

// Initialize a phase offset for each button - how early/late should
// it cross the 0/target point, relative to the ruling phase metronome?
foreach (var button in buttons) {
// Note that this value can be arbitrarily large to start,
// if we have buttons that start way off and don't join the cycle
// until a few repetitions in.
buttons[i].phaseShift = (buttons[i].MyRectTransform.position.x - RespawnPos.position.x)/span;
}

while (needsToMove) {
phase -= Time.deltaTime * phaseSpeed;

// Wrap around at zero.
if (phase < 0f) {
phase += 1f;

// Any buttons that have not yet hit the target need
// their phase shift updated so they don't teleport back.
foreach (var button in buttons) {
if (button.phaseShift >= 0f) button.phaseShift -= 1f;
}
}

foreach (var button in buttons) {
// Compute button's position in cycle according to metronome.
float buttonPhase = phase + button.phaseShift;
// Wrap around at zero (teleport from target back to respawn point).
if (buttonPhase < 0f) buttonPhase += 1.0f;

// Turn button phase into corresponding position.
var pos = targetPos.transform.position;
pos.x += buttonPhase * span;
button.MyRectTransform.position = pos;
}

yield return null;
}
}

• This was incredibly useful. I had a feeling there was an issue with the logic I was using. Thank you so much for explaining why this was occurring on top of providing multiple solutions. It seems to work perfectly! 🙏 Oct 7, 2023 at 17:49