Fix multiplayer state: stacked players, ghosts after rejoin; add a build stamp
checkpoint / checkpoint (push) Successful in 1m40s
checkpoint / checkpoint (push) Successful in 1m40s
- Clients now hard-snap to the server's spawn position on the first snapshot. player.gd starts everyone on the fixed PlayerSpawn marker but the server spreads spawns out -- so one client saw players ~1m apart (correct) while the other saw them stacked (its local player still on the marker, never corrected because the ~1m drift was under the old 1.5m threshold). Threshold also tightened to 0.6m. - game.gd._exit_tree() closes the ENet connection cleanly when you leave a world, so the server fires peer_disconnected and removes your player. Before, leaving just changed scene and the server never knew -- everyone else kept seeing a frozen "ghost". - server.gd: once-a-second sweep of players whose peer is no longer connected, as a backstop for a missed disconnect. - Build stamp: checkpoint_ops bakes "<PC> <commit>" into application/config/version at export; the menu and in-world HUD show "build PC3 <hash>" so playtest reports aren't ambiguous about which build is running. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015jtxAiu5dAW4WxCNdGkrkJ
This commit is contained in:
+33
-5
@@ -14,23 +14,45 @@ extends Node3D
|
|||||||
|
|
||||||
const REMOTE_LERP := 12.0
|
const REMOTE_LERP := 12.0
|
||||||
## Snap the local player to the server position past this drift (metres).
|
## Snap the local player to the server position past this drift (metres).
|
||||||
## Generous -- only catches real desync / teleport / a wall the client walked
|
## Tight enough that the client always ends up where the server actually
|
||||||
## through, not normal prediction noise.
|
## spawned it (which is spread out, not the fixed marker player.gd uses).
|
||||||
const CORRECTION_THRESHOLD := 1.5
|
const CORRECTION_THRESHOLD := 0.6
|
||||||
|
|
||||||
@onready var _player: Player = $Player
|
@onready var _player: Player = $Player
|
||||||
|
|
||||||
var _net: NetworkClient = null
|
var _net: NetworkClient = null
|
||||||
var _networked := false
|
var _networked := false
|
||||||
|
var _aligned := false # snapped to the server spawn yet?
|
||||||
var _remotes: Dictionary = {} # peer_id -> Node3D
|
var _remotes: Dictionary = {} # peer_id -> Node3D
|
||||||
var _status_label: Label = null
|
var _status_label: Label = null
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
|
_show_build_stamp()
|
||||||
if Session.active:
|
if Session.active:
|
||||||
_setup_networked()
|
_setup_networked()
|
||||||
|
|
||||||
|
|
||||||
|
func _show_build_stamp() -> void:
|
||||||
|
var layer := CanvasLayer.new()
|
||||||
|
add_child(layer)
|
||||||
|
var lbl := Label.new()
|
||||||
|
lbl.text = "build %s" % ProjectSettings.get_setting("application/config/version", "dev")
|
||||||
|
lbl.add_theme_color_override("font_color", Color(1, 1, 1, 0.5))
|
||||||
|
lbl.add_theme_font_size_override("font_size", 13)
|
||||||
|
lbl.set_anchors_and_offsets_preset(Control.PRESET_BOTTOM_LEFT)
|
||||||
|
lbl.position = Vector2(8, -22)
|
||||||
|
layer.add_child(lbl)
|
||||||
|
|
||||||
|
|
||||||
|
func _exit_tree() -> void:
|
||||||
|
# Leaving the world -- close the ENet connection cleanly so the server
|
||||||
|
# fires peer_disconnected and removes our player (otherwise everyone else
|
||||||
|
# keeps seeing a frozen "ghost" of us).
|
||||||
|
if _net != null and is_instance_valid(_net):
|
||||||
|
_net.disconnect_from_server()
|
||||||
|
|
||||||
|
|
||||||
func _setup_networked() -> void:
|
func _setup_networked() -> void:
|
||||||
_show_status("Connecting to %s…" % Session.host)
|
_show_status("Connecting to %s…" % Session.host)
|
||||||
_net = NetworkClient.new()
|
_net = NetworkClient.new()
|
||||||
@@ -57,11 +79,17 @@ func _physics_process(_delta: float) -> void:
|
|||||||
var inp: Dictionary = _player.net_input
|
var inp: Dictionary = _player.net_input
|
||||||
_net.send_input(inp["move_x"], inp["move_z"], inp["jump"], inp["look_yaw"])
|
_net.send_input(inp["move_x"], inp["move_z"], inp["jump"], inp["look_yaw"])
|
||||||
|
|
||||||
# 2. Server-authoritative correction backstop.
|
# 2. Server-authoritative position.
|
||||||
var own: Dictionary = _net.get_own_server_state()
|
var own: Dictionary = _net.get_own_server_state()
|
||||||
if own.has("position"):
|
if own.has("position"):
|
||||||
var server_pos: Vector3 = own["position"]
|
var server_pos: Vector3 = own["position"]
|
||||||
if _player.global_position.distance_to(server_pos) > CORRECTION_THRESHOLD:
|
if not _aligned:
|
||||||
|
# First snapshot: hard-snap to where the server actually spawned
|
||||||
|
# us (player.gd starts everyone on the fixed marker, but the
|
||||||
|
# server spreads spawns out).
|
||||||
|
_player.apply_server_correction(server_pos)
|
||||||
|
_aligned = true
|
||||||
|
elif _player.global_position.distance_to(server_pos) > CORRECTION_THRESHOLD:
|
||||||
_player.apply_server_correction(server_pos)
|
_player.apply_server_correction(server_pos)
|
||||||
|
|
||||||
# 3. Remote players.
|
# 3. Remote players.
|
||||||
|
|||||||
@@ -50,6 +50,14 @@ var _is_authenticating: bool = false
|
|||||||
# ─── Lifecycle ────────────────────────────────────────────────────────────────
|
# ─── Lifecycle ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
|
var v := Label.new()
|
||||||
|
v.text = "build %s" % ProjectSettings.get_setting("application/config/version", "dev")
|
||||||
|
v.add_theme_color_override("font_color", Color(1, 1, 1, 0.4))
|
||||||
|
v.add_theme_font_size_override("font_size", 12)
|
||||||
|
v.set_anchors_and_offsets_preset(Control.PRESET_BOTTOM_RIGHT)
|
||||||
|
v.position = Vector2(-140, -22)
|
||||||
|
add_child(v)
|
||||||
|
|
||||||
# Cache node references.
|
# Cache node references.
|
||||||
_auth_buttons_row = $CenterContainer/VBoxContainer/AuthButtonsRow
|
_auth_buttons_row = $CenterContainer/VBoxContainer/AuthButtonsRow
|
||||||
_login_button = $CenterContainer/VBoxContainer/AuthButtonsRow/LoginButton
|
_login_button = $CenterContainer/VBoxContainer/AuthButtonsRow/LoginButton
|
||||||
|
|||||||
@@ -102,11 +102,27 @@ func _physics_process(_delta: float) -> void:
|
|||||||
if not is_running:
|
if not is_running:
|
||||||
return
|
return
|
||||||
tick += 1
|
tick += 1
|
||||||
|
if tick % PHYSICS_HZ == 0:
|
||||||
|
_sweep_stale_players()
|
||||||
_broadcast_snapshot()
|
_broadcast_snapshot()
|
||||||
if tick % STATUS_WRITE_EVERY == 0:
|
if tick % STATUS_WRITE_EVERY == 0:
|
||||||
_write_status()
|
_write_status()
|
||||||
|
|
||||||
|
|
||||||
|
## Backstop for _on_peer_disconnected: once a second, drop any player whose
|
||||||
|
## peer is no longer connected (covers a missed disconnect signal, so a
|
||||||
|
## rejoining client never sees a frozen ghost of its old self).
|
||||||
|
func _sweep_stale_players() -> void:
|
||||||
|
var connected := multiplayer.get_peers()
|
||||||
|
for peer_id in players.keys():
|
||||||
|
if peer_id not in connected:
|
||||||
|
var p: Node = players[peer_id]
|
||||||
|
if is_instance_valid(p):
|
||||||
|
p.queue_free()
|
||||||
|
players.erase(peer_id)
|
||||||
|
print("[Server] swept stale player %d (%d left)" % [peer_id, players.size()])
|
||||||
|
|
||||||
|
|
||||||
func _broadcast_snapshot() -> void:
|
func _broadcast_snapshot() -> void:
|
||||||
var snap := {"tick": tick, "players": {}}
|
var snap := {"tick": tick, "players": {}}
|
||||||
for peer_id in players:
|
for peer_id in players:
|
||||||
|
|||||||
Reference in New Issue
Block a user