Asynchronously and smoothly move a 2D player character?

Let’s say I have a 2D rpg game (think Pokemon or old Zelda).

I want to hit “up” and have the player character smoothly walk one block up (say a block is 32 pixels, or some noticeable distance, and characters always move in units of blocks… basically Pokemon, like I said).

On beginner’s instinct I wanted to do this:

void Update() {
    if(Input.GetKeyDown(KeyCode.upArrow) {
        Vector2 newPos= //blah blah blah Mr Freeman
        transform.position = Vector2.Lerp(oldPos, newPos, Time.deltaTime);
    }
}

But obviously the problem arises that Lerp must happen continuously to appear like smooth motion, and since it is enclosed within that if statement, that will definitely not be the case, meaning the object will jump across the screen.

The obviously (and obviously bad) “solution” would be a while loop:

	// Update is called once per frame
	void Update () {
		moveCharacter();
	}


	void moveCharacter() {
		if(Input.GetKeyDown (right)) {
			Vector2 to = //blah blah blah
			while(Vector2.Distance(transform.position, to) > 3) {
				transform.position = Vector2.Lerp (transform.position, to, Time.deltaTime);
			}
		}
	}

But this raises some questions. First, it seems like Time.deltaTime doesn’t change within the same iteration of Update()? That is to say, Update() was called once, which then called moveCharacter(), which started the while loop. But since we’re still in just the first iteration of Update(), does Time.deltaTime remain accurate, so that Time.deltaTime in the beginning of a long Update() call will be different than Time.deltaTime by the end?

Also, I’m curious as to what thread this while loop sits on. If I have a lengthy while loop (never a good idea) and it’s in a script associated with this player GameObject, if it starts looping forever, what will it block? Will the whole game freeze as this while loop performs? Or is threading done intelligently?

In the end, my question is “How can I perform this translation smoothly over time without blocking other execution and without doing something needlessly complex.” Hopefully I get answers to all those other questions I asked along the way, too…

Thanks for listening.

Unity runs in a single thread - Updates and other Unity special functions are called sequentially and they can block.

Normal functions are not threaded and cause blocking behaviour (your code blocks execution, moves the character gradually from a to b over one frame, then resumes).

Coroutines run when they can (similar timing to Update, but I’m not sure what order Update, coroutines, and LateUpdate are called in), but are still part of the main program and will block while they’re executing. You use yield return {value} to pause execution of a coroutine, and you must always use StartCoroutine to begin a coroutine (if your coroutines seem to be silently failing, you probably forgot to StartCoroutine them)

yield return null - wait one frame

yield return new WaitForSeconds(float) - wait for some number of seconds

yield return StartCoroutine(Function()) - wait for a subroutine to complete before continuing

void Update() {
  if ( Input.GetKeyDown(KeyCode.UpArrow) ) {
    StartCoroutine(Move(Vector3.up));
  }
}

bool isMoving = false;
float speed = 2f;

IEnumerator Move(Vector3 offsetFromCurrent) {
  if ( isMoving ) yield break; // exit function
  isMoving = true;
  Vector3 from = transform.position;
  Vector3 to = from + offsetFromCurrent;
  for ( float t = 0f; t < 1f; t += Time.deltaTime * speed ) {
    transform.position = Vector3.Lerp(from,to,t);
    yield return null;
  }
  transform.position = to;
  isMoving = false;
}

Actual threads run in parallel as expected, but it’s important to note that their access to Unity objects (Transform, etc) is very limited (you generally need to create a separate class, unrelated to Unity, which your Unity-linked class can communicate with).

private float currentX; //Current x position
private float currentY; //Current y position
private Vector2 playerPos; //Player Position
private float stepTargetX; //Destination after key press
private float stepTargetY; //Destination after key press

void Update()
{
    if(Input.GetKeyDown(Up) && playerY == stepTargetY) //Can only move after previous move is finished
    {
        stepTargetY = playerY + 32 //or whatever number you want
    }
    
    if(playerY < stepTargetY)
    {
        Mathf.Lerp(playerY, stepTargetY, //Whatever time you want * Time.deltaTime)
    }
    //Repeat for other directions

    playerPos = new Vector2(playerX, playerY);
    transform.position = playerPos;

}

Hope it helps get you started. Idk about answering the rest of those haha but I think this should work. If not let me know.