feat(*): Add orientation-readable team-coloured ship meshes, smooth the ball-cam orbit, and hide arena walls the camera is outside of

This commit is contained in:
Josh Creek
2026-07-19 22:18:21 +01:00
parent dfb7285704
commit 4f075b352f
5 changed files with 171 additions and 52 deletions
+51 -46
View File
@@ -9,8 +9,11 @@ extends Node3D
signal camera_mode_changed(is_ball_cam: bool)
@export var camera_distance := 8.0
@export var camera_height := 4.0
@export var camera_height := 3.0
@export var camera_smoothing := 10.0
@export var orbit_smoothing := 6.0 # How fast the camera swings around the ship in ball cam
@export var look_smoothing := 20.0 # How fast the camera re-centres its look target
@export var min_camera_height := 1.0 # Keeps the camera from dipping through the arena floor
var target: Ship
var ball_cam_enabled := true
@@ -18,6 +21,10 @@ var ball_cam_enabled := true
@onready var camera: Camera3D = $Camera3D
var _ball: Node3D
# Smoothed horizontal direction from ball to ship; the ball-cam orbits along
# this so the camera swings smoothly around the ship instead of chasing a
# raw position (which whips when ball and ship are close together).
var _orbit_dir := Vector3.BACK
func _ready():
@@ -48,60 +55,58 @@ func _get_ball() -> Node3D:
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
# Ball cam keeps the ship between the camera and the ball: the camera sits
# on the ball→ship line (horizontal component only), looking at the ball,
# so the ship stays low-centre in frame and the ball stays centred.
var ship_pos: Vector3 = target.global_transform.origin
var ball_pos: Vector3 = 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()
# Smooth the orbit direction in angle space rather than lerping the camera
# position directly — when ball and ship pass close to each other the raw
# direction flips instantly, and slerping the direction turns that into a
# controlled swing around the ship. The vertical component is dropped so
# a ball flying overhead pitches the camera up instead of shoving it into
# the floor or the sky.
var flat := Vector3(ship_pos.x - ball_pos.x, 0.0, ship_pos.z - ball_pos.z)
if flat.length() > 0.25:
var t := 1.0 - exp(-orbit_smoothing * delta) # frame-rate independent
_orbit_dir = _orbit_dir.slerp(flat.normalized(), t).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
var camera_target_pos := ship_pos + _orbit_dir * camera_distance + Vector3.UP * camera_height
camera_target_pos.y = maxf(camera_target_pos.y, min_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)
var pos_t := 1.0 - exp(-camera_smoothing * delta)
camera.global_position = camera.global_position.lerp(camera_target_pos, pos_t)
# 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)
# Look slightly above the ball's centre; look smoothing is faster than
# position smoothing so the ball never drifts out of frame while the
# camera is still swinging into place.
_smooth_look_at(ball_pos + Vector3.UP * 0.5, 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
var ship_pos: Vector3 = target.global_transform.origin
var ship_forward: Vector3 = -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
var camera_target_pos := ship_pos - ship_forward * camera_distance + Vector3.UP * camera_height
camera_target_pos.y = maxf(camera_target_pos.y, min_camera_height)
# Smoothly move camera
camera.global_transform.origin = camera.global_transform.origin.lerp(camera_target_pos, camera_smoothing * delta)
var pos_t := 1.0 - exp(-camera_smoothing * delta)
camera.global_position = camera.global_position.lerp(camera_target_pos, pos_t)
# 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)
# Look ahead of the ship
_smooth_look_at(ship_pos + ship_forward * 10.0, delta)
func _smooth_look_at(point: Vector3, delta: float) -> void:
var to_point := point - camera.global_position
if to_point.length() < 0.1:
return
var dir := to_point.normalized()
if absf(dir.dot(Vector3.UP)) > 0.99:
return # Nearly vertical: looking_at would be degenerate, keep last frame
var look_basis := Basis.looking_at(dir, Vector3.UP)
var look_t := 1.0 - exp(-look_smoothing * delta)
camera.global_basis = camera.global_basis.slerp(look_basis, look_t).orthonormalized()