diff --git a/scripts/game.gd b/scripts/game.gd index b120261..5a6974b 100644 --- a/scripts/game.gd +++ b/scripts/game.gd @@ -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. diff --git a/scripts/main_menu.gd b/scripts/main_menu.gd index 617623c..df8edcb 100644 --- a/scripts/main_menu.gd +++ b/scripts/main_menu.gd @@ -50,6 +50,14 @@ var _is_authenticating: bool = false # ─── Lifecycle ──────────────────────────────────────────────────────────────── 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. _auth_buttons_row = $CenterContainer/VBoxContainer/AuthButtonsRow _login_button = $CenterContainer/VBoxContainer/AuthButtonsRow/LoginButton diff --git a/scripts/server.gd b/scripts/server.gd index 2061ce1..9545c53 100644 --- a/scripts/server.gd +++ b/scripts/server.gd @@ -102,11 +102,27 @@ func _physics_process(_delta: float) -> void: if not is_running: return tick += 1 + if tick % PHYSICS_HZ == 0: + _sweep_stale_players() _broadcast_snapshot() if tick % STATUS_WRITE_EVERY == 0: _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: var snap := {"tick": tick, "players": {}} for peer_id in players: