class_name ShipCameraRig extends Node3D # Third-person camera for a Ship. Ball Cam keeps the ship between the camera # and the ball; Ship Cam chases behind the ship. The game mode spawns this # rig and assigns `target` after spawning the player's ship — ships # themselves are camera-free (a headless/AI ship never needs one). signal camera_mode_changed(is_ball_cam: bool) @export var camera_distance := 8.0 @export var camera_height := 4.0 @export var camera_smoothing := 10.0 var target: Ship var ball_cam_enabled := true @onready var camera: Camera3D = $Camera3D var _ball: Node3D func _ready(): # Group lets the HUD discover the rig for the camera-mode instrument add_to_group("ship_camera") 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 not is_instance_valid(target): return var ball := _get_ball() if ball_cam_enabled and ball: _update_ball_cam(delta, ball) else: _update_ship_cam(delta) func _get_ball() -> Node3D: if not is_instance_valid(_ball): _ball = get_tree().get_first_node_in_group("ball") return _ball func _update_ball_cam(delta, ball: Node3D): # In ball cam, camera positions itself so the ship is between camera and ball # Physics: Vector mathematics for 3D positioning var ship_pos = target.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 = target.global_transform.origin var ship_forward = -target.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)