Fix multiplayer state: stacked players, ghosts after rejoin; add a build stamp
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:
2026-09-06 00:39:30 +00:00
parent da4e1420ee
commit 3639fb3132
3 changed files with 57 additions and 5 deletions
+33 -5
View File
@@ -14,23 +14,45 @@ extends Node3D
const REMOTE_LERP := 12.0
## Snap the local player to the server position past this drift (metres).
## Generous -- only catches real desync / teleport / a wall the client walked
## through, not normal prediction noise.
const CORRECTION_THRESHOLD := 1.5
## Tight enough that the client always ends up where the server actually
## spawned it (which is spread out, not the fixed marker player.gd uses).
const CORRECTION_THRESHOLD := 0.6
@onready var _player: Player = $Player
var _net: NetworkClient = null
var _networked := false
var _aligned := false # snapped to the server spawn yet?
var _remotes: Dictionary = {} # peer_id -> Node3D
var _status_label: Label = null
func _ready() -> void:
_show_build_stamp()
if Session.active:
_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:
_show_status("Connecting to %s…" % Session.host)
_net = NetworkClient.new()
@@ -57,11 +79,17 @@ func _physics_process(_delta: float) -> void:
var inp: Dictionary = _player.net_input
_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()
if own.has("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)
# 3. Remote players.