mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-11 08:23:45 +00:00
chore(multiplayer): Phase 0 refactors + graphics/perf settings groundwork
Lands the non-networked Phase 0 tasks from multiplayer-todo.md (ship/camera/ arena refactors, sim constants, background FPS handling) plus a first pass at exposing graphics/performance settings (presets, resolution scaling, vsync, FPS cap, perf overlay) and a GPU profiling harness for the real-hardware follow-up in task 0.15b.
This commit is contained in:
+72
-7
@@ -1,5 +1,6 @@
|
||||
class_name Ship
|
||||
extends RigidBody3D
|
||||
const SimConstants = preload("res://scripts/sim_constants.gd")
|
||||
|
||||
# Physics-driven spaceship. All movement is force/torque-based, applied in
|
||||
# _integrate_forces from a ShipAction supplied by a pluggable ShipController
|
||||
@@ -44,7 +45,8 @@ extends RigidBody3D
|
||||
# Non-tinted hull meshes, runtime-merged into one ArrayMesh by
|
||||
# _build_merged_hull() (Nose/TailFin stay separate MeshInstance3Ds since
|
||||
# _apply_team_color() retints them per-team and must keep addressing them by
|
||||
# name). Verified via get_surface_count()/surface_get_material() before
|
||||
# name, under $Visual — see that function). Verified via
|
||||
# get_surface_count()/surface_get_material() before
|
||||
# writing this: hull and canopy are each a single surface with their own
|
||||
# distinct opaque StandardMaterial3D (canopy is NOT alpha/transparent despite
|
||||
# the name), and engine_l/engine_r are each 2 surfaces, also all distinct
|
||||
@@ -105,6 +107,43 @@ var _current_action: ShipAction = ShipAction.new()
|
||||
var _inert_action: ShipAction = ShipAction.new()
|
||||
var _boundary: ArenaBoundary
|
||||
|
||||
var _pending_teleport: Transform3D
|
||||
var _has_pending_teleport := false
|
||||
|
||||
|
||||
# Queues an authoritative teleport, applied at the top of the next
|
||||
# _integrate_forces — the only Jolt-safe place to write state.transform
|
||||
# directly (see GameMode._reset_body / task 0.15) — instead of racing the
|
||||
# physics step via set_deferred("global_transform", ...).
|
||||
func queue_teleport(to: Transform3D) -> void:
|
||||
_pending_teleport = to
|
||||
_has_pending_teleport = true
|
||||
|
||||
|
||||
# --- Netcode correction hooks (Phase 4; see multiplayer-todo.md §4.4) ---
|
||||
# Both stay zero until Phase 4 wires a reconciliation pass in, so the guarded
|
||||
# hook in _integrate_forces below is a no-op today.
|
||||
# Velocity delta from a soft correction, consumed once then cleared —
|
||||
# applied in full immediately (invisible to the player, and it's the
|
||||
# *cause* of future position error, so blending it just prolongs
|
||||
# divergence).
|
||||
var net_vel_correction := Vector3.ZERO
|
||||
# Rendered offset between the body and $Visual while a soft correction
|
||||
# decays away, so a position correction moves the collider in full without
|
||||
# visibly teleporting the mesh. Same decay convention as drag/righting
|
||||
# torque (_tick_scaled) above.
|
||||
var net_visual_offset := Vector3.ZERO
|
||||
const NET_VISUAL_OFFSET_DECAY := 0.88
|
||||
|
||||
|
||||
# Feeds thrust_z/turbo into the movement VFX for a ship with no local
|
||||
# controller driving _integrate_forces (a frozen remote ship never calls
|
||||
# get_action(), so _update_movement_vfx's engine glow/flame would otherwise
|
||||
# read a stale or zeroed action and show dead engines).
|
||||
func set_visual_action(thrust_z: float, turbo: bool) -> void:
|
||||
_current_action.thrust.z = thrust_z
|
||||
_current_action.turbo = turbo
|
||||
|
||||
# Instrument signals for efficient data distribution
|
||||
signal speed_changed(speed: float)
|
||||
signal attitude_changed(pitch: float, roll: float, yaw: float)
|
||||
@@ -135,6 +174,13 @@ var _engine_cores: Array[MeshInstance3D] = []
|
||||
var _engine_flames: Array[MeshInstance3D] = []
|
||||
var _engine_lights: Array[OmniLight3D] = []
|
||||
|
||||
# All rendered geometry (hull, canopy, engine cores/flames/lights, Nose,
|
||||
# TailFin) parents under this instead of the RigidBody3D directly, so a
|
||||
# future prediction correction (task 0.14) can offset the visual without
|
||||
# moving the collider — see multiplayer-todo.md task 0.2. CollisionShape3D
|
||||
# and the controller child correctly stay on the body itself.
|
||||
@onready var visual: Node3D = $Visual
|
||||
|
||||
|
||||
func _ready():
|
||||
# Add ship to group for instrument discovery
|
||||
@@ -177,7 +223,7 @@ func _apply_team_color() -> void:
|
||||
return
|
||||
var accent := _get_team_material(team)
|
||||
for mesh_name in ["Nose", "TailFin"]:
|
||||
var mesh := get_node_or_null(mesh_name) as MeshInstance3D
|
||||
var mesh := get_node_or_null("Visual/" + mesh_name) as MeshInstance3D
|
||||
if mesh:
|
||||
mesh.material_override = accent
|
||||
|
||||
@@ -205,7 +251,7 @@ func _build_merged_hull() -> void:
|
||||
var instance := MeshInstance3D.new()
|
||||
instance.name = "MergedHull"
|
||||
instance.mesh = mesh
|
||||
add_child(instance)
|
||||
visual.add_child(instance)
|
||||
|
||||
|
||||
# Attach the node that drives this ship (player, AI, or network). Replaces
|
||||
@@ -238,7 +284,7 @@ func _build_movement_vfx() -> void:
|
||||
core.position = engine_pos
|
||||
core.mesh = core_mesh
|
||||
core.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||
add_child(core)
|
||||
visual.add_child(core)
|
||||
_engine_cores.append(core)
|
||||
|
||||
# A single conventional orange flame replaces the layered particle plume
|
||||
@@ -265,7 +311,7 @@ func _build_movement_vfx() -> void:
|
||||
flame.mesh = flame_mesh
|
||||
flame.visible = false
|
||||
flame.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||
add_child(flame)
|
||||
visual.add_child(flame)
|
||||
_engine_flames.append(flame)
|
||||
|
||||
var light := OmniLight3D.new()
|
||||
@@ -275,7 +321,7 @@ func _build_movement_vfx() -> void:
|
||||
light.omni_range = 3.5
|
||||
light.omni_attenuation = 2.0
|
||||
light.shadow_enabled = false
|
||||
add_child(light)
|
||||
visual.add_child(light)
|
||||
_engine_lights.append(light)
|
||||
|
||||
func _vfx_material(color: Color, energy: float) -> StandardMaterial3D:
|
||||
@@ -343,6 +389,25 @@ func _has_telemetry_listeners() -> bool:
|
||||
|
||||
|
||||
func _integrate_forces(state):
|
||||
if _has_pending_teleport:
|
||||
_has_pending_teleport = false
|
||||
state.transform = _pending_teleport
|
||||
state.linear_velocity = Vector3.ZERO
|
||||
state.angular_velocity = Vector3.ZERO
|
||||
reset_physics_interpolation()
|
||||
|
||||
# --- Netcode correction hook (Phase 4) --- guarded: both fields default
|
||||
# to Vector3.ZERO and nothing writes them yet, so neither branch runs
|
||||
# today.
|
||||
if net_vel_correction != Vector3.ZERO:
|
||||
state.linear_velocity += net_vel_correction
|
||||
net_vel_correction = Vector3.ZERO
|
||||
if net_visual_offset != Vector3.ZERO:
|
||||
net_visual_offset *= _tick_scaled(NET_VISUAL_OFFSET_DECAY, state.step)
|
||||
if net_visual_offset.length_squared() < 0.0001:
|
||||
net_visual_offset = Vector3.ZERO
|
||||
visual.position = net_visual_offset
|
||||
|
||||
# One action per physics tick, pulled from the controller (deterministic)
|
||||
_current_action = controller.get_action() if controller else _inert_action
|
||||
|
||||
@@ -448,7 +513,7 @@ func apply_rotation_forces(state: PhysicsDirectBodyState3D, rotation_input: Vect
|
||||
# by the actual elapsed tick time `step`, so `v *= _tick_scaled(k, step)`
|
||||
# decays at the same rate per second regardless of physics_ticks_per_second.
|
||||
func _tick_scaled(k: float, step: float) -> float:
|
||||
return pow(k, step * 60.0)
|
||||
return pow(k, step * SimConstants.TICK_HZ)
|
||||
|
||||
|
||||
func apply_drag_and_limits(state: PhysicsDirectBodyState3D, rotation_input: Vector3):
|
||||
|
||||
Reference in New Issue
Block a user