mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-10 16:04:04 +00:00
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:
+66
-5
@@ -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")
|
||||
|
||||
@@ -119,3 +119,5 @@ roll_right={
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
+30
-1
@@ -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.
|
||||
|
||||
+51
-46
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user