Force based EVA movement

This commit is contained in:
2025-11-16 19:07:24 +01:00
parent ec69ed2ee5
commit 398ec829ae
4 changed files with 179 additions and 219 deletions

View File

@ -29,7 +29,6 @@ var _pitch_yaw_input: Vector2 = Vector2.ZERO
@onready var zero_g_movemement_component: ZeroGMovementComponent = $ZeroGMovementComponent
## Physics State (Managed by Pawn)
# var angular_velocity: Vector3 = Vector3.ZERO
@export var angular_damping: float = 0.95 # Base damping
## Other State Variables
@ -68,10 +67,12 @@ func _physics_process(_delta: float):
_reset_inputs()
func _integrate_forces(state: PhysicsDirectBodyState3D):
# 2. Let the active movement controller apply its forces
# Let the active movement controller apply its forces
if zero_g_movemement_component:
# We pass the physics state and delta time (state.step)
zero_g_movemement_component.process_movement(state.step, _move_input, _vertical_input, _roll_input, _l_click_input, _r_click_input)
# We pass the physics state
zero_g_movemement_component.process_movement(state, _move_input, _vertical_input, _roll_input, _l_click_input, _r_click_input)
if eva_suit_component and zero_g_movemement_component.movement_state == ZeroGMovementComponent.MovementState.IDLE:
eva_suit_component.process_eva_movement(state, _move_input, _vertical_input, _roll_input, _r_click_input)
# --- Universal Rotation ---
func _apply_mouse_rotation():

View File

@ -8,12 +8,16 @@ var pawn: CharacterPawn3D
## EVA Parameters (Moved from ZeroGPawn)
@export var orientation_speed: float = 1.0 # Used for orienting body to camera
@export var linear_acceleration: float = 1.0
@export var roll_torque_acceleration: float = 2.5
@export var roll_torque_acceleration: float = 0.25
@export var angular_damping: float = 0.95 # Base damping applied by pawn, suit might add more?
@export var inertia_multiplier: float = 1.0 # How much the suit adds to pawn's base inertia (placeholder)
@export var stabilization_kp: float = 5.0
@export var stabilization_kd: float = 1.0
var _auto_orient_target: Basis = Basis() # Stores the target orientation
var _is_auto_orienting: bool = false # Flag to signal the pawn
@export var auto_orient_stop_velocity_threshold: float = 0.01 # (in rad/s)
## State
var stabilization_target: Node3D = null
var stabilization_enabled: bool = false
@ -23,46 +27,21 @@ func _ready():
if not pawn:
printerr("EVAMovementComponent must be a child of a CharacterBody3D pawn.")
return
# Make sure the paths match your CharacterPawn scene structure
# if camera_anchor:
# camera = camera_anchor.get_node_or_null("SpringArm/Camera3D") # Adjusted path for SpringArm
# if not camera_anchor or not camera:
# printerr("EVAMovementComponent could not find CameraPivot/SpringArm/Camera3D on pawn.")
## Called by Pawn's _integrate_forces when suit equipped
func process_eva_movement(state: PhysicsDirectBodyState3D, move_input: Vector2, vertical_input: float, roll_input: float, orienting_input: PlayerController3D.KeyInput):
# --- 1. Handle Orient Input ---
if orienting_input.pressed or orienting_input.held:
_set_auto_orient_target(state)
# --- Standardized Movement API ---
## Called by Pawn's _physics_process when in FLOATING state with suit equipped
func process_movement(delta: float, move_input: Vector2, vertical_input: float, roll_input: float, orienting_input: PlayerController3D.KeyInput):
var orienting = orienting_input.held
if not is_instance_valid(pawn): return
if orienting:
_orient_pawn(delta)
_process_auto_orientation(state) # [Function 2] Run the controller
# Check if stabilization is active and handle it first
if stabilization_enabled and is_instance_valid(stabilization_target):
_apply_stabilization_torques(delta)
_apply_stabilization_torques(state)
else:
# Apply regular movement/torque only if not stabilizing
_apply_floating_movement(delta, move_input, vertical_input, roll_input)
func apply_thrusters(delta: float, move_input: Vector2, vertical_input: float, roll_input: float):
if not is_instance_valid(pawn): return
# Apply Linear Velocity
var pawn_forward = -pawn.global_basis.z
var pawn_right = pawn.global_basis.x
var pawn_up = pawn.global_basis.y
var move_dir_horizontal = (pawn_forward * move_input.y + pawn_right * move_input.x)
var move_dir_vertical = pawn_up * vertical_input
var combined_move_dir = move_dir_horizontal + move_dir_vertical
if combined_move_dir != Vector3.ZERO:
pawn.apply_central_force(combined_move_dir * linear_acceleration)
# Apply Roll Torque
var roll_torque_global = -pawn.basis.z * (roll_input) * roll_torque_acceleration * delta # Sign fixed
pawn.apply_torque(roll_torque_global)
_apply_floating_movement(state, move_input, vertical_input, roll_input)
## Called by Pawn when entering FLOATING state with suit
func on_enter_state():
@ -76,78 +55,68 @@ func on_exit_state():
# --- Internal Logic ---
func _apply_floating_movement(delta: float, move_input: Vector2, vertical_input: float, roll_input: float):
func _apply_floating_movement(state: PhysicsDirectBodyState3D, move_input: Vector2, vertical_input: float, roll_input: float):
# Apply Linear Velocity
var pawn_forward = -pawn.global_basis.z
var pawn_right = pawn.global_basis.x # Use pawn's right for consistency
var pawn_up = pawn.global_basis.y
var move_dir_horizontal = (pawn_forward * move_input.y + pawn_right * move_input.x)
var move_dir_vertical = pawn_up * vertical_input
var move_dir_horizontal = (-state.transform.basis.z * move_input.y + state.transform.basis.x * move_input.x)
var move_dir_vertical = state.transform.basis.y * vertical_input
var combined_move_dir = move_dir_horizontal + move_dir_vertical
if combined_move_dir != Vector3.ZERO:
pawn.apply_central_force(combined_move_dir.normalized() * linear_acceleration)
state.apply_central_force(combined_move_dir.normalized() * linear_acceleration)
# --- Apply Roll Torque ---
# Calculate torque magnitude based on input
var roll_acceleration = pawn.basis.z * (-roll_input) * roll_torque_acceleration * delta
if roll_input != 0.0:
_is_auto_orienting = false # Cancel auto-orientation if rolling manually
# Apply the global torque vector using the pawn's helper function
pawn.apply_torque(roll_acceleration)
var roll_acceleration = state.transform.basis.z * (-roll_input) * roll_torque_acceleration
# Apply the global torque vector using the pawn's helper function
state.apply_torque(roll_acceleration)
func _set_auto_orient_target(state: PhysicsDirectBodyState3D):
# Set the target to where the camera is currently looking
var target_forward = - pawn.camera_anchor.global_basis.z # Look where camera looks
var target_up = state.transform.basis.y
_auto_orient_target = Basis.looking_at(target_forward, target_up)
_is_auto_orienting = true # Start the orientation process
# --- Auto-Orientation Logic ---
func _orient_pawn(delta: float):
# 1. Determine Target Orientation Basis
var initial_cam_basis = pawn.camera_anchor.global_basis
var target_forward = -pawn.camera_anchor.global_basis.z # Look where camera looks
var target_up = Vector3.UP # Default up initially
func _process_auto_orientation(state: PhysicsDirectBodyState3D):
# This function runs every physics frame
if not _is_auto_orienting:
return # Not orienting, do nothing
# --- THE FIX: Adjust how target_up is calculated ---
# Calculate velocity components relative to camera orientation
# var _forward_velocity_component = pawn.velocity.dot(target_forward)
# var _right_velocity_component = pawn.velocity.dot(pawn.camera_anchor.global_basis.x)
# 2. Calculate Torque using PD Controller
var torque = MotionUtils.calculate_pd_rotation_torque(
_auto_orient_target,
state.transform.basis,
state.angular_velocity, # Read from state
orientation_speed, # Kp
2 * sqrt(orientation_speed) # Kd (Critically Damped)
)
# Only apply strong "feet trailing" if significant forward/backward movement dominates
# and we are actually moving.
#if abs(forward_velocity_component) > abs(right_velocity_component) * 0.5 and velocity.length_squared() > 0.1:
#target_up = -velocity.normalized()
## Orthogonalize to prevent basis skew
#var target_right = target_up.cross(target_forward).normalized()
## If vectors are parallel, cross product is zero. Fallback needed.
#if target_right.is_zero_approx():
#target_up = transform.basis.y # Fallback to current up
#else:
#target_up = target_forward.cross(target_right).normalized()
#else:
## If primarily strafing or stationary relative to forward,
## maintain the current body's roll orientation (its local Y-axis).
target_up = pawn.transform.basis.y
# 2. Apply the torque to the physics state
state.apply_torque(torque)
# 3. Check for stop condition
var ang_vel_mag = state.angular_velocity.length()
var axis = state.angular_velocity.normalized()
# Create the target basis
var target_basis = Basis.looking_at(target_forward, target_up)
# If we are close enough AND slow enough, stop.
if ang_vel_mag < auto_orient_stop_velocity_threshold:
_is_auto_orienting = false
_auto_orient_target = pawn.global_basis # Set target to current for next time
if axis.is_normalized():
var physics_rotation = Basis().rotated(axis, ang_vel_mag * state.step)
pawn.camera_anchor.transform.basis = physics_rotation.inverse() * pawn.camera_anchor.transform.basis
# Optional Pitch Offset (Experimental):
# Apply the desired 70-degree pitch relative to the forward direction
# var target_pitch_rad = deg_to_rad(target_body_pitch_degrees)
# target_basis = target_basis.rotated(target_basis.x, target_pitch_rad) # Rotate around the target right vector
# 2. Smoothly Interpolate Towards Target Basis
var current_basis = pawn.global_basis
var new_basis = current_basis.slerp(target_basis, delta * orientation_speed).get_rotation_quaternion()
# Store the body's yaw *before* applying the new basis
var _old_body_yaw = current_basis.get_euler().y
var _old_body_pitch = current_basis.get_euler().x
# 3. Apply the new orientation
pawn.global_basis = new_basis
# 4. Reset camera pivot to rotation to what it was before we rotated the parent
pawn.camera_anchor.global_basis = initial_cam_basis
# --- Add new function placeholder ---
# TODO: Implement Rotation Stabilization Logic
func _apply_stabilization_torques(_delta: float):
func _apply_stabilization_torques(_state: PhysicsDirectBodyState3D):
if not is_instance_valid(stabilization_target):
stabilization_enabled = false
return
@ -168,7 +137,7 @@ func _apply_stabilization_torques(_delta: float):
# - Proportional Term (based on orientation error): P = rotational_error * stabilization_kp
# - Derivative Term (based on relative spin): D = relative_angular_velocity * stabilization_kd
# - Required Torque = -(P + D) # Negative to counteract error/spin
var required_torque = -(rotational_error * stabilization_kp + relative_angular_velocity * stabilization_kd)
var required_torque = - (rotational_error * stabilization_kp + relative_angular_velocity * stabilization_kd)
print("Applying stabilization torque: ", required_torque)
# 4. Convert Required Torque into Thruster Actions:
@ -178,7 +147,27 @@ func _apply_stabilization_torques(_delta: float):
# - Apply the forces/torques (similar to how _apply_floating_movement applies roll torque).
# Example (highly simplified, assumes direct torque application possible):
# angular_velocity += (required_torque / inertia) * delta
# --- Old logic for feet trailing (commented out) ---
# --- THE FIX: Adjust how target_up is calculated ---
# Calculate velocity components relative to camera orientation
# var _forward_velocity_component = pawn.velocity.dot(target_forward)
# var _right_velocity_component = pawn.velocity.dot(pawn.camera_anchor.global_basis.x)
# Only apply strong "feet trailing" if significant forward/backward movement dominates
# and we are actually moving.
#if abs(forward_velocity_component) > abs(right_velocity_component) * 0.5 and velocity.length_squared() > 0.1:
#target_up = -velocity.normalized()
## Orthogonalize to prevent basis skew
#var target_right = target_up.cross(target_forward).normalized()
## If vectors are parallel, cross product is zero. Fallback needed.
#if target_right.is_zero_approx():
#target_up = transform.basis.y # Fallback to current up
#else:
#target_up = target_forward.cross(target_right).normalized()
#else:
## If primarily strafing or stationary relative to forward,
## maintain the current body's roll orientation (its local Y-axis).
# --- Add methods for enabling/disabling stabilization, setting target etc. ---
func set_stabilization_enabled(enable: bool):

View File

@ -4,7 +4,6 @@ class_name ZeroGMovementComponent
## References
var pawn: CharacterPawn3D
var camera_pivot: Node3D
## State & Parameters
var current_grip: GripArea3D = null # Use GripArea3D type hint
@ -14,7 +13,7 @@ var nearby_grips: Array[GripArea3D] = []
@export var gripping_linear_damping: float = 6.0 # How quickly velocity stops
@export var gripping_linear_kd: float = 2 * sqrt(gripping_linear_damping) # How quickly velocity stops
@export var gripping_angular_damping: float = 3.0 # How quickly spin stops
@export var gripping_orient_speed: float = 2 * sqrt(gripping_angular_damping) # How quickly pawn rotates to face grip
@export var gripping_orient_speed: float = 2 * sqrt(gripping_angular_damping) # How quickly pawn rotates to face grip
var _target_basis: Basis # The orientation the PD controller is currently seeking
var _manual_roll_timer: Timer
@ -29,18 +28,17 @@ var _manual_roll_timer: Timer
@export var release_past_grip_threshold: float = 0.4 # How far past the grip origin before releasing
var next_grip_target: GripArea3D = null # The grip we are trying to transition to
# --- Launch Parameters ---
# --- Seeking Climb State ---
var _seeking_climb_input: Vector2 = Vector2.ZERO # The move_input held when seeking started
# --- Launch Parameters ---
@export var launch_charge_rate: float = 1.5
@export var max_launch_speed: float = 4.0
var launch_direction: Vector3 = Vector3.ZERO
var launch_charge: float = 0.0
# Enum for internal state
enum MovementState {
enum MovementState {
IDLE,
REACHING,
GRIPPING,
@ -48,76 +46,65 @@ enum MovementState {
SEEKING_CLIMB,
CHARGING_LAUNCH
}
var current_state: MovementState = MovementState.IDLE:
var movement_state: MovementState = MovementState.IDLE:
set(new_state):
if new_state == current_state: return
_on_exit_state(current_state) # Call exit logic for old state
current_state = new_state
_on_enter_state(current_state) # Call enter logic for new state
if new_state == movement_state: return
_on_exit_state(movement_state) # Call exit logic for old state
movement_state = new_state
_on_enter_state(movement_state) # Call enter logic for new state
func _ready():
pawn = get_parent() as CharacterPawn3D
if not pawn: printerr("ZeroGMovementComponent must be child of CharacterPawn3D")
camera_pivot = pawn.get_node_or_null("CameraPivot")
if not camera_pivot: printerr("ZeroGMovementComponent couldn't find CameraPivot")
_manual_roll_timer = Timer.new()
_manual_roll_timer.one_shot = true
_manual_roll_timer.wait_time = manual_roll_reset_delay
_manual_roll_timer.timeout.connect(_on_manual_roll_timeout)
add_child(_manual_roll_timer)
# --- Standardized Movement API ---
## Called by Pawn when relevant state is active (e.g., GRABBING_GRIP, REACHING_MOVE)
func process_movement(delta: float, move_input: Vector2, vertical_input: float, roll_input: float, reach_input: PlayerController3D.KeyInput, release_input: PlayerController3D.KeyInput):
func process_movement(physics_state: PhysicsDirectBodyState3D, move_input: Vector2, vertical_input: float, roll_input: float, reach_input: PlayerController3D.KeyInput, release_input: PlayerController3D.KeyInput):
if not is_instance_valid(pawn): return
_update_state(move_input, reach_input, release_input)
match current_state:
match movement_state:
MovementState.IDLE:
_process_idle(delta, move_input, vertical_input, roll_input, release_input)
_process_idle(physics_state, move_input, vertical_input, roll_input, release_input)
MovementState.REACHING:
_process_reaching()
_process_reaching(physics_state)
MovementState.GRIPPING:
_apply_grip_physics(delta, move_input, roll_input)
_process_grip_physics(physics_state, move_input, roll_input)
MovementState.CLIMBING:
_apply_climb_physics(move_input)
_process_climb_physics(physics_state, move_input)
MovementState.SEEKING_CLIMB:
_process_seeking_climb(move_input)
_process_seeking_climb(physics_state, move_input)
MovementState.CHARGING_LAUNCH:
_handle_launch_charge(delta)
_process_launch_charge(physics_state, move_input, reach_input)
# === STATE MACHINE ===
func _on_enter_state(state : MovementState):
print("ZeroGMovementComponent activated for state: ", MovementState.keys()[state])
# TODO: Use forces to match velocity to grip
# if state == MovementState.GRIPPING:
# pawn.velocity = Vector3.ZERO
# pawn.angular_velocity = Vector3.ZERO
# else: # e.g., REACHING_MOVE?
# state = MovementState.IDLE # Or SEARCHING?
func _on_exit_state(state: MovementState):
print("ZeroGMovementComponent deactivated for state: ", MovementState.keys()[state])
pass
func _on_enter_state(movement_state: MovementState):
print("ZeroGMovementComponent activated for movement_state: ", MovementState.keys()[movement_state])
func _on_exit_state(movement_state: MovementState):
print("ZeroGMovementComponent deactivated for movement_state: ", MovementState.keys()[movement_state])
# Ensure grip is released if state changes unexpectedly
#if state == MovementState.GRIPPING:
#_release_current_grip()
# if movement_state == MovementState.GRIPPING:
# _release_current_grip()
func _update_state(
move_input: Vector2,
reach_input: PlayerController3D.KeyInput,
release_input: PlayerController3D.KeyInput,
):
match current_state:
match movement_state:
MovementState.IDLE:
# Already handled initiating reach in process_movement
if reach_input.pressed or reach_input.held:
current_state = MovementState.REACHING
movement_state = MovementState.REACHING
MovementState.REACHING:
# TODO: If reach animation completes/hand near target -> GRIPPING
# If interact released during reach -> CANCEL -> IDLE
@ -140,8 +127,8 @@ func _update_state(
if (reach_input.pressed or reach_input.held) and move_input != Vector2.ZERO:
_start_charge(move_input)
return
elif move_input != Vector2.ZERO:
_start_climb(move_input) # This is overshadowed by the above check.
elif move_input != Vector2.ZERO and is_instance_valid(current_grip):
movement_state = MovementState.CLIMBING
MovementState.CLIMBING:
if reach_input.pressed or reach_input.held:
_start_charge(move_input)
@ -154,36 +141,26 @@ func _update_state(
return
# Continue climbing logic (finding next grip) happens in _process_climbing
MovementState.CHARGING_LAUNCH:
if not (reach_input.pressed or reach_input.held):
_execute_launch(move_input)
elif move_input == Vector2.ZERO: # Cancel charge while holding interact
current_state = MovementState.GRIPPING
if move_input == Vector2.ZERO: # Cancel charge while holding interact
movement_state = MovementState.GRIPPING
print("ZeroGMovementComponent: Cancelled Launch Charge")
# === MOVEMENT PROCESSING ===
func _process_idle(delta: float, move_input: Vector2, vertical_input: float, roll_input: float, release_input: PlayerController3D.KeyInput):
# State is IDLE (free-floating).
# Check for EVA suit usage.
var has_movement_input = (move_input != Vector2.ZERO or vertical_input != 0.0 or roll_input != 0.0)
if has_movement_input and is_instance_valid(pawn.eva_suit_component):
# Use EVA suit
pawn.eva_suit_component.apply_thrusters(pawn, delta, move_input, vertical_input, roll_input)
# Check for body orientation (if applicable)
if release_input.held and is_instance_valid(pawn.eva_suit_component):
pawn.eva_suit_component._orient_pawn(delta) # Use suit's orient
func _process_reaching():
func _process_idle(_physics_state: PhysicsDirectBodyState3D, _move_input: Vector2, _vertical_input: float, _roll_input: float, _release_input: PlayerController3D.KeyInput):
# TODO: Implement free-floating auto orientation against bulkheads to maintain orientation with ship
pass
func _process_reaching(physics_state: PhysicsDirectBodyState3D):
# TODO: Drive IK target towards current_grip.get_grip_transform().origin
# TODO: Monitor distance / animation state
# For now, we just instantly grip.
# For now, _we just instantly grip.
if _seeking_climb_input != Vector2.ZERO:
_attempt_grip(next_grip_target) # Complete the seek-reach
_attempt_grip(physics_state, next_grip_target) # Complete the seek-reach
else:
_attempt_grip(_find_best_grip())
_attempt_grip(physics_state, _find_best_grip())
func _apply_grip_physics(delta: float, _move_input: Vector2, roll_input: float):
func _process_grip_physics(physics_state: PhysicsDirectBodyState3D, _move_input: Vector2, roll_input: float):
if not is_instance_valid(pawn) or not is_instance_valid(current_grip):
_release_current_grip(); return
@ -197,35 +174,36 @@ func _apply_grip_physics(delta: float, _move_input: Vector2, roll_input: float):
# Rotate the current target basis around the grip's Z-axis
var grip_z_axis = current_grip.global_basis.z
_target_basis = _target_basis.rotated(grip_z_axis, -roll_input * manual_roll_speed * delta)
else:
# User is not rolling. Start the timer if it's not already running.
if _manual_roll_timer.is_stopped():
_manual_roll_timer.start()
_target_basis = _target_basis.rotated(grip_z_axis, -roll_input * manual_roll_speed * physics_state.step)
# Restart the timer
_manual_roll_timer.start()
elif _manual_roll_timer.wait_time < 0.0:
_on_manual_roll_timeout(physics_state) # Immediate reset if delay is negative
# --- 3. Apply Linear Force (PD Controller) ---
pawn.apply_central_force(_get_hold_force())
physics_state.apply_central_force(_get_hold_force(physics_state))
_apply_orientation_torque(_target_basis)
_apply_orientation_torque(physics_state, _target_basis)
func _apply_climb_physics(move_input: Vector2):
func _process_climb_physics(physics_state: PhysicsDirectBodyState3D, move_input: Vector2):
if not is_instance_valid(pawn) or not is_instance_valid(current_grip):
_stop_climb(true); return
# 1. Calculate Climb Direction: For climbing we interpret W as up from the pawns perspective instead of forward
var climb_direction = move_input.y * pawn.global_basis.y + move_input.x * pawn.global_basis.x
var climb_direction = move_input.y * physics_state.transform.basis.y + move_input.x * physics_state.transform.basis.x
climb_direction = climb_direction.normalized()
# 2. Find Next Grip
next_grip_target = _find_best_grip(climb_direction, INF, climb_angle_threshold_deg)
# 3. Check for Handover: This should be more eager to mark a new grip as current than below check is to release when climbing past
var performed_handover = _attempt_grip(next_grip_target)
var performed_handover = _attempt_grip(physics_state, next_grip_target)
# 4. Check for Release Past Grip (if no handover)
if not performed_handover:
var current_grip_pos = current_grip.global_position
var vector_from_grip_to_pawn = pawn.global_position - current_grip_pos
var vector_from_grip_to_pawn = physics_state.transform.origin - current_grip_pos
var distance_along_climb_dir = vector_from_grip_to_pawn.dot(climb_direction)
if distance_along_climb_dir > release_past_grip_threshold: # Release threshold
_release_current_grip(move_input)
@ -233,13 +211,13 @@ func _apply_climb_physics(move_input: Vector2):
# 5. Apply Combined Forces for Climbing & Holding
# --- Force 1: Positional Hold (From _apply_grip_physics) ---
# --- Force 1: Positional Hold (From _process_grip_physics) ---
# Calculate the force needed to stay at that position
var force_hold = _get_hold_force()
var force_hold = _get_hold_force(physics_state)
# --- Force 2: Climbing Movement ---
var target_velocity = climb_direction * climb_speed
var error_vel = target_velocity - pawn.linear_velocity
var error_vel = target_velocity - physics_state.linear_velocity
var force_climb = error_vel * climb_acceleration # Kp = climb_acceleration
# Find the part of the "hold" force that is parallel to our climb direction
@ -255,30 +233,29 @@ func _apply_climb_physics(move_input: Vector2):
# We apply *both* forces. The hold force will manage the offset,
# while the climb force will overpower it in the climb direction.
var total_force = force_hold + force_climb
pawn.apply_central_force(total_force)
physics_state.apply_central_force(total_force)
# 6. Apply Angular Force (Auto-Orient to current grip)
var target_basis = _choose_grip_orientation(current_grip.global_basis)
_apply_orientation_torque(target_basis)
var target_basis = _choose_grip_orientation(physics_state, current_grip.global_basis)
_apply_orientation_torque(physics_state, target_basis)
func _process_seeking_climb(move_input: Vector2):
func _process_seeking_climb(physics_state: PhysicsDirectBodyState3D, move_input: Vector2):
# If the player's input has changed from what initiated the seek, cancel it.
if not move_input.is_equal_approx(_seeking_climb_input):
var target_grip = _find_best_grip()
_seeking_climb_input = Vector2.ZERO # Reset for next time
if _attempt_grip(target_grip):
if _attempt_grip(physics_state, _find_best_grip()):
# Successfully found and grabbed a grip. The state is now GRIPPING.
print("Seeking Climb ended, gripped new target.")
else:
current_state = MovementState.IDLE
movement_state = MovementState.IDLE
# No grip found. Transition to IDLE.
print("Seeking Climb ended, no grip found. Reverting to IDLE.")
# --- Grip Helpers
## The single, authoritative function for grabbing a grip.
func _attempt_grip(target_grip: GripArea3D) -> bool:
func _attempt_grip(physics_state: PhysicsDirectBodyState3D, target_grip: GripArea3D) -> bool:
if not is_instance_valid(target_grip):
return false
@ -289,7 +266,7 @@ func _attempt_grip(target_grip: GripArea3D) -> bool:
old_grip.release(pawn)
_manual_roll_timer.stop()
_target_basis = _choose_grip_orientation(target_grip.global_basis)
_target_basis = _choose_grip_orientation(physics_state, target_grip.global_basis)
current_grip = target_grip
@ -298,28 +275,28 @@ func _attempt_grip(target_grip: GripArea3D) -> bool:
_seeking_climb_input = Vector2.ZERO
# If we weren't already climbing, transition to GRIPPING state.
if current_state != MovementState.CLIMBING:
current_state = MovementState.GRIPPING
if movement_state != MovementState.CLIMBING:
movement_state = MovementState.GRIPPING
print("Successfully gripped: ", current_grip.get_parent().name)
return true
else:
# Failed to grab the new grip.
print("Failed to grip: ", target_grip.get_parent().name, " (likely occupied).")
if current_state == MovementState.CLIMBING:
if movement_state == MovementState.CLIMBING:
_stop_climb(false) # Stop climbing, return to gripping previous one
return false
# --- Grip Orientation Helper ---
func _choose_grip_orientation(grip_basis: Basis) -> Basis:
func _choose_grip_orientation(physics_state: PhysicsDirectBodyState3D, grip_basis: Basis) -> Basis:
# 1. Define the two possible target orientations based on the grip.
# Both will look away from the grip's surface (-Z).
var look_at_dir = -grip_basis.z.normalized()
var look_at_dir = - grip_basis.z.normalized()
var target_basis_up = Basis.looking_at(look_at_dir, grip_basis.y.normalized()).orthonormalized()
var target_basis_down = Basis.looking_at(look_at_dir, -grip_basis.y.normalized()).orthonormalized()
# 2. Get the pawn's current orientation.
var current_basis = pawn.global_basis
var current_basis = physics_state.transform.basis
# 3. Compare which target orientation is "closer" to the current one.
# We can do this by finding the angle of rotation needed to get from current to each target.
@ -399,11 +376,11 @@ func _release_current_grip(move_input: Vector2 = Vector2.ZERO):
# If we were climbing and are still holding a climb input, start seeking.
if move_input != Vector2.ZERO:
current_state = MovementState.SEEKING_CLIMB
movement_state = MovementState.SEEKING_CLIMB
_seeking_climb_input = move_input # Store the input that started the seek
# print("ZeroGMovementComponent: Released grip, now SEEKING_CLIMB.")
else:
current_state = MovementState.IDLE
movement_state = MovementState.IDLE
# print("ZeroGMovementComponent: Released grip, now IDLE.")
@ -413,16 +390,6 @@ func _cancel_reach():
print("ZeroGMovementComponent: Reach cancelled.")
# --- Climbing Helpers ---
func _start_climb(move_input: Vector2):
if not is_instance_valid(current_grip): return
current_state = MovementState.CLIMBING
# Calculate initial climb direction based on input relative to camera/grip
var pawn_up = pawn.global_basis.y
var pawn_right = pawn.global_basis.x
print("ZeroGMoveController: Started Climbing in direction: ", (pawn_up * move_input.y + pawn_right * move_input.x).normalized())
func _stop_climb(release_grip: bool):
# print("ZeroGMoveController: Stopping Climb. Release Grip: ", release_grip)
# TODO: Implement using forces
@ -431,23 +398,23 @@ func _stop_climb(release_grip: bool):
if release_grip:
_release_current_grip() # Transitions to IDLE
else:
current_state = MovementState.GRIPPING # Go back to stationary gripping
movement_state = MovementState.GRIPPING # Go back to stationary gripping
func _apply_orientation_torque(target_basis: Basis):
func _apply_orientation_torque(physics_state: PhysicsDirectBodyState3D, target_basis: Basis):
var torque = MotionUtils.calculate_pd_rotation_torque(
target_basis,
pawn.global_basis,
pawn.angular_velocity, # Use angular_velocity (from RigidBody3D)
gripping_orient_speed, # Kp
physics_state.transform.basis,
physics_state.angular_velocity, # Use angular_velocity (from RigidBody3D)
gripping_orient_speed, # Kp
gripping_angular_damping # Kd
)
pawn.apply_torque(torque)
physics_state.apply_torque(torque)
# --- Launch helpers ---
func _start_charge(move_input: Vector2):
if not is_instance_valid(current_grip): return # Safety check
current_state = MovementState.CHARGING_LAUNCH
movement_state = MovementState.CHARGING_LAUNCH
launch_charge = 0.0
# Calculate launch direction based on input and push-off normal
@ -460,32 +427,35 @@ func _start_charge(move_input: Vector2):
print("ZeroGMovementComponent: Charging Launch")
func _handle_launch_charge(delta: float):
func _process_launch_charge(physics_state: PhysicsDirectBodyState3D, move_input: Vector2, reach_input: PlayerController3D.KeyInput):
if not (reach_input.pressed or reach_input.held):
_execute_launch(physics_state, move_input)
# hold on to current grip
pawn.apply_central_force(_get_hold_force())
physics_state.apply_central_force(_get_hold_force(physics_state))
launch_charge = min(launch_charge + launch_charge_rate * delta, max_launch_speed)
launch_charge = min(launch_charge + launch_charge_rate * physics_state.step, max_launch_speed)
func _execute_launch(move_input: Vector2):
func _execute_launch(physics_state: PhysicsDirectBodyState3D, move_input: Vector2):
if not is_instance_valid(current_grip): return # Safety check
_release_current_grip(move_input) # Release AFTER calculating direction
pawn.apply_impulse(launch_direction * launch_charge)
physics_state.apply_impulse(launch_direction * launch_charge)
launch_charge = 0.0
# Instead of going to IDLE, go to SEEKING_CLIMB to find the next grip.
# The move_input that started the launch is what we'll use for the seek direction.
# _seeking_climb_input = (pawn.global_basis.y.dot(launch_direction) * Vector2.UP) + (pawn.global_basis.x.dot(launch_direction) * Vector2.RIGHT)
# current_state = MovementState.SEEKING_CLIMB
print("ZeroGMovementComponent: Launched with speed ", pawn.linear_velocity.length(), " and now SEEKING_CLIMB")
# movement_state = MovementState.SEEKING_CLIMB
print("ZeroGMovementComponent: Launched with speed ", physics_state.linear_velocity.length(), " and now SEEKING_CLIMB")
# --- Force Calculation Helpers ---
func _get_hold_force() -> Vector3:
func _get_hold_force(state) -> Vector3:
if not is_instance_valid(pawn) or not is_instance_valid(current_grip):
return Vector3.ZERO
var grip_base_transform = current_grip.global_transform
var grip_base_transform = current_grip.global_transform
var target_direction = grip_base_transform.basis.z.normalized()
var hold_distance = _get_hold_distance()
var target_position = grip_base_transform.origin + target_direction * hold_distance
@ -493,19 +463,19 @@ func _get_hold_force() -> Vector3:
# Calculate the force needed to stay at that position
var force_hold = MotionUtils.calculate_pd_position_force(
target_position,
pawn.global_position,
pawn.linear_velocity,
state.transform.origin,
state.linear_velocity,
gripping_linear_damping, # Kp
gripping_linear_kd # Kd
gripping_linear_kd # Kd
)
return force_hold
# --- Manual Roll Reset ---
func _on_manual_roll_timeout():
func _on_manual_roll_timeout(physics_state: PhysicsDirectBodyState3D):
# Timer fired. This means the user hasn't touched roll for [delay] seconds.
# We smoothly reset the _target_basis back to the closest grip orientation.
if is_instance_valid(current_grip):
_target_basis = _choose_grip_orientation(current_grip.global_basis)
_target_basis = _choose_grip_orientation(physics_state, current_grip.global_basis)
# --- Signal Handlers ---

View File

@ -107,7 +107,7 @@ func _integrate_forces(state: PhysicsDirectBodyState3D):
# 4. Apply Rotational Physics (T = I * angular_acceleration)
# REFACTOR: Use the simplified 3D torque equation from your CharacterPawn3D
#if inertia.length() > 0:
var angular_acceleration = accumulated_torque / inertia
# var angular_acceleration = accumulated_torque / inertia
# print("Inertia for %s: %s" % [self, inertia])
# print("Angular Acceleration for %s: %s" % [self, angular_acceleration])
# angular_velocity += angular_acceleration * state.step
@ -151,7 +151,7 @@ func recalculate_physical_properties():
var local_pos = part.global_position - self.global_position
# REFACTOR: This logic (Parallel Axis Theorem) is still correct for Vector3
var r_squared = (local_pos - local_center_of_mass).length_squared()
total_inertia += part.base_mass * r_squared
# total_inertia += part.base_mass * r_squared
# --- Step 3: Assign the final values ---
self.mass = total_mass