How can I adjust the direction of my rigidobody.velocity without stuttering?

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
[SerializeField] private float rotationSpeed = 10f;
[SerializeField] private float molasses = 2f;
[SerializeField] private AnimationCurve catchUp;
[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() {
}

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

public void Boost() {
}

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?

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.