WIP Climbing movement

This commit is contained in:
olof.pettersson
2025-10-28 16:25:32 +01:00
parent 8e3f415cb4
commit f51672c6a9

View File

@ -10,20 +10,30 @@ var camera_pivot: Node3D
var current_grip: GripArea3D = null # Use GripArea3D type hint
var nearby_grips: Array[GripArea3D] = []
# --- Reach Parameters ---
@export var reach_speed: float = 10.0 # Speed pawn moves towards grip
@export var reach_orient_speed: float = 10.0 # Speed pawn orients to grip
@export var launch_charge_rate: float = 20.0
@export var max_launch_speed: float = 15.0
# Add export variables for damping/orientation speeds while gripping
# --- Grip damping parameters ---
@export var gripping_linear_damping: float = 5.0 # How quickly velocity stops
@export var gripping_angular_damping: float = 5.0 # How quickly spin stops
@export var gripping_orient_speed: float = 2.0 # How quickly pawn rotates to face grip
# --- Climbing parameters ---
@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
var climb_direction_world: Vector3 = Vector3.ZERO # Direction currently climbing
var next_grip_target: GripArea3D = null # The grip we are trying to transition to
# --- Launch Parameters ---
@export var launch_charge_rate: float = 20.0
@export var max_launch_speed: float = 15.0
var launch_direction: Vector3 = Vector3.ZERO
var launch_charge: float = 0.0
# Enum for internal state (distinct from pawn's main state)
# Enum for internal state
enum MovementState {
IDLE,
REACHING,
@ -65,10 +75,12 @@ func process_movement(delta: float, move_input: Vector2, reach_input: PlayerCont
_try_initiate_reach()
_process_reaching(delta)
MovementState.GRIPPING:
_handle_gripping_state(delta, move_input)
_process_gripping(delta, move_input)
if release_input.pressed or release_input.held:
_release_current_grip()
MovementState.CLIMBING:
_process_climbing(delta)
MovementState.CHARGING_LAUNCH:
_handle_launch_charge(delta)
@ -80,7 +92,7 @@ func handle_collision(collision: KinematicCollision3D, collision_energy_loss: fl
pawn.velocity = pawn.velocity.bounce(surface_normal)
pawn.velocity *= (1.0 - collision_energy_loss * 0.5)
## Called by Pawn when entering a state managed by this controller
# === STATE MACHINE
func _on_enter_state(state : MovementState):
print("ZeroGMovementComponent activated for state: ", MovementState.keys()[state])
if state == MovementState.GRIPPING:
@ -89,7 +101,6 @@ func _on_enter_state(state : MovementState):
# else: # e.g., REACHING_MOVE?
# state = MovementState.IDLE # Or SEARCHING?
## Called by Pawn when exiting a state managed by this controller
func _on_exit_state(state: MovementState):
print("ZeroGMovementComponent deactivated for state: ", MovementState.keys()[state])
@ -97,11 +108,9 @@ func _on_exit_state(state: MovementState):
# if state == MovementState.GRIPPING:
# _release_current_grip()
# --- Internal Logic ---
func _update_state(
_delta: float,
_move_input: Vector2,
move_input: Vector2,
reach_input: PlayerController3D.KeyInput,
_release_input: PlayerController3D.KeyInput,
):
@ -121,9 +130,18 @@ func _update_state(
if 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:
# print("ZeroGMovementComponent: Climbing State Active")
pass
if not is_instance_valid(current_grip):
_stop_climb(true) # Release grip and stop
return
if move_input == Vector2.ZERO: # Player stopped giving input
_stop_climb(false) # Stop moving, return to GRIPPING
return
# Continue climbing logic (finding next grip) happens in _process_climbing
# elif move_input != Vector2.ZERO:
# _start_charge(move_input) # Start charging launch
# MovementState.CHARGING_LAUNCH:
@ -135,6 +153,88 @@ func _update_state(
# print("ZeroGMovementComponent: Cancelled Launch Charge")
# === MOVEMENT PROCESSING ===
func _process_reaching(_delta: float):
_try_initiate_reach()
# TODO: Drive IK target towards current_grip.get_grip_transform().origin
# TODO: Monitor distance / animation state
# When close enough: state = MovementState.GRIPPING
pass
func _process_gripping(delta: float, _move_input: Vector2):
if not is_instance_valid(pawn) or not is_instance_valid(current_grip): # Safety check
_cancel_reach() # Transition out if grip or pawn is invalid
return
# 1. Dampen Existing Motion
pawn.velocity = pawn.velocity.lerp(Vector3.ZERO, delta * gripping_linear_damping)
pawn.angular_velocity = pawn.angular_velocity.lerp(Vector3.ZERO, delta * gripping_angular_damping)
# Stop completely if very slow, prevents jitter
if pawn.velocity.length_squared() < 0.001:
pawn.velocity = Vector3.ZERO
if pawn.angular_velocity.length_squared() < 0.001:
pawn.angular_velocity = Vector3.ZERO
# 2. Move Pawn Towards Grip Position (lerp)
var grip_base_transform = current_grip.get_grip_transform(pawn.global_position)
# --- Calculate Offset ---
# 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 target_position = grip_base_transform.origin + grip_in_direction * _get_hold_distance()
# --- End Offset Calculation ---
pawn.global_transform.origin = pawn.global_transform.origin.lerp(target_position, delta * reach_speed)
# 3. Orient Pawn Towards Grip Orientation (slerp)
# Make the pawn smoothly rotate to match the grip's desired orientation
pawn.global_transform.basis = pawn.global_transform.basis.slerp(chosen_basis, delta * gripping_orient_speed)
# TODO: Later, replace step 2 and 3 with IK driving the hand bone to the target_transform.origin,
# while the physics/orientation logic stops the main body's momentum.
func _process_climbing(delta: float):
if not is_instance_valid(pawn) or not is_instance_valid(current_grip):
_stop_climb(true) # Safety check
return
# 1. Accelerate towards climb speed in the climb direction
var target_velocity = climb_direction_world * 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
# 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()
# 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)
# --- Grip Helpers
func _try_initiate_reach():
var closest_grip: GripArea3D = _find_closest_available_grip()
@ -154,70 +254,104 @@ func _find_closest_available_grip() -> GripArea3D:
for grip: GripArea3D in nearby_grips:
if grip.is_occupied():
continue
var distance = pawn.global_transform.origin.distance_to(grip.global_transform.origin)
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
func _process_reaching(_delta: float):
_try_initiate_reach()
# TODO: Drive IK target towards current_grip.get_grip_transform().origin
# TODO: Monitor distance / animation state
# When close enough: state = MovementState.GRIPPING
pass
# --- Modify _find_climb_target_grip to accept world direction ---
func _find_climb_target_grip(world_direction: Vector3) -> GripArea3D:
var best_grip: GripArea3D = null
var min_dist_sq = INF
var desired_dir = world_direction.normalized()
# --- Modify _handle_gripping_state ---
func _handle_gripping_state(delta: float, _move_input: Vector2):
if not is_instance_valid(pawn) or not is_instance_valid(current_grip): # Safety check
_cancel_reach() # Transition out if grip or pawn is invalid
return
var angle_threshold_rad = deg_to_rad(climb_angle_threshold_deg)
var dot_threshold = cos(angle_threshold_rad / 2.0)
# 1. Dampen Existing Motion
pawn.velocity = pawn.velocity.lerp(Vector3.ZERO, delta * gripping_linear_damping)
pawn.angular_velocity = pawn.angular_velocity.lerp(Vector3.ZERO, delta * gripping_angular_damping)
for grip in nearby_grips:
if not is_instance_valid(grip) or grip == current_grip or not grip.can_grab(pawn):
continue
# Stop completely if very slow, prevents jitter
if pawn.velocity.length_squared() < 0.001:
pawn.velocity = Vector3.ZERO
if pawn.angular_velocity.length_squared() < 0.001:
pawn.angular_velocity = Vector3.ZERO
var grip_pos = grip.global_position
var dir_to_grip = pawn.global_position.direction_to(grip_pos)
# 2. Move Pawn Towards Grip Position (lerp)
var grip_base_transform = current_grip.get_grip_transform(pawn.global_position)
# 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
# --- Calculate Offset ---
# Get the pawn's reach radius (assuming GripDetector has a SphereShape3D)
var reach_offset = Vector3(0, 0 , -1) # Default value
if pawn.grip_detector:
reach_offset = pawn.grip_detector.position
# Calculate the final target position with the offset
var grip_in_direction = grip_base_transform.basis.z.normalized()
# --- Helper for Hold Distance ---
func _get_hold_distance() -> float:
# Use the pawn.grip_detector.position.length() method if you prefer that:
if is_instance_valid(pawn) and is_instance_valid(pawn.grip_detector):
return pawn.grip_detector.position.length()
else:
return 0.5 # Fallback distance if detector isn't set up right
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
func _release_current_grip():
if is_instance_valid(current_grip):
current_grip.release(pawn)
current_grip = null
current_state = MovementState.IDLE
print("ZeroGMovementComponent: Released grip and returned to FLOATING state.")
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()
func _cancel_reach():
# TODO: Logic to stop IK/animation if reach is cancelled mid-way
_release_current_grip() # Ensure grip reference is cleared
current_state = MovementState.IDLE
print("ZeroGMovementComponent: Reach cancelled.")
var hold_distance = reach_offset.length()
var target_position = grip_base_transform.origin + grip_in_direction * hold_distance
# --- End Offset Calculation ---
# --- Climbing Helpers ---
func _start_climb(move_input: Vector2):
if not is_instance_valid(current_grip): return
current_state = MovementState.CLIMBING
pawn.global_transform.origin = pawn.global_transform.origin.lerp(target_position, delta * reach_speed)
# 3. Orient Pawn Towards Grip Orientation (slerp)
# Make the pawn smoothly rotate to match the grip's desired orientation
pawn.global_transform.basis = pawn.global_transform.basis.slerp(chosen_basis, delta * gripping_orient_speed)
# 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
# TODO: Later, replace step 2 and 3 with IK driving the hand bone to the target_transform.origin,
# while the physics/orientation logic stops the main body's momentum.
# Project input onto plane perpendicular to grip normal? Or just use camera space?
# Let's use camera space initially for simplicity
climb_direction_world = (pawn_up * move_input.y + pawn_right * move_input.x).normalized()
print("ZeroGMoveController: Started Climbing in direction: ", climb_direction_world)
func _stop_climb(release_grip: bool):
print("ZeroGMoveController: Stopping Climb. Release Grip: ", release_grip)
pawn.velocity = pawn.velocity.lerp(Vector3.ZERO, 0.5) # Apply some braking
next_grip_target = null
climb_direction_world = Vector3.ZERO
if release_grip:
_release_current_grip() # Transitions to IDLE
else:
current_state = MovementState.GRIPPING # Go back to stationary gripping
func _perform_grip_handover():
if not is_instance_valid(next_grip_target): return
print("Attempting handover to: ", next_grip_target.get_parent().name)
if next_grip_target.grab(pawn):
# Successfully grabbed the next one
if is_instance_valid(current_grip):
current_grip.release(pawn) # Release the old one
current_grip = next_grip_target # Update current grip reference
next_grip_target = null # Clear the target
print("Handover successful. New grip: ", current_grip.get_parent().name)
# Stay in CLIMBING state, velocity continues
else:
# Failed to grab next grip (e.g., became occupied)
print("Handover failed - couldn't grab next grip.")
_stop_climb(false) # Stop climbing, return to gripping previous one
# --- Launch helpers ---
func _start_charge(move_input: Vector2):
if not is_instance_valid(current_grip): return
current_state = MovementState.CHARGING_LAUNCH
@ -252,21 +386,6 @@ func _execute_launch():
print("ZeroGMovementComponent: Launched with speed ", pawn.velocity.length())
func _release_current_grip():
if is_instance_valid(current_grip):
current_grip.release(pawn)
current_grip = null
current_state = MovementState.IDLE
print("ZeroGMovementComponent: Released grip and returned to FLOATING state.")
func _cancel_reach():
# TODO: Logic to stop IK/animation if reach is cancelled mid-way
_release_current_grip() # Ensure grip reference is cleared
current_state = MovementState.IDLE
print("ZeroGMovementComponent: Reach cancelled.")
# --- Signal Handlers ---
func on_grip_area_entered(area: Area3D):
print("Area detected")