1
\$\begingroup\$

I am working on an Air Hockey game; I tried making the movement physics-based, by doing this:

using UnityEngine;

public class Player : MonoBehaviour
{
    private Rigidbody m_Rigidbody;

    private float HorizontalInput;
    private float VerticalInput;

    public float Speed = 1.0f;

    private void Start()
    {
        m_Rigidbody = GetComponent<Rigidbody>();    
    }

    private void Update()
    {
        HorizontalInput = Input.GetAxisRaw("Horizontal");
        VerticalInput = Input.GetAxisRaw("Vertical");
    }

    private void FixedUpdate()
    {
        m_Rigidbody.AddForce(new Vector3(VerticalInput * Speed, 0.0f, -HorizontalInput * Speed),ForceMode.VelocityChange);
    }
}

and by adding a PhysicsMaterial to the MeshCollider attached to my player:

The movement should be quick and responsive, but it feels rather sloppy and unresponsive; I also tried using Input.GetAxisRaw(...), but the controls become quite clunky.
How can I improve on this? Is there a go-to way to do this type of movement in Unity?

(I would like the final version of the game to be played on controllers)

\$\endgroup\$

2 Answers 2

2
\$\begingroup\$

I know it's common advice to capture input in Update and defer acting on it until FixedUpdate, but I think this advice is overly simplistic. I don't follow it in my own games.

CAUTION: This worked in Unity versions circa 2018 when this answer was written. It does not work in 2019 versions I've tried - it looks like they updated the engine to match the documentation instead of the other way around, breaking a useful latency-reduction trick. :(

The numbered advice points later in the answer are still good practices to follow that will also have a dramatic impact on your responsiveness, so all is not lost.

Because FixedUpdate runs before Update each frame (when using the default game loop at least), doing it this way forces at least one unnecessary frame of latency:

| Frame 1                      | Frame 2                       |
| FixeUpdate...Update...Render | FixedUpdate...Update...Render |
                  ^                     ^                      ^
Input cached here |   Physics acts here |                      | Results Visible

If we check input in FixedUpdate, we can eliminate this unnecessary latency in any frames when FixedUpdate is called at least once (and we can adjust our FixedUpdate rate at or above our target framerate to ensure this is almost always the case):

| Frame 1                      | 
| FixeUpdate...Update...Render | 
   ^                           ^ 
   | Input & Physics           | Results Visible in the same frame

Now we need to be a bit cautious doing this, to ensure we don't miss or double-dip on instantaneous actions like Jumps. Here's one way we can do that:

bool hasHandledInputThisFrame = false;

void HandleInput(bool isFixedUpdate) {
    bool hadAlreadyHandled = hasHandledInputThisFrame;
    hasHandledInputThisFrame = isFixedUpdate;
    if(hadAlreadyHandled)
        return;

    /* Perform any instantaneous actions, using Time.fixedDeltaTime where necessary */
} 

void FixedUpdate() {
    // If we're first at-bat, handle the input immediately and mark it already-handled.
    HandleInput(true);

    /* Apply any continuous physics changes here. */
}

void Update() {
    // Catch any missed input and reset the flag for next frame.
    HandleInput(false); 
}

There, now we know we don't have unnecessary latency creeping in from the order of our update methods. What else can we do to improve responsiveness?

  1. Since we're using physics-based movement, we can get an apparent stutter or judder in the motion due to the mismatch between the rendering framerate and the physics update. This can make the movement look low-performance.

    Enabling interpolation on the Rigidbody smooths this out. Technically it can add a fraction of a physics step of latency because we're blending with a past state, but in my experience the smoothness it gives is even more important to the perception & feel of the controls - so long as your physics step isn't unusually long. As long as the player can see some effect of their recent input blending in through the interpolation, rather than no change at all or a stuttery inconsistent response, it still reduces perceived latency.

    Once you do this, make sure you don't set the position/rotation via the transform component. Doing this effectively turns off interpolation and you're back to judder land. Route all transformation changes via the rigidbody to keep it smooth.

  2. Separate your character's speed from their acceleration. Giving a high acceleration - and in particular, an even higher deceleration when stopping / changing direction, helps the controls feel snappy, even if the net movement speed stays where it is.

    Here's a quick example I whipped up for my students a few weeks ago, that shows the basic idea. You can of course elaborate on this with custom acceleration curves, sliding effects, etc.


float maxAcceleration = 10f;
float maxDeceleration = 20f;
float maxSpeed = 10f;

void FixedUpdate() {
    // Handle instantaneous inputs exactly once per frame.
    HandleInput(true);

    // Now handle the continuous inputs like analog stick direction every step:

    // Capture analog stick direction (yes, this works fine in FixedUpdate).
    Vector3 analog = new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical"));
    analog = Vector3.ClampMagnitude(analog, 1f);

    Vector2 targetVelocity = analog * maxSpeed;

    AccelerateToward(targetVelocity);
}


void AccelerateToward(Vector3 targetVelocity) {
    float limit = maxAcceleration;

    var deltaV = targetVelocity - body.velocity;

    // If we're stopping or reversing direction, use deceleration limit instead.
    if (Vector3.Dot(body.velocity, deltaV) < 0f)
        limit = maxDeceleration;

    // Compute an acceleration to use this frame, within our allowed limits.
    // (Since we're calling this in FixedUpdate, deltaTime gives our fixed timestep)
    var accel = deltaV / Time.deltaTime;
    accel = Vector2.ClampMagnitude(accel, limit);

    body.AddForce(accel, ForceMode.Acceleration);
}
  1. Provide immediate feedback. The biggest ingredient in perceived latency is waiting to see evidence that the game heard you and is listening. If you use physics or animation-based movement, this could take several frames to become apparent. But if you can feed back some acknowledgement earlier, you tighten the loop between input & confirmation, and the controls feel instantly tighter even with no change in the underlying gameplay behaviour. Some forms this can take:

    • rotating the character in the new direction (especially if they're a circle/capsule and this has no physics impacts anyway)

      If rotating the whole character is too extreme, you can try using IK to turn the character's head to look where they're going to move next, or rotate the thrust effects coming out of a spaceship....

    • throwing out particles like a skid effect when changing movement suddenly

    • sound effects (eg. a skid or heavier footfall when stopping/turning)

\$\endgroup\$
0
\$\begingroup\$

(1) Have you tried to increase Gravity in controller settings? (2) And I would take Time.deltatime into account, because the movement of player should be same relative to different framerates.

enter image description here

\$\endgroup\$
3
  • \$\begingroup\$ The movement is physics based, so Time.deltaTime isn't helpful, since each instruction inFixedUpdate() gets executed in a complete cycle when physics stuff is processed by Unity. Gravity doesn't change much, since it's not really related to the actual motion of sliding onto the slippery surface \$\endgroup\$
    – user115399
    Commented Dec 22, 2018 at 18:13
  • \$\begingroup\$ @GabrieleVierti including deltaTime is valuable even for FixedUpdate code, so that you retain the same behaviour even if you later change your FixedUpdate rate. You wouldn't want to have to go re-tune all your speed & acceleration parameters after making such a change. "Gravity" here doesn't have anything to do with the interaction with the surface - it's a measure of how quickly the reported value from the button input re-centers when the buttons are released. So it can have an impact on responsiveness when using a keyboard / DPad to emulate analog input, but won't affect an analog stick. \$\endgroup\$
    – DMGregory
    Commented Dec 22, 2018 at 21:03
  • \$\begingroup\$ @DMGregory Ah, I didn't think about the "gravity" option in the input settings :P \$\endgroup\$
    – user115399
    Commented Dec 23, 2018 at 8:05

You must log in to answer this question.