First Person Controller Has Mouse / Camera Stutter

Godot Version

Godot Engine v4.2.1.stable.official.b09f793f5

Question

Hi I’m working on a first person controller for a 3D game. Sometimes when testing there are sporadic spikes in _process and _physics_process resulting in the camera (controlled by the mouse) to stutter a random direction. However it happens unreliably, sometimes when I reboot Godot and or my computer the issue goes away, but sometimes it sticks around.

I’ve consulted the docs (especially: Fixing jitter, stutter and input lag — Godot Engine (stable) documentation in English), looked up related issues on the godot forums / reddit. I’ve learned a lot about Godot in the process accept what is causing this camera/mouse stutter.

Any help or guidance would be much appreciated. Thanks in advance!

Specs:

  • Intel(R) Core™ i7-10700K CPU @ 3.80GHz 3.79 GHz
  • RAM 32.0 GB
  • Windows 11 Home
  • Running off an SSD
  • NVIDIA GeForce GTX 980 Ti
  • Godot Engine v4.2.1.stable.official.b09f793f5
  • Vulkan API 1.3.277 - Forward+

Things I have tried:

  • Updating OS, graphics drivers, etc
  • Turning off all environmental effects/lighting
  • Disabling Vsync and/or switching to different vsync modes
  • Enabling G-Sync on GPU
  • Using Fullscreen, Exclusive Fullscreen, and windowed mode
  • Low Processor mode Enabled/Disabled
  • Setting monitor refresh rate to 60Hz to match the in game 60 phys ticks setting
  • Setting Application > Run > Max FPS to slightly below the physics ticks (56)
  • Setting the GPU to a max performance profile
  • Switching updating the camera movements between _process and _physics_process
  • Checking my CPU, RAM, GPU etc aren’t bottlenecking/heating
  • I’ve even switched mice to rule out any weird polling rate / input issues
  • Adjusting the max_physics_steps_per_frame
  • Tried turning off some of the remote tree refresh settings to make sure Godot wasn’t slowing it down when it was running
  • For sanity check I export a .exe and the issue still happens there too
  • Probably some other steps I tried but forgot - was trying to figure this out for the better part of a day on my own

I’m 99% sure it’s something in how my code may be structured and my lack of understanding how/when _process vs _physics_process are ran. The fact that it is so inconsistent makes me feel like it’s a hardware issue but I tried my best to rule that out. I’ve been trying to use the profiler tool but all I can see is that for some reason a bunch of physics stuff is getting piled up to the max_physics_steps_per_frame.

class_name Player extends CharacterBody3D

@onready var visual_mesh: MeshInstance3D = $visual_mesh
@onready var collision_mesh: CollisionShape3D = $collision_mesh

var camera_input: Vector2
var rotational_velocity: Vector2
var _yaw_input
var _pitch_input
var SMOOTHNESS = 15
var mouse_sens = 0.7
var _mouse_rotation: Vector3
var _player_rotation: Vector3
var _camera_rotation: Vector3
@onready var CAMERA_CONTROLLER = $camera_rig
@onready var camera_yaw = $camera_rig/Camera_Yaw
@onready var camera_pitch = $camera_rig/Camera_Yaw/Camera_Pitch

var gravity = 9.8
var JUMP_VELOCITY = 5.0
var SPEED = 6.0

func _ready():
    Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)

func _unhandled_input(event):
    
    if event is InputEventMouseMotion:
        camera_input = event.relative
        
    if event.is_action_pressed("exit"):
        get_tree().quit()

func _physics_process(delta):
    movement(delta)

func _process(delta):
    update_camera(delta)

func update_camera(delta):
    rotational_velocity = lerp(rotational_velocity, camera_input, delta * SMOOTHNESS)
    
    _yaw_input = -rotational_velocity.x *  mouse_sens
    _pitch_input = -rotational_velocity.y * mouse_sens
    
    _mouse_rotation.x += _pitch_input * delta
    _mouse_rotation.x = clamp(_mouse_rotation.x, deg_to_rad(-90.0), deg_to_rad(90.0))
    _mouse_rotation.y += _yaw_input * delta
    
    _player_rotation = Vector3(0.0,_mouse_rotation.y,0.0)
    _camera_rotation = Vector3(_mouse_rotation.x,0.0,0.0)

    camera_pitch.transform.basis = Basis.from_euler(_camera_rotation)
    camera_yaw.transform.basis = Basis.from_euler(_player_rotation)
    
    CAMERA_CONTROLLER.rotation.z = 0.0

    camera_input = Vector2.ZERO


func movement(delta):
    if not is_on_floor():
        velocity.y -= gravity * delta

    if Input.is_action_just_pressed("jump") and is_on_floor():
        velocity.y = JUMP_VELOCITY

    var input_dir = Input.get_vector( "move_right", "move_left", "move_backward","move_forward")
    var direction = (camera_yaw.transform.basis * Vector3(input_dir.x, 0, input_dir.y)).normalized()
    if direction:
        velocity.x = direction.x * SPEED
        velocity.z = direction.z * SPEED
    else:
        velocity.x = move_toward(velocity.x, 0, SPEED)
        velocity.z = move_toward(velocity.z, 0, SPEED)

    move_and_slide()