diff --git a/Game/objects/ship.tscn b/Game/objects/ship.tscn index 59c4cc26..1678210a 100644 --- a/Game/objects/ship.tscn +++ b/Game/objects/ship.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=5 format=3 uid="uid://p07epxnh8wwp"] +[gd_scene load_steps=13 format=3 uid="uid://p07epxnh8wwp"] [ext_resource type="Script" uid="uid://dyq1n7q1bqjps" path="res://scripts/ship.gd" id="1_efag7"] @@ -6,8 +6,52 @@ friction = 0.1 bounce = 0.2 -[sub_resource type="BoxMesh" id="BoxMesh_efag7"] -size = Vector3(1, 1, 4) +[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_hull"] +albedo_color = Color(0.35, 0.37, 0.42, 1) +metallic = 0.6 +roughness = 0.4 + +[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_accent"] +albedo_color = Color(0.25, 0.55, 1, 1) +metallic = 0.3 +roughness = 0.5 +emission_enabled = true +emission = Color(0.25, 0.55, 1, 1) +emission_energy_multiplier = 0.35 + +[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_canopy"] +albedo_color = Color(0.15, 0.85, 1, 1) +metallic = 0.8 +roughness = 0.1 +emission_enabled = true +emission = Color(0.15, 0.85, 1, 1) +emission_energy_multiplier = 0.5 + +[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_engine"] +albedo_color = Color(1, 0.55, 0.15, 1) +emission_enabled = true +emission = Color(1, 0.55, 0.15, 1) +emission_energy_multiplier = 2.0 + +[sub_resource type="BoxMesh" id="BoxMesh_hull"] +material = SubResource("StandardMaterial3D_hull") +size = Vector3(1, 0.75, 2.7) + +[sub_resource type="PrismMesh" id="PrismMesh_nose"] +material = SubResource("StandardMaterial3D_accent") +size = Vector3(1, 1.4, 0.75) + +[sub_resource type="BoxMesh" id="BoxMesh_canopy"] +material = SubResource("StandardMaterial3D_canopy") +size = Vector3(0.55, 0.28, 0.8) + +[sub_resource type="BoxMesh" id="BoxMesh_fin"] +material = SubResource("StandardMaterial3D_accent") +size = Vector3(0.1, 0.5, 0.8) + +[sub_resource type="BoxMesh" id="BoxMesh_engine"] +material = SubResource("StandardMaterial3D_engine") +size = Vector3(0.7, 0.5, 0.25) [sub_resource type="BoxShape3D" id="BoxShape3D_dsjou"] size = Vector3(1, 1, 4) @@ -18,8 +62,25 @@ physics_material_override = SubResource("PhysicsMaterial_ship") inertia = Vector3(1, 1, 1) script = ExtResource("1_efag7") -[node name="MeshInstance3D" type="MeshInstance3D" parent="."] -mesh = SubResource("BoxMesh_efag7") +[node name="Hull" type="MeshInstance3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.125, 0.35) +mesh = SubResource("BoxMesh_hull") + +[node name="Nose" type="MeshInstance3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 0, 1, 0, -1, 0, 0, -0.125, -1.3) +mesh = SubResource("PrismMesh_nose") + +[node name="Canopy" type="MeshInstance3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.36, -0.5) +mesh = SubResource("BoxMesh_canopy") + +[node name="TailFin" type="MeshInstance3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.25, 1.25) +mesh = SubResource("BoxMesh_fin") + +[node name="EngineGlow" type="MeshInstance3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.125, 1.85) +mesh = SubResource("BoxMesh_engine") [node name="CollisionShape3D" type="CollisionShape3D" parent="."] shape = SubResource("BoxShape3D_dsjou") diff --git a/Game/project.godot b/Game/project.godot index 26c1d098..f47e8a1f 100644 --- a/Game/project.godot +++ b/Game/project.godot @@ -119,3 +119,5 @@ roll_right={ + + diff --git a/Game/scripts/arena_boundary.gd b/Game/scripts/arena_boundary.gd index a9e54a2d..cae7af2d 100644 --- a/Game/scripts/arena_boundary.gd +++ b/Game/scripts/arena_boundary.gd @@ -11,3 +11,25 @@ const INNER_HEIGHT := 12.0 # Goal-centre distance from arena centre; the end walls sit 1 m behind, so a # ball pinned against them still overlaps the goal sensor. const GOAL_LINE_Z := 17.0 + +@onready var _wall_pos_x: MeshInstance3D = $WallPosXMesh +@onready var _wall_neg_x: MeshInstance3D = $WallNegXMesh +@onready var _wall_pos_z: MeshInstance3D = $WallPosZMesh +@onready var _wall_neg_z: MeshInstance3D = $WallNegZMesh +@onready var _ceiling: MeshInstance3D = $CeilingMesh + + +func _process(_delta: float) -> void: + # The translucent field material tints everything behind it, so any face + # the camera has crossed to the outside of is hidden entirely — looking + # into the arena from outside stays clear, while faces seen from inside + # keep their tint. Collision is untouched; only the meshes toggle. + var camera := get_viewport().get_camera_3d() + if camera == null: + return # headless (RL/CI) has no camera + var p := to_local(camera.global_position) + _wall_pos_x.visible = p.x < INNER_HALF_X + _wall_neg_x.visible = p.x > -INNER_HALF_X + _wall_pos_z.visible = p.z < INNER_HALF_Z + _wall_neg_z.visible = p.z > -INNER_HALF_Z + _ceiling.visible = p.y < INNER_HEIGHT diff --git a/Game/scripts/ship.gd b/Game/scripts/ship.gd index 323900ab..e47a4f51 100644 --- a/Game/scripts/ship.gd +++ b/Game/scripts/ship.gd @@ -19,8 +19,18 @@ extends RigidBody3D @export var drag_coefficient = 0.98 # Linear drag (air resistance) @export var angular_drag = 0.95 # Rotational drag +# Accent colours per team, applied to the nose and tail fin meshes so the +# two sides are tellable apart at a glance. +const TEAM_COLORS := { + 0: Color(0.25, 0.55, 1.0), + 1: Color(1.0, 0.5, 0.15), +} + # Which team this ship plays for (0 or 1). Set by the game mode on spawn. -var team: int = 0 +var team: int = 0: + set(value): + team = value + _apply_team_color() var controller: ShipController var _current_action: ShipAction = ShipAction.new() @@ -62,6 +72,25 @@ func _ready(): controller = child break + _apply_team_color() + + +func _apply_team_color() -> void: + if not is_inside_tree(): + return + var color: Color = TEAM_COLORS.get(team, TEAM_COLORS[0]) + var accent := StandardMaterial3D.new() + accent.albedo_color = color + accent.metallic = 0.3 + accent.roughness = 0.5 + accent.emission_enabled = true + accent.emission = color + accent.emission_energy_multiplier = 0.35 + for mesh_name in ["Nose", "TailFin"]: + var mesh := get_node_or_null(mesh_name) as MeshInstance3D + if mesh: + mesh.material_override = accent + # Attach the node that drives this ship (player, AI, or network). Replaces # any existing controller; parents the new one under the ship if needed. diff --git a/Game/scripts/ship_camera.gd b/Game/scripts/ship_camera.gd index 47be3be0..1c29fa00 100644 --- a/Game/scripts/ship_camera.gd +++ b/Game/scripts/ship_camera.gd @@ -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()