Bouncing problems

Godot Version

4.3

Question

hey, im making an fps controller and i wanna make it so you bounce slightly if you jump and hit a wall. the way im doing it right now is just inverting the direction.x and direction.z if you hit a wall but it only works if you're hitting the wall head on. i get why it's happening but i dont really know how to solve it, any tips?

You can use Vector3.bounce using your velocity and the normal of the collision, this will get you the new velocity reflected off the wall.

Some links that may help:

2 Likes

im pretty new to programing so the best i could do was this:

if !is_on_floor() && is_on_wall():
	bounce_velocity = velocity.bounce(wall_normal)
	velocity = bounce_velocity

it clearly doesn’t work but i can’t really figure out how or why?

The snippet you’ve shared looks good, so hard to say why it doesn’t work without more context on how your character moves, how you get the wall normal, etc

1 Like

If you post the full gd file we can help with implementation.

i can’t post the gd file directly, should i just copy all the code?

Yeah. Just copy and paste it in a code block by clicking this icon.

Alright, here’s everything!

extends CharacterBody3D

# node vars

@onready var player: CharacterBody3D = $"."

@onready var nek: Node3D = $nek
@onready var head: Node3D = $nek/head
@onready var eyes: Node3D = $nek/head/eyes
@onready var camera_3d: Camera3D = $nek/head/eyes/Camera3D

@onready var standing_colision_shape: CollisionShape3D = $StandingColisionShape
@onready var crouching_colision_shape: CollisionShape3D = $CrouchingColisionShape

@onready var ray_cast_3d: RayCast3D = $RayCast3D
@onready var ray_cast_3d_2: RayCast3D = $RayCast3D2
@onready var ray_cast_3d_3: RayCast3D = $RayCast3D3
@onready var ray_cast_3d_4: RayCast3D = $RayCast3D4

@onready var player_animation: AnimationPlayer = $nek/head/eyes/AnimationPlayer

# sound vars

@onready var jump_sfx: AudioStreamPlayer3D = $audio/sfx/jumpSFX
@onready var land_sfx: AudioStreamPlayer3D = $audio/sfx/landSFX
@onready var slide_sfx: AudioStreamPlayer3D = $audio/sfx/slideSFX
@onready var sprinting_sfx: AudioStreamPlayer3D = $audio/sfx/sprintingSFX
@onready var walking_sfx: AudioStreamPlayer3D = $audio/sfx/walkingSFX
@onready var supersprinting_sfx: AudioStreamPlayer3D = $audio/sfx/supersprintingSFX

var sfx_volume = -10.0
var sfx_off = -100.0

# speed vars

var current_speed = 5.0

@export var crouch_speed = 3.0
@export var walking_speed = 4.0
@export var sprint_speed = 9.0
@export var super_sprint_speed = 11.0

@export var mouse_sentitivity = 0.3

var lerp_speed = 8.0
var lerp_speed_slow = 2.0
var lerp_speed_air = 1.0

# states

var walking = false
var sprinting = false
var crouching = false
var freelooking = false
var sliding = false
var super_sprinting = false

var dead = false

# slide vars

var slide_timer = 0.0
var slide_timer_max = 1.0
var slide_vector = Vector2.ZERO
@export var slide_jump_velocity = 6
@export var slide_speed = 15

# head bobbing vars

const head_bobbing_sprinting_speed = 16.0
const head_bobbing_walking_speed = 14.0
const head_bobbing_crouching_speed = 10.0

const head_bobbing_sprinting_intensity = 0.3
const head_bobbing_walking_intensity = 0.1
const head_bobbing_crouching_intensity = 0.05

var head_bobbing_vector = Vector2.ZERO
var head_bobbing_index = 0.0
var head_bobbing_current_intensity = 0.0

# movement vars

var direction = Vector3.ZERO

@export var crouching_depth = -0.2

@export var jump_velocity = 4.5

var freeLookTilt = 5

var last_velocity = Vector3.ZERO

var wall_normal = get_wall_normal()

var bounce_velocity = Vector3.ZERO


func _ready() -> void:
	Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)

func _input(event: InputEvent) -> void:
	
	#mouse movement
	if dead:
		return
	if event is InputEventMouseMotion:
		if freelooking:
			nek.rotate_y(deg_to_rad(-event.relative.x * mouse_sentitivity))
			nek.rotation.y = clamp(nek.rotation.y,deg_to_rad(-120),deg_to_rad(120))
			head.rotate_x(deg_to_rad(-event.relative.y * mouse_sentitivity))
			head.rotation.x = clamp(head.rotation.x,deg_to_rad(-89),deg_to_rad(89))
		else:
			rotate_y(deg_to_rad(-event.relative.x * mouse_sentitivity))
			head.rotate_x(deg_to_rad(-event.relative.y * mouse_sentitivity))
			head.rotation.x = clamp(head.rotation.x,deg_to_rad(-89),deg_to_rad(89))


func _physics_process(delta):
	
	if !is_on_floor() && is_on_wall():
		bounce_velocity = velocity.bounce(wall_normal)
		velocity = bounce_velocity
	
	# getting movement input
	var input_dir := Input.get_vector("left", "right", "forward", "backward")
	
	## handle movement state
	
	# slide begin logic
	
	if ((sprinting or super_sprinting) && Input.is_action_just_pressed("slide") && input_dir != Vector2.ZERO && !sliding) && !is_on_wall() && is_on_floor():
			sliding = true
			freelooking = true
			slide_timer = slide_timer_max
			slide_vector = input_dir
			print("slide begin")
			head.position.y = lerp(head.position.y,crouching_depth,delta*lerp_speed)
			slide_sfx.set_volume_db(sfx_volume)
			slide_sfx.play()
			
			# crouching
	if Input.is_action_pressed("crouch"):
		
		current_speed = lerp(current_speed,crouch_speed,delta*lerp_speed)
		head.position.y = lerp(head.position.y,crouching_depth,delta*lerp_speed)
		
		standing_colision_shape.disabled = true
		crouching_colision_shape.disabled = false
		
		
		sliding = false
		walking = false
		sprinting = false
		crouching = true
		super_sprinting = false
		
	elif !ray_cast_3d.is_colliding() and !ray_cast_3d_2.is_colliding() and !ray_cast_3d_3.is_colliding() and !ray_cast_3d_4.is_colliding():
		
		# standing
		
		standing_colision_shape.disabled = false
		crouching_colision_shape.disabled = true
		
		head.position.y = lerp(head.position.y,0.0,delta*lerp_speed)
		
		## handle sprinting
		
		if Input.is_action_pressed("sprint"):
			
				# super sprinting
			if super_sprinting:
				current_speed = lerp(current_speed,super_sprint_speed,delta*lerp_speed)
				
				walking = false
				sprinting = false
				crouching = false
				super_sprinting = true
				
				# sprinting
			elif is_on_floor():
				current_speed = lerp(current_speed,sprint_speed,delta*lerp_speed)
				
				walking = false
				sprinting = true
				crouching = false
				super_sprinting = false
				
		# walking
		elif is_on_floor():
			current_speed = lerp(current_speed,walking_speed,delta*lerp_speed)
			
			walking = true
			sprinting = false
			crouching = false
			super_sprinting = false
	
	
	# handle freelooking
	
	if Input.is_action_pressed("freeLook") or sliding:
		freelooking = true
		eyes.rotation.z = -deg_to_rad(nek.rotation.y * freeLookTilt)
	else:
		freelooking = false
		nek.rotation.y = lerp(nek.rotation.y,0.0,delta*lerp_speed)
		eyes.rotation.z = lerp(eyes.rotation.z,0.0,delta*lerp_speed)

	# slide end logic
	
	if sliding:
		slide_timer -= delta
		standing_colision_shape.disabled = true
		crouching_colision_shape.disabled = false
		crouching = true
		if slide_timer <= 0 and is_on_floor():
			sliding = false
			freelooking = false
			print("slide end")
			
			
		elif is_on_wall() or is_on_ceiling():
			sliding = false
			freelooking = false
			super_sprinting = false
	
	if !sliding:
		slide_sfx.set_volume_db(lerp(slide_sfx.volume_db,sfx_off,delta*lerp_speed_slow))
		
		
	# handle headbobbing
	
	if !sliding:
		if sprinting or super_sprinting:
			head_bobbing_current_intensity = head_bobbing_sprinting_intensity
			head_bobbing_index += head_bobbing_sprinting_speed*delta
		elif walking:
			head_bobbing_current_intensity = head_bobbing_walking_intensity
			head_bobbing_index += head_bobbing_walking_speed*delta
		elif crouching:
			head_bobbing_current_intensity = head_bobbing_crouching_intensity
			head_bobbing_index += head_bobbing_crouching_speed*delta
		
		if is_on_floor() && !sliding && input_dir != Vector2.ZERO:
			head_bobbing_vector.y = sin(head_bobbing_index)
			head_bobbing_vector.x = sin(head_bobbing_index/2)+0.5
			
			eyes.position.y = lerp(eyes.position.y,head_bobbing_vector.y*(head_bobbing_current_intensity/2),delta*lerp_speed)
			eyes.position.x = lerp(eyes.position.x,head_bobbing_vector.x*head_bobbing_current_intensity,delta*lerp_speed)
		else:
			eyes.position.y = lerp(eyes.position.y,0.0,delta*lerp_speed)
			eyes.position.x = lerp(eyes.position.x,0.0,delta*lerp_speed)
			
	
	# handle head tilting
	
	if Input.is_action_pressed("left") && sprinting:
		camera_3d.rotation.y = lerp(camera_3d.rotation.y,deg_to_rad(2),delta*lerp_speed_slow)
		camera_3d.rotation.z = lerp(camera_3d.rotation.z,deg_to_rad(2),delta*lerp_speed_slow)
	elif Input.is_action_pressed("right") && sprinting:
		camera_3d.rotation.y = lerp(camera_3d.rotation.y,deg_to_rad(-2),delta*lerp_speed_slow)
		camera_3d.rotation.z = lerp(camera_3d.rotation.z,deg_to_rad(-2),delta*lerp_speed_slow)
	elif Input.is_action_pressed("left") && super_sprinting:
		camera_3d.rotation.y = lerp(camera_3d.rotation.y,deg_to_rad(4),delta*lerp_speed_slow)
		camera_3d.rotation.z = lerp(camera_3d.rotation.z,deg_to_rad(4),delta*lerp_speed_slow)
	elif Input.is_action_pressed("right") && super_sprinting:
		camera_3d.rotation.y = lerp(camera_3d.rotation.y,deg_to_rad(-4),delta*lerp_speed_slow)
		camera_3d.rotation.z = lerp(camera_3d.rotation.z,deg_to_rad(-4),delta*lerp_speed_slow)
	else:
		camera_3d.rotation.y = lerp(camera_3d.rotation.y,0.0,delta*lerp_speed_slow)
		camera_3d.rotation.z = lerp(camera_3d.rotation.z,0.0,delta*lerp_speed_slow)
		
	# Add the gravity.
	if Input.is_action_pressed("backward") && !is_on_floor():
		velocity += get_gravity() * delta * 3
	elif !is_on_floor():
		velocity += get_gravity() * delta
		
		# handle slide jump
	if Input.is_action_just_pressed("jump") && is_on_floor() && sliding:
		velocity.y = slide_jump_velocity
		sliding = false
		player_animation.play("slideJump")
		super_sprinting = true
		jump_sfx.play()
		
		# Handle jump.
	elif Input.is_action_just_pressed("jump") && is_on_floor() && !crouching:
		velocity.y = jump_velocity
		player_animation.play("jump")
		jump_sfx.play()
	
		# handle landing
	if is_on_floor():
		if last_velocity.y < 0.0:
			player_animation.play("land")
			land_sfx.play()
	
	# Get the input direction and handle the movement/deceleration.
	# As good practice, you should replace UI actions with custom gameplay actions.
	if is_on_floor():
		direction = lerp(direction,(transform.basis * Vector3(input_dir.x, 0, input_dir.y)).normalized(),delta * lerp_speed)
	else:
		direction = lerp(direction,(transform.basis * Vector3(input_dir.x, 0, input_dir.y)).normalized(),delta * lerp_speed_air)
	
	if sliding:
		direction = (transform.basis * Vector3(slide_vector.x,0,slide_vector.y)).normalized()
		head.position.y = lerp(head.position.y,crouching_depth,delta*lerp_speed)
		current_speed = (slide_timer + 0.5) * slide_speed
	
	if direction:
		velocity.x = direction.x * current_speed
		velocity.z = direction.z * current_speed
		
	else:
		velocity.x = move_toward(velocity.x, 0, current_speed)
		velocity.z = move_toward(velocity.z, 0, current_speed)
		
	last_velocity = velocity
	
	move_and_slide()

1 Like

Woof. That’s a big boy code block.

Looking at your code, I see you’re already implementing a bounce mechanism using velocity.bounce(wall_normal) like suggested. This method calculates the reflection of your velocity vector against the wall normal, which should give you proper bouncing in all directions.

Here’s how to improve your wall bounce implementation:

# Add a bounce factor to control the bounciness
@export var wall_bounce_factor = 0.7  # Values between 0-1, where 1 is full bounce

func _physics_process(delta):
    # Your existing code...
    
    if !is_on_floor() && is_on_wall():
        var wall_normal = get_wall_normal()
        
        # Calculate bounce with dampening factor
        bounce_velocity = velocity.bounce(wall_normal) * wall_bounce_factor
        
        # Apply the bounce velocity
        velocity = bounce_velocity
        
        # Optional: Add a small upward boost on wall hit for better game feel
        if velocity.y < 0:  # Only if moving downward
            velocity.y += 2.0  # Small upward boost

You might also want to add a small cooldown to prevent multiple bounces happening in quick succession. This is just what I would do as a suggestions, Not really required.

var can_bounce = true
var bounce_cooldown = 0.1  # Time in seconds

func _physics_process(delta):
    # Update cooldown
    if !can_bounce:
        bounce_cooldown -= delta
        if bounce_cooldown <= 0:
            can_bounce = true
    
    if !is_on_floor() && is_on_wall() && can_bounce:
        # Do bounce calculation
        
        # Set cooldown
        can_bounce = false
        bounce_cooldown = 0.1

The velocity.bounce() method should work with walls at any angle, not just head-on collisions. If you’re still having issues, make sure your collision detection is working correctly and that get_wall_normal() is returning accurate values, Ill try and read through the docs more later and see if I’m forgetting anything important. Hope this helps!

hmm, it’s still not working. im wondering if im messing with the velocity somewhere else and that might mess it up?

i debugged it a bit and it seems the only thing that doesn’t work is the actual changing of the velocity. if i say print(“bounce”) when i should bounce, it prints bounce.