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

@@ -9,6 +9,69 @@ extends SceneTree
var _outcome := ""
var _reason := ""
var _pos := Vector3.ZERO
var _corrections := 0
var _others := {} ## name -> last position seen in a snapshot
var _terrain: Node3D
## Walks (and optionally cheats) on the server's island, sending positions
## 20 times a second like a real client. Corrections from the server are
## obeyed, as the real client does.
## --walk line --speed 4 --walk-time 3 walk along +X, then stand still
## --cheat teleport after 1 s, claim to be 60 m away
## --cheat outside walk straight out toward the world's edge at 7 m/s
## --cheat respawn a flagged teleport to the spawn (legitimate)
func _move(net: Node, args: Dictionary, res: Dictionary) -> void:
_terrain = load("res://scripts/world/terrain.gd").new()
root.add_child(_terrain) # takes its seed from the server (net.world_info)
_pos = net.world_info.get("spawn", Vector3.ZERO)
net.corrected.connect(func(p): _corrections += 1; _pos = p)
net.snapshot.connect(func(_t, states):
for s in states:
if net.players.has(int(s[0])):
_others[net.players[int(s[0])].name] = s[1])
var mode := String(args.get("walk", args.get("cheat", "")))
var speed := float(args.get("speed", 4.0))
var dur := float(args.get("walk-time", 3.0))
res.walk_start = [_pos.x, _pos.y, _pos.z]
var t := 0.0
var dir := Vector3(1, 0, 0)
if mode == "outside":
dir = Vector3(_pos.x, 0, _pos.z).normalized() if Vector2(_pos.x, _pos.z).length() > 1.0 else Vector3(1, 0, 0)
speed = 7.0
dur = 40.0
var step := 0.05
var prev_us := Time.get_ticks_usec()
while t < dur and net.state == "connected":
var now_us := Time.get_ticks_usec()
var real_dt := (now_us - prev_us) / 1000000.0
prev_us = now_us
var teleported := false
var anim := 1
match mode:
"teleport":
if absf(t - 1.0) < step * 0.5:
_pos += Vector3(60, 0, 0)
anim = 0
"respawn":
if absf(t - 1.0) < step * 0.5:
var sp: Vector3 = net.world_info.get("island_spawn", _pos)
_pos = sp + Vector3(0.5, 0, 0.5)
teleported = true
anim = 0
_:
_pos += dir * speed * real_dt # real elapsed time, like the game's physics
_pos.y = _terrain.sample_height(_pos.x, _pos.z)
net.send_state(_pos, 0.0, 0.0, anim, "", teleported)
await create_timer(step).timeout
t += step
if mode == "outside" and _corrections > 0 and t > 1.0:
break
res.walk_end = [_pos.x, _pos.y, _pos.z]
# Stand still for a moment, still sending, so the last position settles.
for _i in range(10):
net.send_state(_pos, 0.0, 0.0, 0, "", false)
await create_timer(step).timeout
func _initialize() -> void:
_run.call_deferred()
@@ -35,6 +98,8 @@ func _run() -> void:
res.joined = true
res.server_name = net.server_info.get("server_name", "")
res.admin = bool(net.players.get(net.local_peer, {}).get("admin", false))
if args.has("walk") or args.has("cheat"):
await _move(net, args, res)
var saw := {}
var stay := float(args.get("stay", 2.0))
var s := 0.0
@@ -44,6 +109,11 @@ func _run() -> void:
await create_timer(0.1).timeout
s += 0.1
res.saw = saw.keys()
res.corrections = _corrections
res.final_pos = [_pos.x, _pos.y, _pos.z]
if _others.has("Alice"):
var ap: Vector3 = _others.Alice
res.alice_pos = [ap.x, ap.y, ap.z]
net.leave()
for _i in range(10):
await process_frame