M2.2: player bodies, name tags, shared island and checked movement

- Joining loads the server's island (seed, spawn, clock); the server
  generates the same terrain from its seed and keeps a world clock that
  pauses when empty
- Other players are procedural low-poly characters in their chosen colours
  with name tags (hidden by walls, fading with distance), walk/run/swim
  animation and held items
- Movement: local and instant, sent at 20 Hz stamped with the sender's
  clock; the server refuses impossible moves (speed, rising, leaving the
  island, underground) and sends the player back; flagged respawns allowed
- Avatars drawn 100 ms behind and interpolated, with brief extrapolation
- Online: no pausing, player list in the Esc menu, Disconnect back to the
  title (with the reason); animals off and world changes local-only until
  M2.3-M2.5, shown on the HUD
- test_net_world: real server + walking and cheating bots at 100 ms +/-
  30 ms simulated latency (27 checks)
This commit is contained in:
Paul Bailey
2026-11-04 15:08:14 +00:00
parent f62f71658f
commit d0f473e57b
14 changed files with 1093 additions and 29 deletions

View File

@@ -23,6 +23,8 @@ var _main: VBoxContainer
var _controls: VBoxContainer
var _quit_btn: Button
var _quit_armed := false
var _online := false ## on a server: no pausing, no saving here
var _players_label: Label
func bind(p: CharacterBody3D) -> void:
player = p
@@ -79,7 +81,15 @@ func bind(p: CharacterBody3D) -> void:
_main.add_child(sens_row)
var saved: bool = Saves.has_world()
if saved:
_online = Net.state == "connected"
if _online:
_players_label = UITheme.label("", 15)
_players_label.autowrap_mode = TextServer.AUTOWRAP_WORD
_players_label.custom_minimum_size = Vector2(340, 0)
_main.add_child(_players_label)
_main.add_child(_button("Disconnect", _on_disconnect))
_quit_btn = _button("Disconnect & Quit to Desktop", _on_quit)
elif saved:
_main.add_child(_button("Save & Quit to Title", _on_quit_to_title))
_quit_btn = _button("Save & Quit to Desktop", _on_quit)
else:
@@ -87,6 +97,8 @@ func bind(p: CharacterBody3D) -> void:
_main.add_child(_quit_btn)
var note_text := "Autosaves every 2 minutes. World: %s" % Saves.read_meta(Saves.current_id).get("name", "?") if saved \
else "Not playing a saved world: progress won't be kept."
if _online:
note_text = "The game keeps running while this menu is open."
var note := UITheme.label(note_text, 13, UITheme.TEXT_DIM)
note.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_main.add_child(note)
@@ -125,8 +137,14 @@ func open() -> void:
_show_controls(false)
_disarm_quit()
player.ui_open = true
get_tree().paused = true
get_tree().paused = not _online # a shared world can't be paused by one player
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
if _online:
var names: Array[String] = []
for p in Net.players.values():
names.append(String(p.name) + (" (admin)" if p.get("admin", false) else ""))
names.sort()
_players_label.text = "%s · %d online\n%s" % [Net.server_info.get("server_name", "Server"), names.size(), ", ".join(names)]
func close() -> void:
visible = false
@@ -148,7 +166,16 @@ func _show_controls(on: bool) -> void:
func _on_fullscreen(on: bool) -> void:
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_FULLSCREEN if on else DisplayServer.WINDOW_MODE_WINDOWED)
## Leaves the server (world.gd sees Net.left and goes back to the title).
func _on_disconnect() -> void:
close()
Net.leave()
func _on_quit() -> void:
if _online:
Net.leave()
get_tree().quit()
return
if Saves.has_world():
player.get_parent().save_now()
get_tree().quit()
@@ -169,4 +196,6 @@ func _on_quit_to_title() -> void:
func _disarm_quit() -> void:
_quit_armed = false
if _online:
return
_quit_btn.text = "Save & Quit to Desktop" if Saves.has_world() else "Quit to Desktop"