WIP Climbing movement
This commit is contained in:
@ -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")
|
||||
|
||||
Reference in New Issue
Block a user