Climb in more directions

This commit is contained in:
2025-10-29 19:39:15 +01:00
parent 1ab2c06336
commit 97ccb2a9ac

View File

@ -23,7 +23,7 @@ var nearby_grips: Array[GripArea3D] = []
@export var climb_speed: float = 2.0
@export var grip_handover_distance: float = 1 # How close to next grip to initiate handover
@export var climb_acceleration: float = 10.0 # How quickly pawn reaches climb_speed
@export var climb_angle_threshold_deg: float = 60.0 # How wide the forward cone is
@export var climb_angle_threshold_deg: float = 80.0 # How wide the forward cone is
var climb_direction_world: Vector3 = Vector3.ZERO # Direction currently climbing
var next_grip_target: GripArea3D = null # The grip we are trying to transition to
@ -76,11 +76,8 @@ func process_movement(delta: float, move_input: Vector2, reach_input: PlayerCont
_process_reaching(delta)
MovementState.GRIPPING:
_process_gripping(delta, move_input)
if release_input.pressed or release_input.held:
_release_current_grip()
MovementState.CLIMBING:
_process_climbing(delta)
_process_climbing(delta, move_input)
MovementState.CHARGING_LAUNCH:
_handle_launch_charge(delta)
@ -112,7 +109,7 @@ func _update_state(
_delta: float,
move_input: Vector2,
reach_input: PlayerController3D.KeyInput,
_release_input: PlayerController3D.KeyInput,
release_input: PlayerController3D.KeyInput,
):
match current_state:
MovementState.IDLE:
@ -127,14 +124,14 @@ func _update_state(
_cancel_reach()
MovementState.GRIPPING:
# print("ZeroGMovementComponent: Gripping State Active")
if not is_instance_valid(current_grip):
if release_input.pressed or release_input.held or not is_instance_valid(current_grip):
_release_current_grip()
# Pawn's main state machine will handle transition out
if move_input != Vector2.ZERO:
_start_climb(move_input)
pass
MovementState.CLIMBING:
if not is_instance_valid(current_grip):
if release_input.pressed or release_input.held or not is_instance_valid(current_grip):
_stop_climb(true) # Release grip and stop
return
if move_input == Vector2.ZERO: # Player stopped giving input
@ -183,15 +180,7 @@ func _process_gripping(delta: float, _move_input: Vector2):
# Calculate the final target position with the offset
var grip_in_direction = grip_base_transform.basis.z.normalized()
var grip_up_vector = grip_base_transform.basis.y.normalized()
var grip_down_vector = -grip_base_transform.basis.y.normalized()
var pawn_up_vector = pawn.global_transform.basis.y
var dot_up = pawn_up_vector.dot(grip_up_vector)
var dot_down = pawn_up_vector.dot(grip_down_vector)
var chosen_orientation_up_vector = grip_up_vector if dot_up >= dot_down else grip_down_vector
var chosen_basis = Basis.looking_at(-grip_in_direction, chosen_orientation_up_vector).orthonormalized()
var chosen_basis = _choose_grip_orientation(current_grip.global_basis)
var target_position = grip_base_transform.origin + grip_in_direction * _get_hold_distance()
# --- End Offset Calculation ---
@ -206,37 +195,35 @@ func _process_gripping(delta: float, _move_input: Vector2):
# while the physics/orientation logic stops the main body's momentum.
func _process_climbing(delta: float):
func _process_climbing(delta: float, move_input: Vector2):
if not is_instance_valid(pawn) or not is_instance_valid(current_grip):
_stop_climb(true) # Safety check
return
var climb_direction = move_input.y * pawn.global_basis.y + move_input.x * pawn.global_basis.x
climb_direction = climb_direction.normalized()
# 1. Accelerate towards climb speed in the climb direction
var target_velocity = climb_direction_world * climb_speed
var target_velocity = climb_direction * climb_speed
pawn.velocity = pawn.velocity.lerp(target_velocity, delta * climb_acceleration)
pawn.angular_velocity = pawn.angular_velocity.lerp(Vector3.ZERO, delta * gripping_angular_damping) # Dampen spin while climbing
# 2. Find the next potential grip in the climb direction
next_grip_target = _find_climb_target_grip(climb_direction_world) # Use world direction
next_grip_target = _find_best_grip(climb_direction) # Use world direction
# 3. Check for Handover
if is_instance_valid(next_grip_target):
var next_grip_pos = next_grip_target.global_position # Use grip's actual position for distance check
var dist_sq_to_next = pawn.global_position.distance_squared_to(next_grip_pos)
if dist_sq_to_next < grip_handover_distance:
_perform_grip_handover()
_perform_grip_handover()
# 4. (Optional) Maintain Orientation relative to current grip or next target?
# Keep simple for now: orient towards current grip transform
var target_transform = current_grip.global_transform
# Only lerp origin slightly to allow movement, prioritize basis slerp
pawn.global_transform.origin = pawn.global_transform.origin.lerp(target_transform.origin + target_transform.basis.z * _get_hold_distance(), delta * reach_speed * 0.5) # Slower position lerp
pawn.global_transform.basis = pawn.global_transform.basis.slerp(target_transform.basis, delta * gripping_orient_speed)
# pawn.global_transform.origin = pawn.global_transform.origin.lerp(target_transform.origin + target_transform.basis.z * _get_hold_distance(), delta * reach_speed * 0.5) # Slower position lerp
pawn.global_transform.basis = pawn.global_transform.basis.slerp(_choose_grip_orientation(current_grip.global_basis), delta * gripping_orient_speed)
# --- Grip Helpers
# Attempts to find and grab the best available grip within range
func _try_initiate_reach():
var closest_grip: GripArea3D = _find_closest_available_grip()
var closest_grip: GripArea3D = _find_best_grip()
if is_instance_valid(closest_grip):
current_grip = closest_grip
@ -248,41 +235,68 @@ func _try_initiate_reach():
print("ZeroGMovementComponent: No available grips in range to initiate reach.")
# TODO: Initiate generic surface grab?
func _find_closest_available_grip() -> GripArea3D:
var closest_grip: GripArea3D = null
var closest_distance: float = INF
for grip: GripArea3D in nearby_grips:
if grip.is_occupied():
continue
var distance = pawn.global_transform.origin.distance_squared_to(grip.global_transform.origin)
if distance < closest_distance:
closest_distance = distance
closest_grip = grip
return closest_grip
# --- Grip Orientation Helper ---
func _choose_grip_orientation(grip_basis: Basis) -> Basis:
var grip_up_vector = grip_basis.y.normalized()
var grip_down_vector = -grip_basis.y.normalized()
var pawn_up_vector = pawn.global_transform.basis.y
# --- Modify _find_climb_target_grip to accept world direction ---
func _find_climb_target_grip(world_direction: Vector3) -> GripArea3D:
var dot_up = pawn_up_vector.dot(grip_up_vector)
var dot_down = pawn_up_vector.dot(grip_down_vector)
var chosen_orientation_up_vector = grip_up_vector if dot_up >= dot_down else grip_down_vector
return Basis.looking_at(-grip_basis.z.normalized(), chosen_orientation_up_vector).orthonormalized()
# --- Grip Selection Logic ---
# Finds the best grip based on direction, distance, and angle constraints
func _find_best_grip(direction := Vector3.ZERO, max_distance_sq := INF, angle_threshold_deg := 120.0) -> GripArea3D:
var best_grip: GripArea3D = null
var min_dist_sq = INF
var desired_dir = world_direction.normalized()
var min_dist_sq = max_distance_sq # Start checking against max allowed distance
var angle_threshold_rad = deg_to_rad(climb_angle_threshold_deg)
var dot_threshold = cos(angle_threshold_rad / 2.0)
var use_direction_filter = direction != Vector3.ZERO
var max_allowed_angle_rad = 0.0 # Initialize
if use_direction_filter:
# Calculate the maximum allowed angle deviation from the center direction
max_allowed_angle_rad = deg_to_rad(angle_threshold_deg) / 2.0
# Iterate through all grips detected by the pawn
for grip in nearby_grips:
# Basic validity checks
if not is_instance_valid(grip) or grip == current_grip or not grip.can_grab(pawn):
continue
var grip_pos = grip.global_position
# Use direction_to which automatically normalizes
var dir_to_grip = pawn.global_position.direction_to(grip_pos)
var dist_sq = pawn.global_position.distance_squared_to(grip_pos)
# Check distance first
if dist_sq >= min_dist_sq: # Use >= because we update min_dist_sq later
continue
# If using direction filter, check angle constraint
if use_direction_filter:
# Ensure the direction vector we compare against is normalized
var normalized_direction = direction.normalized()
# Calculate the dot product
var dot = dir_to_grip.dot(normalized_direction)
# Clamp dot product to handle potential floating-point errors outside [-1, 1]
dot = clamp(dot, -1.0, 1.0)
# Calculate the actual angle between the vectors in radians
var angle_rad = acos(dot)
# Check if the calculated angle exceeds the maximum allowed deviation
if angle_rad > max_allowed_angle_rad:
# print("Grip ", grip.get_parent().name, " outside cone. Angle: ", rad_to_deg(angle_rad), " > ", rad_to_deg(max_allowed_angle_rad))
continue # Skip this grip if it's outside the cone
# If it passes all filters and is closer than the previous best:
min_dist_sq = dist_sq
best_grip = grip
if is_instance_valid(best_grip):
print("Best grip found: ", best_grip.get_parent().name, " at distance squared: ", min_dist_sq)
# Check if the grip is roughly in the desired direction
var dot = dir_to_grip.dot(desired_dir)
if dot > dot_threshold: # Is it within the forward cone?
var dist_sq = pawn.global_position.distance_squared_to(grip_pos)
if dist_sq < min_dist_sq:
min_dist_sq = dist_sq
best_grip = grip
return best_grip
# --- Helper for Hold Distance ---
@ -313,8 +327,8 @@ func _start_climb(move_input: Vector2):
current_state = MovementState.CLIMBING
# Calculate initial climb direction based on input relative to camera/grip
var pawn_up = pawn.global_transform.basis.y
var pawn_right = pawn.global_transform.basis.x
var pawn_up = pawn.global_basis.y
var pawn_right = pawn.global_basis.x
# Project input onto plane perpendicular to grip normal? Or just use camera space?
# Let's use camera space initially for simplicity