Recoil lerp suggestion

I am really enjoying this course a lot! Praise to Bram and the team for plowing through the latest lectures so reliably! :star_struck:

I do have a remark about the recoil implementation, though.

It doesn’t matter too much, but I think the way the lerp is applied for the recoil recovery is not ideal. On second thought I came up with 5 points I don’t like:

  • lerping C between C and B, instead of between A and B
  • … with a rather chaotic lerp ratio argument not clamped between 0 and 1;
  • using delta in _process but still making assumptions about the frame rate;
  • applying the lerp unconditionally in _process, even outside of the cooldown; and
  • starting the new recoil translation from the current, possibly wrong starting position. (Of course we might want randomness, but hopefully with controlled parameters!)

I’d file the first one under “code smells” - a sign there is something wrong, unless you have a very good reason to do it.
If we would run into bad frame rates, a huge delta * 10 value could even throw the weapon much further forward than its original position, and it could possibly never stop jittering around.

Assuming we want recoil recovery between positions A and B, such that it finishes within the fire period, this is my solution (probably also suffering from some issues) for hitscan_weapon.gd at the end of this lecture:

extends Node3D

@export var fire_rate := 14.0
@export var recoil := 0.05
@export var weapon_mesh: Node3D

@onready var ray_cast: RayCast3D = $RayCast3D
@onready var cooldown_timer: Timer = $CooldownTimer
@onready var weapon_position: Vector3 = weapon_mesh.position
var recoiled_weapon_position: Vector3

func _ready() -> void:
	recoiled_weapon_position = weapon_position
	recoiled_weapon_position.z += recoil
	# so we don't miss final weapon position change
	cooldown_timer.timeout.connect(update_weapon_position)

func _process(delta: float) -> void:
	if is_cooling_down():
		# update recoil visual
		update_weapon_position()
	else:
		if Input.is_action_pressed("fire"):
			shoot()

func is_cooling_down() -> bool:
	return !cooldown_timer.is_stopped()

func get_cooldown_left_ratio() -> float:
	if !is_cooling_down():
		return 0
	# ratio is current time left divided by timer starting value, which would be: 
	# return cooldown_timer.left / (1.0 / fire_rate)
	# so:
	return cooldown_timer.time_left * fire_rate
	
func shoot() -> void:
	cooldown_timer.start(1.0 / fire_rate)
	update_weapon_position()
	if ray_cast.is_colliding():
		print("hit object: " + str(ray_cast.get_collider()))
	
func update_weapon_position() -> void:
	var ratio = get_cooldown_left_ratio()
	# having ratio explicit would make it simple to apply any easing you like, like this:
	# ratio = pow(ratio, 3)
	
	# so we can verify that we go from 1.0 to 0.0 for each shot:
	print(ratio)
	weapon_mesh.position = lerp(weapon_position, recoiled_weapon_position, ratio)

(refers to lecture Recoil and Raycasts of the Complete Godot 3D course)

@Marc_Carlyon thanks for fixing the tags! :bowing_man: :clap:

1 Like

Privacy & Terms