How to tell where my Character will land

Godot Version

v4.2.1

Question

Hello,

I am making a rollerblading game right now and when my character jumps off a cliff or a ramp, I want him to slowly rotate toward the normal of where I will land (Basically like the skate games). I know how to rotate my character to align with whatever the normal is below me but I am having trouble trying to figure out where I will land.

I would just shoot a raycast straight forward and rotate toward whatever normal I hit, but because of the nature of ramps and other things that might be in the way of that straight line it doesn’t work, so I need a way to find the trajectory of my flight path to the spot where I will land so that I can cast a raycast at that point and find the normal to rotate my character to match the landing spot slowly.

Below is my player movement script if that helps. Any help is greatly appreciated


class_name Player
extends CharacterBody3D

@onready var ray = $FloorRayCast
@onready var test = $Camera_Mount

# Jump
var JUMP_VELOCITY = 0 # The actual jump height of the player
var MAX_JUMP = 20 # Maximum height the player will jump
var MIN_JUMP = 14 # Minimum height the player will jump
var jump_timer = 15 # Speed at which the jump velocity rises when holding space

# Movement
var MOVESPEED = 0.0 # current speed for the player
var max_movespeed = 15.0 # max move speed
var speed_acceleration = 0.1 # Speed acceleration to slowly lerp the speed to max speed
var speed_deceleration = 0.001 # Speed deceleration to slowly lerp the speed to 0

# Rotation
var rot_speed = 0.0 # player left and right rotate speed based on A & D input

# Player States
var grind_state = false # Tells weather or not the player is grinding

# Get the gravity from the project settings to be synced with RigidBody nodes.
var gravity = ProjectSettings.get_setting("physics/3d/default_gravity")

var testBool = false
var startHeight
#var current_scene = get_tree().get_current_scene()
#var current_scene = get_root().get_node("TestWorld")
func _ready():
	pass
	
func _physics_process(delta):
	
	#Clamps the velocity so the player doesnt go spinning off into the ether
	velocity.z = clamp(velocity.z, -max_movespeed, max_movespeed)
	velocity.x = clamp(velocity.x, -max_movespeed, max_movespeed)
	
	# Moves forward if the raycast is colliding with the ground or if grinding(This is so the player can jump off the rail in the direction hes facing
	if is_on_floor() or grind_state == true:
		velocity = velocity.move_toward(-global_transform.basis.z * MOVESPEED, 2) # Move the player forward based on facing direction


	# The normal below the player
	var normal = $FloorRayCast.get_collision_normal() # Grabs the normal below

	#Finds the differnce between angles to stand the player upright depending on the normal below
	var newUp = Basis()
	newUp.x = normal.cross(global_transform.basis.z)
	newUp.y = normal
	newUp.z = global_transform.basis.x.cross(normal)
	global_transform.basis = newUp
	scale = Vector3(1,1,1) #Without this the player disappears

	# Add the gravity.
	#Gravity for when the player is not on the ground and not grinding
	if not is_on_floor() and grind_state == false:
		velocity.y -= gravity * 2 * delta
	# Gravity for when the player is on the ground
	# This is so the player sticks to ramps when going down them easier
	if normal != Vector3(0, 1, 0) and is_on_floor():
		velocity.y -= gravity * 8 * delta
		
	# Handles Jump.
	# Starts the jumping build up if the player is on the ground and clicks space or if grinding
	if Input.is_action_pressed("ui_accept") and is_on_floor() or grind_state == true: # and jump_state == true:	
		JUMP_VELOCITY += jump_timer * delta # ramps up the jump velicity while holding down space
		JUMP_VELOCITY = clamp(JUMP_VELOCITY, MIN_JUMP, MAX_JUMP) # Stops the player from jumping too high
	if Input.is_action_just_released("ui_accept") and is_on_floor(): # Actually does the jump when space is released
		_player_jump.call()
		JUMP_VELOCITY = MIN_JUMP # Resets the jump velocity
		
	#Left and right rotation input
	rot_speed = 0 #Resets to 0 every frame to stop the player from spinning on its own
	if Input.is_action_pressed("ui_left"):
		#player_turn_obj.rotate(Vector3(player_turn_obj.rotation.x, rot_speed, player_turn_obj.rotation.z).normalized(), 0.03)
		rot_speed += 0.05
	if Input.is_action_pressed("ui_right"):
		#player_turn_obj.rotate(Vector3(player_turn_obj.rotation.x, -rot_speed, player_turn_obj.rotation.z).normalized(), 0.03)
		rot_speed -= 0.05
	if Input.is_action_pressed("ui_left") or Input.is_action_pressed("ui_right"):
		# Had to put this in here to stop the player from rotating after input was let go
		rotate_object_local(Vector3.UP, rot_speed) # Rotates the player based on its up direction
	
	#Handles forward speed
	if Input.is_action_pressed("ui_up"):
		
		MOVESPEED = lerp(MOVESPEED, max_movespeed, speed_acceleration) #Lerps to max speed
		
		#anim_tree.set("parameters/BlendSpace1D/blend_position", velocity.length()) # Test skate animation
		#if grind_state == false:
		#anim_tree.set("parameters/BlendSpace2D/blend_position", Vector2(0, 1)) # Test skate animation
	
	else:
			MOVESPEED = lerp(MOVESPEED, 0.0, speed_deceleration) #Lerps back down to 0 when no input is made
	
	move_and_slide()
	
# Players jump function, called in the grinding script
func _player_jump():
	#velocity.y = JUMP_VELOCITY
	velocity += get_floor_normal() * JUMP_VELOCITY

func _player_grind():
	pass

What did you end up figuring out, here?

#Determines where the player will land when airborne
func _GroundPrediction(delta):
	var max_points = 300 #The distance of how far the prediction will go
	var exclude_body: Array[RID] = [player.get_rid()] #Tells the prediction rays what to ignore ### UPDATE THIS WHEN ENEMIES OR OTHER OBJECTS ARE ADDED ###
	var pos = player.global_position #Tells the prediction to start at the player
	var vel = player.velocity #Tells the prediction which way the player is going
	var collision # Creates a variable that holds prediction collision information
	for i in max_points: # The loop that iterates the predictions distance and stops when a collision is made
		if predictReset == true: # Stops the script from continueing if the player is already airborne or from going past the point of the collision that is made
			vel.y -= (player.gravity * 2) * delta # Copies the players movement velocity and predicts where it will go based on gravity and direction
			pos += vel * delta # The players future positions while airborne
			#Grabs the world state and projects a ray for each position to find where the player will land
			var direct_state = player.get_world_3d().direct_space_state
			collision = direct_state.intersect_ray(PhysicsRayQueryParameters3D.create(pos + Vector3(0,1,0), pos + Vector3(0,-0.1,0), 3, exclude_body))
			# Visuals for testing
			##var test = test_capsule.instantiate() 
			##add_child(test)
			##test.global_position = pos + Vector3(0,1,0)
		# Stops the prediction if a collision is made
		if collision and predictReset == true: # Plays if a collision is made and will only grab the collision information once
			##var test2 = test_capsule.instantiate()
			##add_child(test2)
			##test2.global_position = collision["position"]
			player.normal = collision["normal"]
			print(collision["normal"])
			##print(collision["position"])
			predictReset = false # Stops the prediction position update and resets for when the player is airborne again
			break

And I rotate the player to the normal with this

	#Finds the differnce between angles to stand the player upright depending on the normal below
	var newUp = Basis()
	newUp.x = normal.cross(global_transform.basis.z)
	newUp.y = normal
	newUp.z = global_transform.basis.x.cross(normal)
	global_transform.basis = newUp
	scale = Vector3(1,1,1) #Without this the player disappears

Its a little bit buggy when I land on ramps that are curving inward but it works for what I need right now