Recoil failing to correctly rebound when manual compensation is at play

I have a recoil script that brings the mouse down the amount it recoiled when done firing, this however breaks when trying to manually compensate for the recoil. In this case, the camera is moved down farther than it was intended.
Here’s the code:
Recoil Object

extends Node3D

#Rotations
var current_rotation : Vector3
var target_rotation : Vector3

#Recoil vectors
var recoil : Vector3
var aim_recoil : Vector3

#Settings
var snap : float
var return_speed : float

func _process(delta):
	# Lerp target rotation to (0,0,0) and lerp current rotation to target rotation
	target_rotation = lerp(target_rotation, Vector3.ZERO, return_speed * delta)
	current_rotation = lerp(current_rotation, target_rotation, snap * delta)
	
	# Set rotation
	rotation = current_rotation
	
	# Camera z axis tilt fix, ignored if tilt intentional
	# I have no idea why it tilts if recoil.z is set to 0
	if recoil.z == 0 and aim_recoil.z == 0:
		global_rotation.z = 0

func fire_recoil(is_aiming : bool = false):
	if is_aiming:
		target_rotation += Vector3(aim_recoil.x, randf_range(-aim_recoil.y, aim_recoil.y), randf_range(-aim_recoil.z, aim_recoil.z))
	else:
		target_rotation += Vector3(recoil.x, randf_range(-recoil.y, recoil.y), randf_range(-recoil.z, recoil.z))

func set_recoil(new_recoil : Vector3):
	recoil = new_recoil

func set_aim_recoil(new_recoil : Vector3):
	aim_recoil = new_recoil
	
func set_recoil_settings(new_snap:float, new_return_speed:float):
	snap = new_snap
	return_speed = new_return_speed

Class

class_name Gun extends Node3D

#Signals
signal fired
signal reload

#Refrences
@onready var recoil_handler = get_tree().get_first_node_in_group("recoil")
@onready var animation_player:AnimationPlayer = get_tree().get_first_node_in_group("gun_anim_player")

#Exports
@export_group("Ammo")
@export var mag_size:int

#Firing Group
@export_group("Firing")
#Recoil Vectors
@export var recoil:Vector3
@export var aim_recoil :Vector3
#Recoil Settings
@export var snap:float
@export var return_speed:float
#Auto
@export var auto:bool = true
#Animation
@export_group("Animations")
@export var anim_multi:float = 1
#Variables
@onready var ammo:int = mag_size


#Code
func _ready() -> void:
	recoil_handler.set_recoil(recoil)
	recoil_handler.set_aim_recoil(aim_recoil)
	recoil_handler.set_recoil_settings(snap, return_speed)
	animation_player.speed_scale = anim_multi

func _process(_delta: float) -> void:
	#Firing
	if auto == true:
		if Input.is_action_pressed("fired") and ammo > 0:
			fired.emit()
	else:
		if Input.is_action_just_pressed("fired") and ammo > 0:
			fired.emit()
	#Reloading
	if Input.is_action_just_pressed("reload"):
		reload.emit()
	#Updating UI
	global.game.gui.get_children()[0].text = str(ammo)+'/'+str(mag_size)

And Gun Extending The Class

extends Gun
@onready var aim:RayCast3D = $"../../Aim"
var bullet_scene = preload("res://Scenes/BulletHole.tscn")

func _on_fired() -> void:
	if not animation_player.is_playing():
		if aim.is_colliding():
			var end = aim.get_collision_point()
			var normal = aim.get_collision_normal()
			bullet_decal(end, normal)
		recoil_handler.fire_recoil()
		animation_player.stop()
		animation_player.play("gun_fire")
		ammo -= 1




func _on_reload() -> void:
	if ammo < mag_size:
		animation_player.stop()
		animation_player.play("reload")
		ammo += 1
		
func bullet_decal(end, normal):
	var bullet_hole = bullet_scene.instantiate()
	get_tree().root.add_child(bullet_hole)
	bullet_hole.position = end
	bullet_hole.look_at(bullet_hole.global_transform.origin + normal, Vector3.UP, )
	if normal != Vector3.UP and normal != Vector3.DOWN:
		bullet_hole.rotate_object_local(Vector3(1,0,0), 90)
	await get_tree().create_timer(1.5).timeout
	var fade = get_tree().create_tween()
	fade.tween_property(bullet_hole, "modulate:a", 0, 1.5)
	await get_tree().create_timer(1.5).timeout
	bullet_hole.queue_free()

And here’s the scenes image

I think you should measure the manual compensation with a variable:

var manual_compensation : Vector3 = Vector3.ZERO  # Track manual compensation

Then check it in an Event function:

func _input(event: InputEvent):
    # Check for mouse movement and apply manual compensation
    if event is InputEventMouseMotion:
        manual_compensation -= Vector3(event.relative.y, event.relative.x, 0) * return_speed

Then maybe this for your _process() function:

func _process(delta):
    # Lerp target rotation to (0,0,0) smoothly
    target_rotation = lerp(target_rotation, Vector3.ZERO, return_speed * delta)

    # Apply both manual compensation and recoil-induced rotation
    current_rotation = lerp(current_rotation, target_rotation + manual_compensation, snap * delta)

    # Set rotation
    rotation = current_rotation

This is me shooting in the dark, but maybe? Hope this helps you get going in the right direction at least.

1 Like

Thx, I’ll check this later!

Nope, my character is basically having a glorified seizure
Ill will tinker around with this code for a bit, maybe only record the input when firing idk.
Thank you.

1 Like