0
\$\begingroup\$

Stuttery movement has been plaguing me for the whole development of this project.

Some info on the game:

  • It's a 2D, top-down space game, and you control a spaceship that can accelerate or rotate.
  • If the ship isn't accelerating, rotation simply rotates the ship.
  • If the ship is moving, rotation also happens on the rigidbody's velocity, so that you're moving in the direction you're accelerating (like in racing games when you aren't drifting).

To get simple, unmoving rotation to work smoothly, I'm using rb.MoveRotation(rb.rotation + angle) (rb references the RB2D component). Doing this with Transform instead causes jitter.

For when accelerating, I'm modifying the rb.velocity's direction manually, as I could find no other way to do this.

The code is called from FixedUpdate(). Here's what it looks like:

public void Rotate(float axis, bool isAccelerating) {
    if (isAccelerating) {
        float angle = (rotationSpeed) * axis * (-1);
        rb.velocity = Vector2.Lerp(rb.velocity, Quaternion.Euler(0f, 0f, angle) * rb.velocity, (1f - smoothness) * Time.deltaTime * sm);
        transform.up = Vector3.Lerp(transform.up, Quaternion.Euler(0f, 0f, angle) * transform.up, (1f - smoothness) * Time.deltaTime * sm);
    } else {
        float angle = (rotationSpeed) * axis * (-1);
        rb.MoveRotation(rb.rotation + angle);
    }
}

As you can see, I tried everything to get the latter smooth. First, I tried simple lerping it. When that made the stutter worse, I tried multiplying the value by Time.deltaTime (and another multiplier, sm — for smoothness multiplier — to speed it up). That didn't help either.

For my camera, I'm using Cinemachin's 2D virtual camera (using Framing Transposer). I made sure to check that it's not the camera causing the stutter; it isn't.

I took a couple videos so that you can see the problem better. I turned the time scale down to 0.3 so it can be seen better on video, but it's there on normal speed, as well.

Here's the whole CharacterController2D.cs, or the relevant parts at least. (There's a variable, molasses and its corresponding _molasses, which is an artifact from old code. Its purpose was to slow down the ship's rotation speed when accelerating, but I've since changed the logic to rotate rb.velocity also, so you can safely ignore it.)

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[RequireComponent(typeof(Rigidbody2D))]
public class CharacterController2D : MonoBehaviour {
    // "public" fields
    [Header("Rotation\n")]
    [SerializeField] private float rotationSpeed = 10f;
    [SerializeField] private float molasses = 2f;
    [SerializeField] private AnimationCurve catchUp;
    [Header("Acceleration\n")]
    [SerializeField] private float acceleration = 40f;
    [SerializeField] private float topSpeed = 30f;
    [SerializeField] private float boostForce = 100f;
    [SerializeField] private float rollDistance = 5f;
    [SerializeField] private float rollDuration = 0.25f;
    [Range(0f, 1f)] public float smoothness;
    public float sm;

    // private fields
    private float axis;
    private float _molasses;

    // references
    public Rigidbody2D rb; // exposed for debugging; change to private later

    private void Start() {
        // caching references
        rb = GetComponent<Rigidbody2D>();
    }

    public void Rotate(float axis, bool isAccelerating) {
        if (isAccelerating) {
            float angle = (rotationSpeed / _molasses) * axis * (-1);
            rb.velocity = Vector2.Lerp(rb.velocity, Quaternion.Euler(0f, 0f, angle) * rb.velocity, (1f - smoothness) * Time.deltaTime * sm);
            transform.up = Vector3.Lerp(transform.up, Quaternion.Euler(0f, 0f, angle) * transform.up, (1f - smoothness) * Time.deltaTime * sm);
        } else {
            float angle = (rotationSpeed / _molasses) * axis * (-1);
            rb.MoveRotation(rb.rotation + angle);
        }
    }

    public void Accelerate() {
        rb.AddForce(transform.up * acceleration / _molasses);
    }

    public void LimitSpeed() {
        if (rb.velocity.magnitude > topSpeed)
            rb.velocity = rb.velocity.normalized * topSpeed;
    }

    public void Boost() {
        rb.AddForce(transform.up * boostForce, ForceMode2D.Impulse);
    }

    public void SetMolassesActive(bool isActive) {
        if (isActive)
            _molasses = molasses;
        else
            _molasses = 1f;
    }
}

And here's PlayerMovement.cs, which calls the methods. Again, relevant parts to slim it down.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[RequireComponent(typeof(CharacterController2D))]
public class PlayerMovement : MonoBehaviour {
    // references
    private CharacterController2D cc;

    //private fields
    private float axis;
    private bool fired1;
    private bool shouldSpin;

    private void Start() {
        cc = GetComponent<CharacterController2D>();
        particles.Stop();
    }

    private void Update() {
        // updating fields
        axis = Input.GetAxis("Horizontal");
        fired1 = Input.GetButton("Fire1");

        if (Input.GetKeyDown(KeyCode.Space))
            cc.Boost();
    }

    private void FixedUpdate() {
        if (fired1 && Mathf.Abs(axis) > 0) {
            cc.SetMolassesActive(true); // ignore this
            cc.Rotate(axis, true);
        } else if (Mathf.Abs(axis) > 0) {
            cc.Rotate(axis, false);
        }

        if (shouldSpin)
            cc.Rotate(1f, false);

        if (fired1) {
            cc.Accelerate();
        }

        cc.SetMolassesActive(false); // and this

        cc.LimitSpeed();
    }
}

Finally, here's my .gitignore-d Unity project: https://we.tl/t-3F6IhPHHzY. It's 5 MB.

To re-iterate the problem: rb.MoveRotation() is the only way I've found that smoothly rotates my player game object, but it's not enough for my 2nd logic case, because I need to adjust my rb.velocity to follow. But simply adjusting it, or even lerping it, causes stuttering. How can I fix this?

\$\endgroup\$
2
\$\begingroup\$

Answer

I figured it out: there's absolutely nothing wrong with changing the direction of your rigidbody's velocity.

The problem was using transform.up to rotate the game object itself. I replaced that with rb.MoveRotation(rb.rotation + angle) and it's buttery smooth now.


Takeaway

In general, if there's anything I've learned from this project, it's that everyone should learn the ins and outs of their game engine of choice. If I had better understood how Unity and its physics engine work under the hood, many of the problems I encountered with jittery movement could've been avoided altogether.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.