fix: a lot of networking bugs

This commit is contained in:
DottsGit
2026-06-06 19:26:22 -04:00
parent ff3e01a9e5
commit dbbfbef4a4
7 changed files with 194 additions and 109 deletions
+133 -81
View File
@@ -54,7 +54,13 @@ var grapple_rope: MeshInstance3D
var synced_movement_state: String = "idle"
var synced_movement_speed: float = 0.0
var synced_is_crouching: bool = false
var synced_weapon_path: String = ""
@export var synced_weapon_path: String = ""
@export var synced_grapple_point: Vector3 = Vector3.ZERO
@export var synced_is_grapple_shooting: bool = false
# For homing rocket
@export var synced_is_targeting: bool = false
@export var synced_homing_target_pos: Vector3 = Vector3.ZERO
var synced_loadout_p1: String = ""
var synced_loadout_p2: String = ""
@@ -315,7 +321,17 @@ func _spawn_remote_tracer(origin: Vector3, target: Vector3) -> void:
tracer.look_at(target, up_vec)
func _spawn_remote_projectile(origin: Vector3, fire_dir: Vector3, weapon_name: String) -> void:
# Spawn a visual-only projectile (no damage, just the mesh flying)
# Try to find the actual weapon to spawn its custom projectile so it looks identical!
if camera:
var wman = camera.get_node_or_null("WeaponManager")
if wman:
for w in wman.weapons.values():
if "weapon_name" in w and w.weapon_name == weapon_name:
if w.has_method("_spawn_custom_projectile"):
w._spawn_custom_projectile(origin, fire_dir)
return
# Fallback if weapon not found (e.g. joined late and weapons didn't build yet)
var proj = Node3D.new()
proj.name = "RemoteProjectile"
@@ -339,7 +355,7 @@ func _spawn_remote_projectile(origin: Vector3, fire_dir: Vector3, weapon_name: S
light.omni_range = 4.0
proj.add_child(light)
else:
# Rocket-shaped mesh (cone + cylinder)
# Generic rocket fallback
var mesh_inst = MeshInstance3D.new()
var p_mesh = CylinderMesh.new()
p_mesh.top_radius = 0.0
@@ -354,7 +370,6 @@ func _spawn_remote_projectile(origin: Vector3, fire_dir: Vector3, weapon_name: S
mesh_inst.rotation.x = deg_to_rad(-90)
proj.add_child(mesh_inst)
# Smoke trail
var smoke = GPUParticles3D.new()
var smoke_mat = ParticleProcessMaterial.new()
smoke_mat.direction = Vector3(0, 0, 1)
@@ -468,7 +483,7 @@ func rpc_take_damage(amount: float, hit_pos: Vector3, attacker_id: int, weapon_n
health = 0.0
die(impulse)
if is_multiplayer_authority():
if multiplayer.is_server():
if get_node_or_null("/root/NetworkManager") and multiplayer.has_multiplayer_peer():
var victim_id = multiplayer.get_unique_id()
var killer_id = attacker_id
@@ -512,12 +527,56 @@ func _ensure_machine() -> MovementStateMachine:
_machine = null
return null
var _client_input_dir: Vector2 = Vector2.ZERO
var _client_wish_dir_world: Vector3 = Vector3.ZERO
var _client_jump: bool = false
var _client_jump_just_pressed: bool = false
var _client_crouch: bool = false
var _client_dash: bool = false
var _client_grapple: bool = false
var _client_grapple_just_pressed: bool = false
@rpc("any_peer", "call_local", "unreliable")
func server_receive_inputs(input_dir: Vector2, wish_dir: Vector3, jump: bool, jump_just: bool, crouch: bool, dash: bool, grapple: bool, grapple_just: bool) -> void:
if not multiplayer.is_server(): return
var sender = multiplayer.get_remote_sender_id()
if sender != str(name).to_int() and sender != 1: return # Accept from owner or host self
_client_input_dir = input_dir
_client_wish_dir_world = wish_dir
_client_jump = jump
_client_jump_just_pressed = jump_just
_client_crouch = crouch
_client_dash = dash
_client_grapple = grapple
_client_grapple_just_pressed = grapple_just
func _physics_process(_delta: float) -> void:
if not is_multiplayer_authority(): return
# Local client captures input and sends it
if is_multiplayer_authority():
var raw_input := Input.get_vector("move_left", "move_right", "move_forward", "move_back")
var forward := -global_transform.basis.z
forward.y = 0.0
forward = forward.normalized()
var right := global_transform.basis.x
right.y = 0.0
right = right.normalized()
var world_dir := (forward * (-raw_input.y) + right * raw_input.x)
if world_dir.length_squared() > 1.0:
world_dir = world_dir.normalized()
var input_jump = Input.is_action_pressed("jump")
var input_jump_just = Input.is_action_just_pressed("jump")
var input_crouch = Input.is_action_pressed("crouch")
var input_dash = Input.is_action_just_pressed("dash")
var input_grapple = Input.is_action_pressed("grapple")
var input_grapple_just = Input.is_action_just_pressed("grapple")
server_receive_inputs.rpc_id(1, raw_input, world_dir, input_jump, input_jump_just, input_crouch, input_dash, input_grapple, input_grapple_just)
var speed = velocity.length()
var wind_factor = clampf((speed - 10.0) / 25.0, 0.0, 1.0)
# Decibel scale is non-linear, so lerping from -80 means it stays inaudible for a long time.
# -40 db is practically silent for a background loop, -15 db is our max quiet wind.
wind_player.volume_db = lerpf(-40.0, -15.0, wind_factor)
if not wind_player.playing and wind_factor > 0.0:
wind_player.play()
@@ -526,67 +585,51 @@ func _physics_process(_delta: float) -> void:
if not sm:
return
# Poll input in _physics_process so it's synchronized with the state machine tick
sm.input_jump_just_pressed = Input.is_action_just_pressed("jump")
sm.input_jump_pressed = Input.is_action_pressed("jump")
sm.input_crouch = Input.is_action_pressed("crouch")
sm.input_dash = Input.is_action_just_pressed("dash")
sm.input_grapple = Input.is_action_pressed("grapple")
sm.input_grapple_just_pressed = Input.is_action_just_pressed("grapple")
# Only the Server sets input values to the StateMachine for evaluation
if multiplayer.is_server():
sm.input_dir = _client_input_dir
sm.wish_dir_world = _client_wish_dir_world
sm.input_jump_pressed = _client_jump
sm.input_jump_just_pressed = _client_jump_just_pressed
sm.input_crouch = _client_crouch
sm.input_dash = _client_dash
sm.input_grapple = _client_grapple
sm.input_grapple_just_pressed = _client_grapple_just_pressed
# Reset one-frame actions
_client_jump_just_pressed = false
_client_dash = false
_client_grapple_just_pressed = false
# Update grapple rope visuals
if (sm.current_state == "grapple" or sm.is_grapple_shooting) and camera:
# Update synced properties for the grapple
if sm.current_state == "grapple" or sm.is_grapple_shooting:
synced_grapple_point = sm.grapple_point
synced_is_grapple_shooting = sm.is_grapple_shooting
# Update grapple rope visuals (runs on all clients for smooth interpolation)
if (synced_movement_state == "grapple" or synced_is_grapple_shooting) and camera:
grapple_rope.visible = true
# For shoot, start at right side of camera slightly below and closer
var start_pos = camera.global_position + camera.global_transform.basis * Vector3(0.3, -0.3, -0.15)
var end_pos = sm.grapple_point
if sm.is_grapple_shooting and sm.grapple_travel_time > 0.0:
# Interpolate end_pos
var t = clampf(sm.grapple_shoot_time / sm.grapple_travel_time, 0.0, 1.0)
end_pos = start_pos.lerp(sm.grapple_point, t)
var end_pos = synced_grapple_point
var dist = start_pos.distance_to(end_pos)
if dist > 0.01:
grapple_rope.global_position = (start_pos + end_pos) / 2.0
# Look at end point
var up_vec = Vector3.UP
var dir = (end_pos - start_pos).normalized()
if abs(dir.dot(up_vec)) > 0.99:
up_vec = Vector3.RIGHT
# Set rotation first, which resets scale
grapple_rope.transform.basis = Basis.looking_at(dir, up_vec) * Basis.from_euler(Vector3(PI/2.0, 0, 0))
# Then apply the scale
grapple_rope.scale = Vector3(1.0, dist, 1.0)
else:
if grapple_rope:
grapple_rope.visible = false
# Swing sound
if sm.current_state == "grapple" and sm.player.velocity.length() > 5.0:
if synced_movement_state == "grapple" and velocity.length() > 5.0:
grapple_swing_player.volume_db = lerpf(grapple_swing_player.volume_db, -15.0, _delta * 10.0)
else:
grapple_swing_player.volume_db = lerpf(grapple_swing_player.volume_db, -80.0, _delta * 15.0)
# Raw 2D input
var raw_input := Input.get_vector("move_left", "move_right", "move_forward", "move_back")
# Transform input direction by player yaw so movement is camera-relative
var forward := -global_transform.basis.z
forward.y = 0.0
forward = forward.normalized()
var right := global_transform.basis.x
right.y = 0.0
right = right.normalized()
var world_dir := (forward * (-raw_input.y) + right * raw_input.x)
if world_dir.length_squared() > 1.0:
world_dir = world_dir.normalized()
sm.input_dir = raw_input
sm.wish_dir_world = world_dir
# Update humanoid model animation state (runs everywhere)
# Update humanoid model animation state
var humanoid = get_node_or_null("HumanoidModel")
@@ -873,44 +916,53 @@ func _on_respawn_pressed() -> void:
@rpc("any_peer", "call_local", "reliable")
func rpc_respawn(spawn_pos: Vector3) -> void:
is_dead = false
health = max_health
shield = 0.0
if is_instance_valid(ragdoll_instance):
if ragdoll_instance.has_meta("spring_arm"):
var sa = ragdoll_instance.get_meta("spring_arm")
if is_instance_valid(sa):
sa.queue_free()
ragdoll_instance.queue_free()
if is_dead:
is_dead = false
health = max_health
shield = max_shield
time_since_last_damage = 0.0
# Restore camera parent
if is_instance_valid(camera) and is_instance_valid(head_pivot):
var p = camera.get_parent()
if p != head_pivot:
p.remove_child(camera)
head_pivot.add_child(camera)
camera.position = Vector3.ZERO
camera.rotation = Vector3.ZERO
if is_instance_valid(ragdoll_instance):
if ragdoll_instance.has_meta("spring_arm"):
var sa = ragdoll_instance.get_meta("spring_arm")
if is_instance_valid(sa):
sa.queue_free()
ragdoll_instance.queue_free()
visible = true
process_mode = Node.PROCESS_MODE_INHERIT
# Restore camera parent
if is_instance_valid(camera) and is_instance_valid(head_pivot):
var p = camera.get_parent()
if p != head_pivot:
p.remove_child(camera)
head_pivot.add_child(camera)
camera.position = Vector3.ZERO
camera.rotation = Vector3.ZERO
# Re-enable movement state machine
if _machine:
_machine.process_mode = Node.PROCESS_MODE_INHERIT
# Re-enable movement state machine
if _machine:
_machine.process_mode = Node.PROCESS_MODE_INHERIT
# Re-enable collision
var col = get_node_or_null("CollisionShape3D")
if col:
col.set_deferred("disabled", false)
var visual_model = get_node_or_null("HumanoidModel")
if visual_model:
visual_model.visible = true
# Re-enable collision
var col = get_node_or_null("CollisionShape3D")
if col:
col.set_deferred("disabled", false)
var visual_model = get_node_or_null("HumanoidModel")
if visual_model:
visual_model.visible = true
# Server sets the actual position to sync to everyone
if multiplayer.is_server() or not multiplayer.has_multiplayer_peer() or multiplayer.multiplayer_peer is OfflineMultiplayerPeer:
position = spawn_pos
velocity = Vector3.ZERO
# Local client resets UI and rebuilds weapons for their view
if is_multiplayer_authority():
if death_screen:
death_screen.visible = false
position = spawn_pos
velocity = Vector3.ZERO
# Optional: reset weapons
var wman = camera.get_node_or_null("WeaponManager") if camera else null