Search code examples
c#unity-game-enginegame-enginegame-physics

How do I create an object to move between two positions in unity, using C#, and why is my code not working?


I have this piece of code right here that should make the block object move between the startPos and endPos objects, but something is wrong about it and I don't know what.

void FixedUpdate()
{
    if (block.transform.position == startPos.transform.position)
    { 
        check = false; 
    }

    if(block.transform.position == endPos.transform.position)
    { 
        check = true;  
    }

    if (check == false)
    {
        block.transform.position = Vector3.Lerp(block.transform.position, endPos.transform.position, .03f);
    }

    if (check == true)
    { 
        block.transform.position = Vector3.Lerp(block.transform.position, startPos.transform.position, .03f);     
    }
}

At some point the block will reach endPos, and then on its way back to startPos it will stop, because the functions will be executed simultaneously. But how is this possible because my if's right there should not allow this to happen?


Solution

  • In general you should always be using

    • Update → called every frame

    instead of

    except you are dealing with Physics somehow (which doesn't seem to be the case here). Also see the Update and FixedUpdate Tutorial


    The issue with Vector3.Lerp is that it doesn't behave as you expect.

    I guess you like that it starts fast and then becomes "smooth" ... but actually this might be your problem.

    It never reaches the target position really. It just gets closer and closer and slower and slower ...

    ... until at some moment the == with a precision of 0.00001f eventually becomes true.

    So it might seem that it stopped but actually it might still be moving just really really slow.


    For the following two alternatives you have to decide a bit what you want to control:

    1. Option: The speed

      If you want to have a linear velocity for the object you should rather use

      // adjust via the Inspector
      [SerializeField] private float moveSpeedInUnityUnitPerSecond = 1f;
      
      // you should use Update here in general
      void Update()
      {
          if (block.transform.position == startPos.transform.position)
          { 
              check = false; 
          }
          // always use else in cases where only on condition can be
          // true at the same time anyway
          else if(block.transform.position == endPos.transform.position)
          { 
              check = true;  
          }
      
          block.transform.position = Vector3.MoveTowards(block.transform.position, check ? startPos.transform.position : endPos.transform.position, Time.deltaTime * moveSpeed);
      }
      
    2. Option: The duration If you rather want a smooth movement but control the duration it takes to reach the target you should use a Lerp but with a factor depending on the time like

      // adjust via the Inspector
      [SerializeField] private float moveDurationInSeconds = 1f;
      
      private float passedTime;
      
      // you should use Update here in general
      void Update()
      {
          // prevent overshooting
          passedTime += Mathf.Min(moveDurationInSeconds - passedTime, Time.deltaTime);
      
          if(passedTime >= moveDurationInSeconds)
          {
              check = !check;
              passedTime = 0;
          }
      
          var lerpFactor = passedTime / moveDurationInSeconds;
          // and now add ease-in and ease-out
          var smoothedLerpFactor = Mathf.SmoothStep(0, 1, lerpFactor);
      
          var fromPosition = check ? endPos.transform.position : startPos.transform.position;
          var toPosition = check ? startPos.transform.position : endPos.transform.position;
      
          block.transform.position = Vector3.Lerp(fromPosition, toPosition, smoothedLerpFactor);
      }
      

      For this you could also use a Coroutine which usually is a bit easier to interpret and maintain:

      // adjust via the Inspector
      [SerializeField] private float moveDurationInSeconds = 1f;
      
      // yes you see correctly one can directly use the Start
      // as a Coroutine
      private IEnumerator Start()
      {
          var fromPosition = startPos.transform.position;
          var toPosition = endPos.transform.position;
      
          // looks strange but as long as you yield somewhere inside
          // the loop it simply means repeat the sequence forever
          // just like the Update method
          while(true)
          {
              var passedTime = 0f;
      
              while(passedTime < moveDurationInSeconds)
              {
                  var lerpFactor = passedTime / moveDurationInSeconds;
                  // and now add ease-in and ease-out
                  var smoothedLerpFactor = Mathf.SmoothStep(0, 1, lerpFactor);
      
                  block.transform.position = Vector3.Lerp(fromPosition, toPosition, smoothedLerpFactor);
      
                  passedTime += Mathf.Min(moveDurationInSeconds - passedTime, Time.deltaTime);
      
                  // reads like: "pause" here, render this frame and continue
                  // from here in the next frame
                  yield return null;
              }
      
              // once reached flip the positions
              var temp = fromPosition;
              fromPosition = toPosition;
              toPosition = temp;
          }
      }
      

      in both cases you could still add more flexibility and instead of simply using the moveDurationInSeconds use

      var fixedDuration = moveDurationInSeconds * Vector3.Distance(fromPosition, toPosition);
      

      this way the movement takes shorter if the positions are closer together and longer if they are further apart. This comes pretty close to the Lerp you used before regarding to the smoothness of motion but you can control very good how long the movement will take.