Files

360 lines
13 KiB
GDScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
class_name Ship
extends RigidBody3D
@export_group("Movement")
@export var thrust_power = 150.0 # Main thruster power
@export var maneuvering_thrust = 75.0 # Side/vertical thruster power
@export var vertical_thrust = 120.0 # Up/down thruster power
@export var turbo_multiplier = 2.5 # Turbo boost multiplier
@export var max_speed = 35.0 # Maximum velocity
@export var rotation_power = 20.0 # Angular thrust power
@export var max_angular_speed = 3.0 # Maximum rotation speed
@export var drag_coefficient = 0.98 # Linear drag (air resistance)
@export var angular_drag = 0.95 # Rotational drag
@export_group("Camera")
var camera_distance = 8.0
var camera_height = 4.0
var camera_smoothing = 10.0
@onready var camera : Camera3D = get_node("Camera3D")
@onready var ball = get_parent().get_node("Ball")
var ball_cam_enabled = true
# Instrument signals for efficient data distribution
signal speed_changed(speed: float)
signal attitude_changed(pitch: float, roll: float, yaw: float)
signal altitude_changed(altitude: float)
signal thrust_changed(thrust_percent: float)
signal angular_velocity_changed(angular_speed: float)
signal camera_mode_changed(is_ball_cam: bool)
signal heading_changed(heading_degrees: float)
# Performance optimization - track last emitted values to avoid unnecessary signals
var _last_speed: float = -1.0
var _last_altitude: float = -999999.0
var _last_angular_speed: float = -1.0
var _last_pitch: float = -999.0
var _last_roll: float = -999.0
var _last_yaw: float = -999.0
var _last_heading: float = -999.0
var _last_thrust: float = -1.0
var _last_camera_mode: bool = true
# Thresholds for signal emission (only emit if change is significant)
const SPEED_THRESHOLD = 0.1 # m/s
const ALTITUDE_THRESHOLD = 0.5 # meters
const ANGULAR_THRESHOLD = 0.01 # rad/s
const ATTITUDE_THRESHOLD = 1.0 # degrees
const THRUST_THRESHOLD = 1.0 # percent
func _ready():
mass = 5
gravity_scale = 1.0
# Add ship to group for instrument discovery
add_to_group("ship")
# Set custom inertia for better rotation
# Physics: I = m * r² (moment of inertia = mass × radius²)
# Lower inertia = easier to rotate, higher inertia = more stable
inertia = Vector3(1.0, 1.0, 1.0)
# Create and apply low-friction physics material
# Physics: F_friction = μ * N (friction force = coefficient × normal force)
# Lower μ (friction coefficient) = less resistance to sliding
var ship_material = PhysicsMaterial.new()
ship_material.friction = 0.1 # Very low friction
ship_material.bounce = 0.2 # Slight bounce
physics_material_override = ship_material
# Make sure the RigidBody is completely free to move and rotate
freeze = false
lock_rotation = false
# Ensure all axes can rotate
axis_lock_angular_x = false
axis_lock_angular_y = false
axis_lock_angular_z = false
print("Ship physics configured - Mass: ", mass, " Gravity scale: ", gravity_scale, " Inertia: ", inertia)
print("Rotation locks - X:", axis_lock_angular_x, " Y:", axis_lock_angular_y, " Z:", axis_lock_angular_z)
func _input(event):
if event.is_action_pressed("ui_accept"): # Enter key
ball_cam_enabled = !ball_cam_enabled
camera_mode_changed.emit(ball_cam_enabled)
func _physics_process(delta):
if camera:
_update_camera(delta)
_emit_telemetry_data()
func _update_camera(delta):
if ball_cam_enabled and ball:
_update_ball_cam(delta)
else:
_update_ship_cam(delta)
func _update_ball_cam(delta):
# In ball cam, camera positions itself so the ship is between camera and ball
# Physics: Vector mathematics for 3D positioning
var ship_pos = global_transform.origin
var ball_pos = ball.global_transform.origin
# Calculate direction from ball to ship
# Physics: Vector subtraction and normalization
# Direction vector: d̂ = (P₂ - P₁) / |P₂ - P₁|
var ball_to_ship = (ship_pos - ball_pos).normalized()
# Position camera behind the ship relative to the ball's position
# This ensures the ship is always between the camera and ball
# Physics: Vector addition for position calculation
# P_camera = P_ship + d̂ * distance + height_offset
var camera_target_pos = ship_pos + ball_to_ship * camera_distance + Vector3.UP * camera_height
# Smoothly move camera to target position
# Physics: Linear interpolation (LERP) for smooth motion
# P(t) = P₀ + t * (P₁ - P₀), where t ∈ [0,1]
# This creates exponential approach to target position
camera.global_transform.origin = camera.global_transform.origin.lerp(camera_target_pos, camera_smoothing * delta)
# Make camera look at the ball
if camera.global_transform.origin.distance_to(ball_pos) > 0.1:
# Calculate direction to ball
var camera_pos = camera.global_transform.origin
var to_ball = (ball_pos - camera_pos).normalized()
# Create look-at transform manually
# Physics: 3D rotation matrices and basis vectors
# Uses right-hand rule: forward = -Z, up = Y, right = X
# Basis matrix transforms local coordinates to world coordinates
var camera_transform = Transform3D()
camera_transform.origin = camera_pos
camera_transform.basis = Basis.looking_at(to_ball, Vector3.UP)
# Apply the rotation smoothly
# Physics: Spherical linear interpolation (SLERP) for rotation
# SLERP provides smooth rotation along great circle on unit sphere
# Maintains constant angular velocity during interpolation
camera.global_transform.basis = camera.global_transform.basis.slerp(camera_transform.basis, camera_smoothing * delta)
func _update_ship_cam(delta):
# In ship cam, camera follows and looks in the same direction as the ship
var ship_pos = global_transform.origin
var ship_forward = -global_transform.basis.z
# Position camera behind and above the ship
var camera_target_pos = ship_pos - ship_forward * camera_distance + Vector3.UP * camera_height
# Smoothly move camera
camera.global_transform.origin = camera.global_transform.origin.lerp(camera_target_pos, camera_smoothing * delta)
# Make camera look in the same direction as the ship
var look_target = ship_pos + ship_forward * 10.0 # Look ahead of the ship
camera.look_at(look_target, Vector3.UP)
func _integrate_forces(state):
# Get thruster input
var thrust_input = get_thrust_input()
var rotation_input = get_rotation_input()
# === TRANSLATION (Movement) ===
apply_thruster_forces(state, thrust_input)
# === ROTATION (Turning) ===
apply_rotation_forces(state, rotation_input)
# === DRAG AND LIMITS ===
apply_drag_and_limits(state, rotation_input)
func get_thrust_input() -> Vector3:
var thrust = Vector3.ZERO
# Forward/Backward thrust (main engines)
if Input.is_action_pressed("move_forward"):
thrust.z += 1.0
if Input.is_action_pressed("move_back"):
thrust.z -= 1.0
# Strafe thrusters (left/right)
if Input.is_action_pressed("move_left"):
thrust.x -= 1.0
if Input.is_action_pressed("move_right"):
thrust.x += 1.0
# Vertical thrusters (up/down)
if Input.is_action_pressed("move_up"):
thrust.y += 1.0
if Input.is_action_pressed("move_down"):
thrust.y -= 1.0
return thrust
func get_rotation_input() -> Vector3:
var rotation = Vector3.ZERO
# Yaw (turn left/right around Y axis) - only use these if they exist
if Input.is_action_pressed("turn_left"):
rotation.y += 1.0
if Input.is_action_pressed("turn_right"):
rotation.y -= 1.0
# Pitch (nose up/down around X axis)
if Input.is_action_pressed("pitch_up"):
rotation.x -= 1.0
if Input.is_action_pressed("pitch_down"):
rotation.x += 1.0
# Roll (bank left/right around Z axis)
if Input.is_action_pressed("roll_left"):
rotation.z += 1.0
if Input.is_action_pressed("roll_right"):
rotation.z -= 1.0
return rotation
func apply_thruster_forces(state: PhysicsDirectBodyState3D, thrust_input: Vector3):
if thrust_input.length() < 0.01:
return
# Convert thrust input to world space forces based on ship orientation
# Physics: F = m * a (Newton's Second Law: Force = mass × acceleration)
# World force = Local force × Rotation matrix (basis transformation)
var ship_basis = global_transform.basis
var world_thrust = Vector3.ZERO
# All thrusters should work relative to ship orientation
# Physics: Vector transformation from local to world coordinates
# F_world = R * F_local (where R is rotation matrix)
# Forward/backward thrust (main engines)
world_thrust += -ship_basis.z * thrust_input.z * thrust_power
# Strafe thrust (left/right maneuvering thrusters)
world_thrust += ship_basis.x * thrust_input.x * maneuvering_thrust
# Vertical thrust (up/down thrusters relative to ship orientation)
world_thrust += ship_basis.y * thrust_input.y * vertical_thrust
# Check for turbo
var is_turbo = Input.is_action_pressed("turbo") and thrust_input.z > 0
if is_turbo:
world_thrust *= turbo_multiplier
# Apply the force
# Physics: Δv = F * Δt / m (change in velocity = force × time / mass)
state.apply_central_force(world_thrust)
func apply_rotation_forces(state: PhysicsDirectBodyState3D, rotation_input: Vector3):
if rotation_input.length() < 0.01:
return
# Apply torque for rotation - simple and effective
# Physics: τ = I * α (torque = moment of inertia × angular acceleration)
# Also: α = τ / I (angular acceleration = torque / moment of inertia)
# Lower inertia = higher angular acceleration for same torque
var torque = Vector3(
rotation_input.x * rotation_power, # Pitch (rotation around X-axis)
rotation_input.y * rotation_power, # Yaw (rotation around Y-axis)
rotation_input.z * rotation_power # Roll (rotation around Z-axis)
)
# Physics: Δω = τ * Δt / I (change in angular velocity = torque × time / inertia)
state.apply_torque(torque)
func apply_drag_and_limits(state: PhysicsDirectBodyState3D, rotation_input: Vector3):
# Linear drag (air resistance)
# Physics: F_drag = -½ * ρ * v² * C_d * A (drag force equation)
# Simplified: v_new = v_old * drag_coefficient (exponential decay)
# This simulates air resistance reducing velocity over time
state.linear_velocity *= drag_coefficient
# Angular drag (rotational resistance)
# Physics: Similar to linear drag but for rotational motion
# τ_drag = -C_angular * ω² (angular drag torque)
# Simplified: ω_new = ω_old * angular_drag (exponential decay)
if rotation_input.length() < 0.01:
# More drag when not actively rotating to stop quicker
state.angular_velocity *= 0.9
else:
# Normal drag when actively rotating
state.angular_velocity *= angular_drag
# Limit maximum speeds
# Physics: Terminal velocity concept - maximum achievable speed
# When thrust force = drag force, acceleration = 0, velocity = constant
if state.linear_velocity.length() > max_speed:
# Normalize to unit vector, then scale to max speed
# Physics: v̂ = v / |v| (unit vector), v_limited = v̂ * v_max
state.linear_velocity = state.linear_velocity.normalized() * max_speed
if state.angular_velocity.length() > max_angular_speed:
# Same concept for angular velocity
# Physics: ω̂ = ω / |ω|, ω_limited = ω̂ * ω_max
state.angular_velocity = state.angular_velocity.normalized() * max_angular_speed
func _emit_telemetry_data():
# Ship only calculates and emits data - HUD handles display
# Performance optimization: only emit signals when values change significantly
# Speed telemetry
# Physics: |v| = √(vₓ² + vᵧ² + vᵤ²) (magnitude of velocity vector)
var current_speed = linear_velocity.length()
if abs(current_speed - _last_speed) > SPEED_THRESHOLD:
speed_changed.emit(current_speed)
_last_speed = current_speed
# Altitude telemetry
# Physics: Height measurement from reference point (y = 0)
var current_altitude = global_transform.origin.y
if abs(current_altitude - _last_altitude) > ALTITUDE_THRESHOLD:
altitude_changed.emit(current_altitude)
_last_altitude = current_altitude
# Angular velocity telemetry
# Physics: |ω| = √(ωₓ² + ωᵧ² + ωᵤ²) (magnitude of angular velocity vector)
var angular_speed = angular_velocity.length()
if abs(angular_speed - _last_angular_speed) > ANGULAR_THRESHOLD:
angular_velocity_changed.emit(angular_speed)
_last_angular_speed = angular_speed
# Camera mode telemetry (only emit when it actually changes)
if ball_cam_enabled != _last_camera_mode:
camera_mode_changed.emit(ball_cam_enabled)
_last_camera_mode = ball_cam_enabled
# Attitude telemetry (pitch, roll, yaw from ship orientation)
# Physics: Euler angles from rotation matrix
# Pitch = rotation around X-axis, Roll = rotation around Z-axis
var ship_rotation = global_transform.basis.get_euler(EULER_ORDER_XYZ)
var pitch_deg = rad_to_deg(ship_rotation.x)
var roll_deg = rad_to_deg(ship_rotation.z)
var yaw_deg = rad_to_deg(ship_rotation.y)
if abs(pitch_deg - _last_pitch) > ATTITUDE_THRESHOLD or \
abs(roll_deg - _last_roll) > ATTITUDE_THRESHOLD or \
abs(yaw_deg - _last_yaw) > ATTITUDE_THRESHOLD:
attitude_changed.emit(pitch_deg, roll_deg, yaw_deg)
_last_pitch = pitch_deg
_last_roll = roll_deg
_last_yaw = yaw_deg
# Heading telemetry (yaw - direction ship is facing)
# Physics: Yaw = rotation around Y-axis (compass heading)
# Convert to 0-360° range for traditional compass display
var heading = fmod(yaw_deg + 360.0, 360.0) # Normalize to 0-360°
if abs(heading - _last_heading) > ATTITUDE_THRESHOLD:
heading_changed.emit(heading)
_last_heading = heading
# Thrust telemetry
# Physics: Thrust output as percentage of maximum available thrust
var thrust_input = get_thrust_input()
var thrust_magnitude = thrust_input.length()
var thrust_percent = thrust_magnitude * 100.0
if abs(thrust_percent - _last_thrust) > THRUST_THRESHOLD:
thrust_changed.emit(thrust_percent)
_last_thrust = thrust_percent