Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

Hovering at a specific height

Discussion in 'Physics' started by theearthwasblue, Jan 13, 2018.

  1. theearthwasblue

    theearthwasblue

    Joined:
    May 22, 2017
    Posts:
    22
    So I'm making a super quick project where you control a hovering ship, and I'm stumped on something. This is eventually how i want the ship to behave: when you press the spacebar, the ship will rise in altitude until it hits a set height relative to the y position of the floor directly below it, at which point it will stop rising and hover, changing altitude only with the y position of the floor or until the player turns off the engines.

    So far, I have a player object with engines that can be turned on and off, and that can move forward and backward. When the player engages the vertical thrust, it automatically rises, uses raytracing to measure in real time how high it is above the floor (in this case, 4 units above the ground), and turns off vertical thrust when it surpasses that limit.

    The problem is that I'm running into issues getting it to hover at a stable rate when it reaches its max height. What it currently does is that when the player turns on the engines, it rises until it reaches its max height above the ground, at which point it turns off vertical thrust and plunges down. It then automatically re-engages thrust and rises back up, where the cycle continues. It keeps bobbing up and down like this until the player turns off the thrust altogether.

    I know exactly what is wrong, I just don't know how to go about fixing it. Basically, it gathers momentum as it rises, and when it hits the max height, it continues flying upwards in a neutral state until it loses its momentum and falls down. Because it gathered momentum from falling, it's thrust cant counteract its vertical velocity in time and it falls, then when it rises the exact same thing keeps happening. How can I get it to "ease" into its max height instead of just blindly throwing itself around? I have an idea about comparing it's Vector3.Up magnitude to the magnitude of Physics.Gravity, and then doing a Lerp to slowly equalize them, but I'm new to this and I don't even know if that's actually possible :p

    thoughts?

    Code (CSharp):
    1. using System.Collections.Generic;
    2. using UnityEngine;
    3.  
    4. public class PlayerMovement : MonoBehaviour
    5. {
    6.     //declar variables here
    7.     private Rigidbody Rb;
    8.     public float speed;
    9.     public float heightF;
    10.  
    11.     float thrust;
    12.  
    13.     public bool on;
    14.  
    15.     // setup variables
    16.     void Start ()
    17.     {
    18.         on = false;
    19.  
    20.         thrust = 15f;
    21.         speed = 7.5f;
    22.  
    23.         Rb = GetComponent<Rigidbody>();      
    24.     }
    25.    
    26.     //main code
    27.     void FixedUpdate ()
    28.     {
    29.         //Functions
    30.         GetHeight();
    31.  
    32.         //Handle movement
    33.         float Vx  = Input.GetAxis("Horizontal");
    34.         float Vz = Input.GetAxis("Vertical");
    35.  
    36.         Vector3 MoveV = new Vector3(0, 1, 0);
    37.         Vector3 MoveH = new Vector3(Vx, 0, Vz);
    38.  
    39.         Rb.AddForce(MoveH * speed);
    40.  
    41.         //Handle verticle thrust
    42.         if (Input.GetKeyUp(KeyCode.Space))
    43.         {
    44.             on = !on;
    45.         }
    46.  
    47.         if (on == true)
    48.         {
    49.             if (heightF <= 4)
    50.             {
    51.                
    52.                 Rb.AddForce(Vector3.up * thrust);
    53.  
    54.             }
    55.         }
    56.     }
    57.  
    58.     //Collect height
    59.     private void GetHeight()
    60.     {
    61.         Ray Height = new Ray(transform.position, new Vector3(0,-1,0));
    62.         RaycastHit floorHit;
    63.  
    64.         if (Physics.Raycast(Height, out floorHit))
    65.         {
    66.             heightF = (transform.position.y-(transform.localScale.y/2)) - floorHit.point.y;
    67.         }
    68.        
    69.     }
    70. }
     
  2. dgoyette

    dgoyette

    Joined:
    Jul 1, 2016
    Posts:
    4,193
    You're describing a system that's probably best handled with a PID controller (https://en.wikipedia.org/wiki/PID_controller). That may sound a bit complicated, but the idea is that at any given moment, your flying object has some "error", which is the amount of distance away from the place it should be. If you just try to correct the error alone, you end up with a lot of jitter and swinging back and forth. For this reason, a PID controller take into account the change in error over time, and allows you to make finer adjustments that anticipate future error based on past error (more or less).

    I've used this approach for almost exactly the purpose you describe: getting a flying object to hover at a desired height, even if other forces temporarily knock it off course. Here's the code I used:
    Code (CSharp):
    1.  
    2.    public float Kp;
    3.    public float Ki;
    4.    public float Kd;
    5.  
    6.     Vector3 totalError = new Vector3( 0, 0, 0 );
    7.     Vector3 lastError = new Vector3( 0, 0, 0 );
    8.  
    9.     void FixedUpdate()
    10.     {
    11.         var currentError = gameObject.transform.position - this.DestinationPosition;
    12.  
    13.         Vector3 cp = currentError * Kp;
    14.         Vector3 cd = Kd * (lastError - currentError) / Time.deltaTime;
    15.         Vector3 ci = totalError * Ki * Time.deltaTime;
    16.  
    17.         lastError = currentError;
    18.         totalError += currentError;
    19.  
    20.         var navigationForce = Vector3.ClampMagnitude( cp + ci + cd, 300 );
    21.         var standardForce = _rigidbody.useGravity ? (Physics.gravity * _rigidbody.mass * -1) : new Vector3( 0, 0, 0 );
    22.  
    23.         var finalForce = standardForce - navigationForce;
    24.  
    25.         _rigidbody.AddForce( finalForce );
    26.     }
    27.  
    Some notes:
    • DestinationPosition is where the object should be. You'll potentially change this often, and the object will fly off for the new location. In your case, since you're adjusting the X and Z coordinates by the player's force, you could potentially set this to a new Vector3 each time through, keeping the X and Z position the object currently has, and just choosing the Y value that makes sense for the hover height.
    • Kp, Ki, and Kd are all "constants", but I'm setting them in the inspector. In my case I was using 30, 0, and 50, respectively. But these are the values you'll want to adjust until it feels right. This can be a bit of trial and error.
    • The "300" refers to how much force the object can exert, and is just a value that seemed to work for me, but you can play around with it.
     
    Gekigengar likes this.
  3. theearthwasblue

    theearthwasblue

    Joined:
    May 22, 2017
    Posts:
    22
    interesting, thanks!

    So I just want to make sure I'm understanding this.

    What I was trying to do is create a more real-world approach where my object is constantly being thrust upward, and where it automatically adjusts said thrust to create a hover at a set height. What you did was create a point that the object is continually "snapping" to, by adding force in the appropriate direction?

    What do Kp, Ki, Kd, Cp, Ci, and Cd actually stand for?
     
  4. dgoyette

    dgoyette

    Joined:
    Jul 1, 2016
    Posts:
    4,193
    The code I posted does the thing you describe you want. I wouldn't say it causes the object to "snap", since that makes it sound like it's jumping around without physics forces being used. Here's a gameplay video of one of my drones flying around with just this functionality:


    The drone is basically flying with waypoint navigation. I tell it where it should try to go, and it gets itself there using physics forces. You can see in the video that it moves pretty smoothly.

    The "K" value are the "constants" of the PID controller. You can read the specifics of that in the wikipedia article. Basically. I'd start them all at 0, and then adjust them until it feels right. It's hard to predict exactly what will feel like in your case. The "C" values are just the implementation of the PID controller, where it's determining an amount of force based on the error over time. In short, those three "C" values should balance each other to prevent overshooting the target point.

    Again, I'm setting a destination for my drone, and it flies both up/down and side to side to get there. In your case, it sound like you'd want to make sure the destination position takes the objects' current X and Z position, but has a dynamic Y position, if you're letting the player move the object left/right as part of your gameplay.
     
  5. alexeu

    alexeu

    Joined:
    Jan 24, 2016
    Posts:
    257
    @theearthwasblue i think your code could work if you use two force vectors. forceUp & forceDown. And when you use one set the other to vector3.zero.
    You should maybe clamp the force before applying it i'm not sure.

    Ps: depending on your goal you can also use one of the simplest way to "hover" i mean
    Mathf.Sin() like this
    Code (CSharp):
    1. float staticHoverForce = Mathf.Sin(Time.time * 2) * thrust;
    2. rb.AddForce(Vector3.up * staticHoverForce, ForceMode.Acceleration);
    and you dont need two forces.
     
    Last edited: Jan 20, 2018
  6. dgoyette

    dgoyette

    Joined:
    Jul 1, 2016
    Posts:
    4,193
    I think that wouldn't work in @theearthwasblue's case, as he mentioned he wanted the flying object to remain relative to the floor. So, if the object flies up a ramp, it should remain about the same distance above the ramp at all times. With your code, nothing would make the object rise as the floor height increased.
     
  7. alexeu

    alexeu

    Joined:
    Jan 24, 2016
    Posts:
    257
    the static one with Mathf.Sin() indeed no. but with 2 forces it works. u need a distance to floor ? raycast it.
    i have no time to make a video but it works. you can even use this to manage forces for a nice hovering while moving
    and critical forces used when you're out range (max height, min heigt).
     
  8. dgoyette

    dgoyette

    Joined:
    Jul 1, 2016
    Posts:
    4,193
    I guess it depends on how precise you want it. With your two-forced approach, the challenge is really in determining how much force per frame. Merely having them on/off will result in oscillation, which @theearthwasblue said he was trying to avoid. Essentially, unless you want the object wobbling, you'd need to apply a different amount of force in every frame. That's exactly what the PID controller computes. It might sound like a complex approach initially, but ultimately it's just a few lines of code, which results in very precise control with no wobble. Or, it could have wobble if you want, by adjusting the constants.

    It depends on what effect you're going for.
     
  9. alexeu

    alexeu

    Joined:
    Jan 24, 2016
    Posts:
    257
    Do you have a concret exemple with that few lines ? i mean we are talking about crafts so lets put some horses in the engine...


    Sure there are many ways to do that depending on the differents goals but its never "few lines"....
     
  10. theearthwasblue

    theearthwasblue

    Joined:
    May 22, 2017
    Posts:
    22
    Sorry its taken me so long to respond: I've been dealing with work and such T__T

    Yeah I definitely think the PID controller is the way to go. Sure its more complicated, but it does exactly what I need it to do. Not to mention it will be easy to adapt this to rotation as well which I'm also going to implement to assure that my hovercraft stays level. @dgoyette after doing some research about PIDs, I just have one more question about the constants, just to make sure I understand your example correctly.

    Kp = correct solely the difference between two values (a big move to get it to the right spot)
    Ki = counteracting outside forces currently acting on the object (ie: counteracting gravity as it travels up)
    Kd = preemptive adjustments based on what it will be doing next (ie: slowing down so that it doesn't overshoot)

    So please correct me if I'm wrong, but this is my understanding of your code as a whole:

    1. take the constants and run them through various applications of position and time, based on their function, to get the Cx variables
    2. the Cx variables will be added via NavigationForce, which will dictate the direction (based on the combined Vector3 values of Cx), and force at which to move the object.
    3. StandardForce exists to counteract the force of gravity, if there is one, so that the PID controller can act without outside influence. If gravity is not used, StandardForce will be (0, 0, 0)
    4. FinalForce is calculated based on the result of subtracting the force of gravity from the applied force of NavigationForce, and then the object is moved in the direction and magnitude of the result

    Is this correct?

    So why bother with constants at all? It seems like it would be more effective to calculate in real time what the forces should be rather than resorting to experimentation. Also, what is the point of messing with gravity by way of StandardForce? Isn't the purpose of a PID so that you can apply force from any direction and the script will correct?

    Not knocking your code: from the video you posted, it seems to work great. I'm just new and genuinely curious :)
     
  11. dgoyette

    dgoyette

    Joined:
    Jul 1, 2016
    Posts:
    4,193
    Your description of Kp and Kd looks good to me. Ki is a little more nebulous, and is mainly used to offset a persistent error/jitter in the system. I didn't even up using it in my case.

    As for why we use "experimentation" to answer this, I think the answer is that it's just not trivial/simple to determine the right amount of force to apply by other means. Or this is perhaps the most elegant simplification of it. Maybe it's somewhere in the wikipedia article :)

    As for the "standardForce" and gravity thing, you're correct that the PID controller would work without this. In my case, I preferred the look and feel of this approach, which makes the object feel more weightless. It would just be exerting more y-axis forces.

    With "navigationForce" I'm capping the maximum possible force to not exceed 300. This is effectively a "max thrust" constraint. Without this, the force could keep increasing, and the object could move very fast. So, ClampMagnitude is being used to ensure that the length of the force Vector doesn't exceed 300. Note, though, that if you don't use the gravity offset that I'm using, you'll need to adjust this. If you're not offseting gravity with "standardForce", it will take a lot of force to keep the object off the ground, and clamping the total force to 300 might just make the object fall out of the sky. So, if you do get rid of the standardForce, you'll want to either get rid of the ClampMagnitude here, or only clamp the horizontal components of the vector.

    Hopefully I haven't added more confusion with this explanation. I really only know as much as I learned while getting my drone to fly.