mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-17 09:12:07 +00:00
Compare commits
11 Commits
772f98b7fe
...
4f075b352f
| Author | SHA1 | Date | |
|---|---|---|---|
| 4f075b352f | |||
| dfb7285704 | |||
| d917d2742b | |||
| cd94252f92 | |||
| c517a85508 | |||
| 4c227c24b4 | |||
| 80e9b676d5 | |||
| c8f052854c | |||
| 4e2d406aa2 | |||
| 7777280062 | |||
| 5973d0893b |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+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"]
|
[ext_resource type="Script" uid="uid://dyq1n7q1bqjps" path="res://scripts/ship.gd" id="1_efag7"]
|
||||||
|
|
||||||
@@ -6,8 +6,52 @@
|
|||||||
friction = 0.1
|
friction = 0.1
|
||||||
bounce = 0.2
|
bounce = 0.2
|
||||||
|
|
||||||
[sub_resource type="BoxMesh" id="BoxMesh_efag7"]
|
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_hull"]
|
||||||
size = Vector3(1, 1, 4)
|
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"]
|
[sub_resource type="BoxShape3D" id="BoxShape3D_dsjou"]
|
||||||
size = Vector3(1, 1, 4)
|
size = Vector3(1, 1, 4)
|
||||||
@@ -18,8 +62,25 @@ physics_material_override = SubResource("PhysicsMaterial_ship")
|
|||||||
inertia = Vector3(1, 1, 1)
|
inertia = Vector3(1, 1, 1)
|
||||||
script = ExtResource("1_efag7")
|
script = ExtResource("1_efag7")
|
||||||
|
|
||||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="."]
|
[node name="Hull" type="MeshInstance3D" parent="."]
|
||||||
mesh = SubResource("BoxMesh_efag7")
|
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="."]
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
|
||||||
shape = SubResource("BoxShape3D_dsjou")
|
shape = SubResource("BoxShape3D_dsjou")
|
||||||
|
|||||||
@@ -119,3 +119,5 @@ roll_right={
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
[node name="Match" type="Node3D"]
|
[node name="Match" type="Node3D"]
|
||||||
script = ExtResource("1_m")
|
script = ExtResource("1_m")
|
||||||
bot_model_path = "res://bots/rookie.json"
|
bot_model_path = "res://bots/run03.json"
|
||||||
|
|
||||||
[node name="Arena" parent="." instance=ExtResource("2_m")]
|
[node name="Arena" parent="." instance=ExtResource("2_m")]
|
||||||
|
|
||||||
|
|||||||
@@ -11,3 +11,25 @@ const INNER_HEIGHT := 12.0
|
|||||||
# Goal-centre distance from arena centre; the end walls sit 1 m behind, so a
|
# Goal-centre distance from arena centre; the end walls sit 1 m behind, so a
|
||||||
# ball pinned against them still overlaps the goal sensor.
|
# ball pinned against them still overlaps the goal sensor.
|
||||||
const GOAL_LINE_Z := 17.0
|
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 drag_coefficient = 0.98 # Linear drag (air resistance)
|
||||||
@export var angular_drag = 0.95 # Rotational drag
|
@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.
|
# 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 controller: ShipController
|
||||||
var _current_action: ShipAction = ShipAction.new()
|
var _current_action: ShipAction = ShipAction.new()
|
||||||
@@ -62,6 +72,25 @@ func _ready():
|
|||||||
controller = child
|
controller = child
|
||||||
break
|
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
|
# Attach the node that drives this ship (player, AI, or network). Replaces
|
||||||
# any existing controller; parents the new one under the ship if needed.
|
# any existing controller; parents the new one under the ship if needed.
|
||||||
|
|||||||
@@ -15,14 +15,39 @@ extends AIController3D
|
|||||||
# Reward shaping weights. Dense terms accrue per physics tick (60 sim-ticks
|
# Reward shaping weights. Dense terms accrue per physics tick (60 sim-ticks
|
||||||
# per sim-second); event terms fire once. Exported so tuning needs no code
|
# per sim-second); event terms fire once. Exported so tuning needs no code
|
||||||
# edits. Goal rewards are added by TrainingMode, which owns goal events.
|
# edits. Goal rewards are added by TrainingMode, which owns goal events.
|
||||||
@export var ball_touch_reward := 0.25
|
@export var ball_touch_reward := 1.0
|
||||||
@export var velocity_to_ball_weight := 0.001
|
@export var velocity_to_ball_weight := 0.02
|
||||||
@export var ball_velocity_to_goal_weight := 0.004
|
@export var ball_velocity_to_goal_weight := 0.004
|
||||||
# Per-tick penalty while touching the arena enclosure (walls/floor/ceiling).
|
# Per-tick penalty scaled by distance to the ball (full value at the arena's
|
||||||
# At 60 ticks/sim-second this is -0.3/s: a ship parked on a wall for a full
|
# far diagonal, 0 on top of the ball). Run04 lesson: with idling worth a flat
|
||||||
# 30 s episode loses ~9 — comparable to conceding — while a brief graze
|
# 0, camping in a corner strictly dominated risking the wall/tilt penalties
|
||||||
# costs almost nothing.
|
# to chase the ball — this makes "do nothing far from the ball" the worst
|
||||||
@export var wall_contact_penalty := 0.005
|
# option instead of the safest. A penalty, not a proximity bonus, so orbiting
|
||||||
|
# the ball farms nothing.
|
||||||
|
@export var ball_distance_penalty := 0.002
|
||||||
|
# Per-tick penalty while pressed against a side wall, end wall, or the
|
||||||
|
# ceiling — NOT the floor (run03 lesson: taxing floor contact punishes the
|
||||||
|
# ship's natural low flight and drowns every other signal). At 60 ticks per
|
||||||
|
# sim-second this is -0.15/s. Halved for run05: the ball lives near walls,
|
||||||
|
# and the old -0.3/s made the productive region of the pitch aversive
|
||||||
|
# relative to the (then far weaker) ball-seeking shaping.
|
||||||
|
@export var wall_contact_penalty := 0.0025
|
||||||
|
# Per-tick penalty for not being upright, scaled by tilt: 0 when flat, full
|
||||||
|
# value (-0.12/s) when inverted. A penalty rather than an upright bonus so a
|
||||||
|
# flat, idle ship farms nothing.
|
||||||
|
@export var tilt_penalty := 0.002
|
||||||
|
|
||||||
|
# Contact normals with y above this are floor contact (exempt from the wall
|
||||||
|
# penalty); below it they read as wall (sideways) or ceiling (downward).
|
||||||
|
const FLOOR_NORMAL_MIN_Y := 0.7
|
||||||
|
|
||||||
|
# Longest possible ship-to-ball separation: the enclosure's interior diagonal.
|
||||||
|
# Normalizes ball_distance_penalty so its export is the worst-case per-tick cost.
|
||||||
|
const MAX_BALL_DISTANCE := sqrt(
|
||||||
|
(2.0 * ArenaBoundary.INNER_HALF_X) ** 2
|
||||||
|
+ (2.0 * ArenaBoundary.INNER_HALF_Z) ** 2
|
||||||
|
+ ArenaBoundary.INNER_HEIGHT ** 2
|
||||||
|
)
|
||||||
|
|
||||||
var ship: Ship
|
var ship: Ship
|
||||||
var rl_controller: RLShipController
|
var rl_controller: RLShipController
|
||||||
@@ -83,20 +108,44 @@ func _physics_process(delta):
|
|||||||
var closing_speed := ship.linear_velocity.dot(to_ball.normalized())
|
var closing_speed := ship.linear_velocity.dot(to_ball.normalized())
|
||||||
reward += velocity_to_ball_weight * closing_speed / ship.max_speed
|
reward += velocity_to_ball_weight * closing_speed / ship.max_speed
|
||||||
|
|
||||||
|
# Dense penalty: distance to the ball, so idling far away bleeds reward
|
||||||
|
# instead of scoring a safe zero (see ball_distance_penalty).
|
||||||
|
if ball_distance_penalty > 0.0:
|
||||||
|
reward -= ball_distance_penalty * to_ball.length() / MAX_BALL_DISTANCE
|
||||||
|
|
||||||
# Dense shaping: ball velocity toward the goal we attack
|
# Dense shaping: ball velocity toward the goal we attack
|
||||||
var ball_to_goal := attack_goal_position - ball.global_position
|
var ball_to_goal := attack_goal_position - ball.global_position
|
||||||
if ball_to_goal.length_squared() > 0.0001:
|
if ball_to_goal.length_squared() > 0.0001:
|
||||||
var ball_progress := ball.linear_velocity.dot(ball_to_goal.normalized())
|
var ball_progress := ball.linear_velocity.dot(ball_to_goal.normalized())
|
||||||
reward += ball_velocity_to_goal_weight * ball_progress / ShipObservations.BALL_SPEED_SCALE
|
reward += ball_velocity_to_goal_weight * ball_progress / ShipObservations.BALL_SPEED_SCALE
|
||||||
|
|
||||||
# Dense penalty: every tick spent in contact with the arena enclosure
|
# Dense penalty: every tick spent pressed against a wall or the ceiling
|
||||||
# (contact monitoring is already on for the ball-touch reward). Ships
|
# (contact monitoring is already on for the ball-touch reward). Ships
|
||||||
# bumping each other or the ball is fine — only the boundary counts.
|
# bumping each other, the ball, or the floor is fine. The boundary is one
|
||||||
if wall_contact_penalty > 0.0:
|
# body, so the contact normal tells us which surface: floor contact
|
||||||
for body in ship.get_colliding_bodies():
|
# pushes the ship up (+Y), walls push sideways, the ceiling down.
|
||||||
if body is ArenaBoundary:
|
if wall_contact_penalty > 0.0 and _wall_or_ceiling_contact():
|
||||||
reward -= wall_contact_penalty
|
reward -= wall_contact_penalty
|
||||||
break
|
|
||||||
|
# Dense penalty: tilt away from upright (0 flat, max when inverted) —
|
||||||
|
# discourages ending up on a side or roof without rewarding idleness.
|
||||||
|
if tilt_penalty > 0.0:
|
||||||
|
var uprightness: float = ship.global_transform.basis.y.dot(Vector3.UP)
|
||||||
|
reward -= tilt_penalty * (1.0 - uprightness) * 0.5
|
||||||
|
|
||||||
|
|
||||||
|
func _wall_or_ceiling_contact() -> bool:
|
||||||
|
var state := PhysicsServer3D.body_get_direct_state(ship.get_rid())
|
||||||
|
if state == null:
|
||||||
|
return false
|
||||||
|
for i in state.get_contact_count():
|
||||||
|
if not state.get_contact_collider_object(i) is ArenaBoundary:
|
||||||
|
continue
|
||||||
|
# Normal points from the surface into the ship: floor ≈ +Y (exempt),
|
||||||
|
# anything flatter or downward is a wall or the ceiling.
|
||||||
|
if state.get_contact_local_normal(i).y < FLOOR_NORMAL_MIN_Y:
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
func _on_ship_body_entered(body: Node) -> void:
|
func _on_ship_body_entered(body: Node) -> void:
|
||||||
|
|||||||
+51
-46
@@ -9,8 +9,11 @@ extends Node3D
|
|||||||
signal camera_mode_changed(is_ball_cam: bool)
|
signal camera_mode_changed(is_ball_cam: bool)
|
||||||
|
|
||||||
@export var camera_distance := 8.0
|
@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 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 target: Ship
|
||||||
var ball_cam_enabled := true
|
var ball_cam_enabled := true
|
||||||
@@ -18,6 +21,10 @@ var ball_cam_enabled := true
|
|||||||
@onready var camera: Camera3D = $Camera3D
|
@onready var camera: Camera3D = $Camera3D
|
||||||
|
|
||||||
var _ball: Node3D
|
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():
|
func _ready():
|
||||||
@@ -48,60 +55,58 @@ func _get_ball() -> Node3D:
|
|||||||
|
|
||||||
|
|
||||||
func _update_ball_cam(delta, ball: Node3D):
|
func _update_ball_cam(delta, ball: Node3D):
|
||||||
# In ball cam, camera positions itself so the ship is between camera and ball
|
# Ball cam keeps the ship between the camera and the ball: the camera sits
|
||||||
# Physics: Vector mathematics for 3D positioning
|
# on the ball→ship line (horizontal component only), looking at the ball,
|
||||||
var ship_pos = target.global_transform.origin
|
# so the ship stays low-centre in frame and the ball stays centred.
|
||||||
var ball_pos = ball.global_transform.origin
|
var ship_pos: Vector3 = target.global_transform.origin
|
||||||
|
var ball_pos: Vector3 = ball.global_transform.origin
|
||||||
|
|
||||||
# Calculate direction from ball to ship
|
# Smooth the orbit direction in angle space rather than lerping the camera
|
||||||
# Physics: Vector subtraction and normalization
|
# position directly — when ball and ship pass close to each other the raw
|
||||||
# Direction vector: d̂ = (P₂ - P₁) / |P₂ - P₁|
|
# direction flips instantly, and slerping the direction turns that into a
|
||||||
var ball_to_ship = (ship_pos - ball_pos).normalized()
|
# 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
|
var camera_target_pos := ship_pos + _orbit_dir * camera_distance + Vector3.UP * camera_height
|
||||||
# This ensures the ship is always between the camera and ball
|
camera_target_pos.y = maxf(camera_target_pos.y, min_camera_height)
|
||||||
# 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
|
var pos_t := 1.0 - exp(-camera_smoothing * delta)
|
||||||
# Physics: Linear interpolation (LERP) for smooth motion
|
camera.global_position = camera.global_position.lerp(camera_target_pos, pos_t)
|
||||||
# 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
|
# Look slightly above the ball's centre; look smoothing is faster than
|
||||||
if camera.global_transform.origin.distance_to(ball_pos) > 0.1:
|
# position smoothing so the ball never drifts out of frame while the
|
||||||
# Calculate direction to ball
|
# camera is still swinging into place.
|
||||||
var camera_pos = camera.global_transform.origin
|
_smooth_look_at(ball_pos + Vector3.UP * 0.5, delta)
|
||||||
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):
|
func _update_ship_cam(delta):
|
||||||
# In ship cam, camera follows and looks in the same direction as the ship
|
# In ship cam, camera follows and looks in the same direction as the ship
|
||||||
var ship_pos = target.global_transform.origin
|
var ship_pos: Vector3 = target.global_transform.origin
|
||||||
var ship_forward = -target.global_transform.basis.z
|
var ship_forward: Vector3 = -target.global_transform.basis.z
|
||||||
|
|
||||||
# Position camera behind and above the ship
|
# 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
|
var pos_t := 1.0 - exp(-camera_smoothing * delta)
|
||||||
camera.global_transform.origin = camera.global_transform.origin.lerp(camera_target_pos, 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
|
# Look ahead of the ship
|
||||||
var look_target = ship_pos + ship_forward * 10.0 # Look ahead of the ship
|
_smooth_look_at(ship_pos + ship_forward * 10.0, delta)
|
||||||
camera.look_at(look_target, Vector3.UP)
|
|
||||||
|
|
||||||
|
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()
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user