Force based EVA movement
This commit is contained in:
@ -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():
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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 ---
|
||||
|
||||
@ -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
|
||||
|
||||
Reference in New Issue
Block a user