- Home /
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.
Answer by Lazdude17 · Jul 02, 2014 at 06:14 AM
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.
I ended up doing everything from your suggestion, except I simplified it a bit by removing the lerp. The reason for this is it turns out I don't want acceleration and deceleration with the movement, just constant speeds. So simply transfor$$anonymous$$g the position directly did the job.
Just wondering, how do I figure out the dimensions of my screen? Not pixels, but whatever unit of density Unity uses? It seems to be far denser than pixels, since lerping a character was specific down to many decimals...
Well when you import a sprite you have the option to set the pixels to units conversion. If you left it at 100 then 100 px X 100 px is One Unity Unit. So if you have a 16X16 character then your Unit would be about 6.25 characters across. So I think you could find out the size you want through that...I believe this to be accurate haha may want to ask a smarter person.
Hey, hope you don't $$anonymous$$d me hopping in here, but this question/thread is pretty much what I'm trying to do for a simple movement test but the above code doesn't seem to work and similar questions on here aren't trying to achieve the same effect as this thread.
So, basically, I'm just trying to increment the x or y position by 0.7 (which would move the character to the next tile) and while i can get an instant "snap" to work easily enough, getting the character sprite to visually transition to that position is the problem I've been having for the past few hours.
The code above seems to be the right idea for what I'm trying to do, but it results in zero character movement. Now, some of the variables were undeclared or mislabeled in the example ("currentX vs "playerX", for instance); or "playerX" & "playerY" never being defined), so I'm wondering if my attempts to correct those somehow screwed something else up?
Here's the code I have so far, for just the "up" and "right" keys (so both x and y values can be modified- i'll just duplicate the other two once i get these working).
private float playerX; //Current x position
private float playerY; //Current y position
private Vector2 playerPos; //Player Position
private float stepTargetX; //Destination after key press
private float stepTargetY; //Destination after key press
void Start()
{
playerX= transform.position.x;
playerY= transform.position.y;
}
void Update()
{
if(Input.Get$$anonymous$$eyDown("up") && playerY == stepTargetY) //Can only move after previous move is finished
{
stepTargetY = playerY + 0.7f; //or whatever number you want
}
if(playerY < stepTargetY)
{
$$anonymous$$athf.Lerp (playerY, stepTargetY, 0.5f * Time.deltaTime);
}
if(Input.Get$$anonymous$$eyDown("right") && playerX == stepTargetX) //Can only move after previous move is finished
{
stepTargetX = playerX + 0.7f; //or whatever number you want
}
if(playerX < stepTargetX)
{
$$anonymous$$athf.Lerp (playerX, stepTargetX, 0.5f * Time.deltaTime);
}
//Repeat for other directions
playerPos = new Vector2(playerX, playerY);
transform.position = playerPos;
}
Answer by Loius · Jul 02, 2014 at 06:11 AM
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).