How to make a smooth camera bob system?

I already have a script, but I want the bob to always attempt to lerp to Vector3.ZERO, so it’s basically fighting against itself, like how you would make deceleration and acceleration in a movement system. And same thing once you aren’t touching the floor as well. But I’m not sure how to do that…
I’m really bad at trigonometry btw.

The current code:

class_name Player extends CharacterBody3D

#Refrences
@export_group("Refrences")
@export var head:Node3D
@export var cam:Camera3D
@export var aim:RayCast3D
@export var inventory:Inventory
@export var cam_rig:Node3D

#Stats
@export_category("Speed")
@export var walk_speed:float
@export var sprint_speed:float
@export_category("Jump")
@export var jump_power:float
@export var jump_boost:float
@export_category("Physics")
@export var ground_accelaration:float = 5
@export var ground_decelaration:float = 5
@export var air_control:float=2
@export var gravity:float = ProjectSettings.get_setting("physics/3d/default_gravity")
@export_category("Camera")
@export var bob_freq := 2.5
@export var bob_amp := 0.1
@export var base_FOV:= 80.0
@export var FOV_change := 1.5
@export var sens := 0.1

#Movement
var target_speed:float
@onready var input_dir:Vector2
var dir:Vector3
#Cam Shake
var current_rotation:Vector3
var target_rotation:Vector3
#Cam Shake Settings
var snap:float
var return_speed:float
#Cam Shake Strength
var shake_strength:Vector3
#Keep Track of Sine Wave
var bob_time:float = 0.0
#State Machine
enum states {walk, sprint}
var state:states

#Lock Mouse
func _ready():
	Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)

#Input
func _input(event):
	#Jumping
	if event:
		if event.is_action_pressed("jump") and is_on_floor():
			jump()
	#Quitting
	if event.is_action_pressed("quit"):
		get_tree().quit()
	#Character Rotation
	if event is InputEventMouseMotion:
		rotate_y(deg_to_rad(-event.relative.x * sens))
		head.rotate_x (deg_to_rad(-event.relative.y * sens))
		head.rotation.x = clamp(head.rotation.x,deg_to_rad(-90),deg_to_rad(90))

#Movement
func _physics_process(delta:float) -> void:
	#State Machine
	match state:
		states.walk:
			target_speed = walk_speed
		states.sprint:
			target_speed = sprint_speed
	#Add Gravity.
	if not is_on_floor():
		velocity.y -= gravity * delta
	#Handle Sprint.
	if Input.is_action_pressed("sprint"):
		state = states.sprint
	else:
		state = states.walk
	#Handle Movement Direction
	input_dir = Input.get_vector("left", "right", "up", "down")
	dir = transform.basis * Vector3(input_dir.x, 0, input_dir.y)
	#Accel and Deccel
	if is_on_floor():
		if dir and velocity.length() < target_speed:
			velocity.x = move_toward(velocity.x, dir.x * target_speed, delta * ground_accelaration)
			velocity.z = move_toward(velocity.z, dir.z * target_speed, delta * ground_accelaration)
		else:
			velocity.x = move_toward(velocity.x, dir.x * target_speed, delta * ground_decelaration)
			velocity.z = move_toward(velocity.z, dir.z * target_speed, delta * ground_decelaration)
	else:
		velocity.x = move_toward(velocity.x, dir.x * target_speed, delta * air_control)
		velocity.z = move_toward(velocity.z, dir.z * target_speed, delta * air_control)
	#Head Bob Process
	bob_time += delta * velocity.length()
	$"Head/Cam Rig/SpringArm3D".transform.origin = headbob(bob_time)
	#FOV
	var velocity_clamped = clamp(velocity.length(), 0.5, sprint_speed * 2)
	var target_fov = base_FOV + FOV_change * velocity_clamped
	cam.fov = lerp(cam.fov, target_fov, delta * 8.0)
	#Begin Movement
	move_and_slide()

func _process(delta:float) -> void:
	target_rotation = target_rotation.slerp(Vector3.ZERO, return_speed * delta)
	current_rotation = current_rotation.slerp(target_rotation, snap * delta)
	#Set Camera Controllers Rotation
	cam_rig.rotation = current_rotation
	#Fix Z Shake
	if shake_strength.z == 0:
		cam_rig.global_rotation.z = 0

#Head Bob Function
func headbob(time:float) -> Vector3:
	var length
	var pos := Vector3.ZERO
	if is_on_floor():
		length = min(get_real_velocity().length(), 1.0)
	else:
		length = 0
	pos.y = sin(time * bob_freq) * bob_amp * length
	pos.x = cos(time * bob_freq/2) * bob_amp * length
	return pos

#Jump Function
func jump():
	velocity.y = get_real_velocity().y + jump_power
	velocity.x += dir.x * jump_boost
	velocity.z += dir.z * jump_boost

image

As far as I can tell, your head bob system already goes to Vector3.ZERO because of the length multiplication in headbob().

Could you try to explain why your current result is unsatisfactory?
How is lerping to Vector3.ZERO any different from the current result?

1 Like

At the moment, the second I start slowing down, the bob snaps to Vector.ZERO, the same is true once I go mid air. It’s extremely jarring. I also noticed how jittering it is when I’m walking into walls, although that might be because it has a frequency too high.

I think it is lerping “smoothly” but I want it to kinda keep its previous inertia?

Kinda like it’s fighting against the want to go back to ZERO and keep bobbing.

That’s how head bobbing feels like in every AAA game i’ve played anyways…

This headbob i have is really smooth this is an example if you want me to further explain just say so…

t_bob += delta * velocity.length() * float(is_on_floor())
Camera.transform.origin = _HeadBob(t_bob)

var velocity_clamped = clamp(velocity.length(), 0.5, SPRINT_SPEED * 2)
var target_fov = BASE_FOV + FOV_CHANGE * velocity_clamped
Camera.fov = lerp(Camera.fov, target_fov, delta * 8.0)

func _process(_delta):
move_and_slide()

#Headbob
func _HeadBob(time) → Vector3:
var pos = Vector3.ZERO
pos.y = sin(time * BOB_FREQ) * BOB_AMP
pos.x = cos(time * BOB_FREQ / 2) * BOB_AMP
return pos

Here are the variables:

#Headbobbing Controls
const BOB_FREQ = 2.0
const BOB_AMP = 0.08
var t_bob = 0.0

#FOV Controls
const BASE_FOV = 75.0
const FOV_CHANGE = 1.5

2 Likes

Could you explain how yours is different than mine?
Using the built in gdscript runner in my head, it seems to function the exact same :stuck_out_tongue:

I’m in simple term sync the bob to the time/frequency of my movement, this works because your camera and player are synced in movement so by altering the frequency the camera moves we can make it follow the left and right direction of the movement i’'l make a video brb

1 Like

It’s true yours seems to be smooth, but that’s because you aren’t resetting the bob back to Vector3.ZERO.

yup, that exactly it

1 Like

Would you like a different example

1 Like

for what i’m making that’s just extra and i’m adding raycast as were speaking, with interactable objects and highlights

1 Like

PAUSE, I figured it out!!!
I stole a little of ur code and came up with this:

#Keep Track of Sine Wave
var bob_time:float = 0.0
var pos:Vector3
var length
#Trimp
var trimp:bool = true
var normal:Vector3
#Lock Mouse
func _ready():
	Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)

#Input
func _input(event):
	#Jumping
	if event:
		if event.is_action_pressed("jump") and is_on_floor():
			jump()
	#Quitting
	if event.is_action_pressed("quit"):
		get_tree().quit()
	#Character Rotation
	if event is InputEventMouseMotion:
		rotate_y(deg_to_rad(-event.relative.x * sens))
		head.rotate_x (deg_to_rad(-event.relative.y * sens))
		head.rotation.x = clamp(head.rotation.x,deg_to_rad(-89),deg_to_rad(89))

#Movement
func _physics_process(delta:float) -> void:
	#Begin Movement
	move_and_slide()
	#Handle Sprint.
	if Input.is_action_pressed("sprint"):
		target_speed = sprint_speed
	else:
		target_speed = walk_speed
	#Handle Movement Direction
	input_dir = Input.get_vector("left", "right", "up", "down")
	dir = transform.basis * Vector3(input_dir.x, 0, input_dir.y)
	#On Floor
	if is_on_floor():
		#Trimp Setup
		trimp = true
		normal = get_floor_normal() as Vector3
		#Accel and Deccel
		if dir and velocity.length() < target_speed:
			velocity.x = move_toward(velocity.x, dir.x * target_speed, delta * ground_accelaration)
			velocity.z = move_toward(velocity.z, dir.z * target_speed, delta * ground_accelaration)
		else:
			velocity.x = move_toward(velocity.x, dir.x * target_speed, delta * ground_decelaration)
			velocity.z = move_toward(velocity.z, dir.z * target_speed, delta * ground_decelaration)
	else:
		velocity.x = move_toward(velocity.x, dir.x * target_speed, delta * air_control)
		velocity.y -= gravity * delta
		velocity.z = move_toward(velocity.z, dir.z * target_speed, delta * air_control)
		if trimp and not normal.is_equal_approx(Vector3.UP):
			var boost = normal.reflect(Vector3.UP) * get_real_velocity()
			velocity += boost
			print("trimp")
		trimp = false
	#Head Bob Process
	bob_time += delta * velocity.length()
	if get_real_velocity().length() > 1.0:
		$"Head/Cam Rig/SpringArm3D".transform.origin = lerp($"Head/Cam Rig/SpringArm3D".transform.origin, headbob(bob_time) * length, 0.01)
	else:
		$"Head/Cam Rig/SpringArm3D".transform.origin = lerp($"Head/Cam Rig/SpringArm3D".transform.origin, Vector3.ZERO, 0.01)
	#FOV
	var velocity_clamped = clamp(velocity.length(), 0.5, sprint_speed * 2)
	var target_fov = base_FOV + FOV_change * velocity_clamped
	cam.fov = lerp(cam.fov, target_fov, delta * 8.0)

func _process(delta:float) -> void:
	target_rotation = target_rotation.slerp(Vector3.ZERO, return_speed * delta)
	current_rotation = current_rotation.slerp(target_rotation, snap * delta)
	#Set Camera Controllers Rotation
	cam_rig.rotation = current_rotation
	#Fix Z Shake
	if shake_strength.z == 0:
		cam_rig.global_rotation.z = 0

#Head Bob Function
func headbob(time:float) -> Vector3:
	var pos := Vector3.ZERO
	length = min(velocity.length(), 1.0)
	pos.y = sin(time * bob_freq) * bob_amp * length * float(is_on_floor())
	pos.x = cos(time * bob_freq/2) * bob_amp * length * float(is_on_floor())
	return pos

did i even help lol!

1 Like

Yep, I basically merged both systems and added lerping, thank you I had this issue for sooo long!!! :pray:

1 Like

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.