- 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)
133 lines
4.4 KiB
GDScript
133 lines
4.4 KiB
GDScript
extends Node
|
|
## In a multiplayer world: sends where you are, shows where everyone else is.
|
|
## Added by world.gd when the game is connected to a server.
|
|
##
|
|
## - Your own movement stays instant (you move locally and tell the server).
|
|
## The server checks each move; if it refuses one it sends you back to your
|
|
## last good position.
|
|
## - Other players are avatars (avatar.gd), drawn INTERP_DELAY behind the
|
|
## server's clock and interpolated between its snapshots.
|
|
## - The time of day follows the server, so everyone shares the same sky
|
|
## (weather follows from the seed and the time).
|
|
|
|
const AvatarScript = preload("res://scripts/net/avatar.gd")
|
|
const INTERP_DELAY := 0.1
|
|
|
|
var world: Node3D
|
|
var player: CharacterBody3D
|
|
var avatars: Dictionary = {} ## peer -> avatar.gd
|
|
var corrections := 0
|
|
var _send_left := 0.0
|
|
var _offset := INF ## server time minus local time (smoothed)
|
|
var _teleport_next := false
|
|
var _root: Node3D
|
|
|
|
func setup(w: Node3D) -> void:
|
|
world = w
|
|
player = w.player
|
|
_root = Node3D.new()
|
|
_root.name = "Avatars"
|
|
w.add_child(_root)
|
|
Net.snapshot.connect(_on_snapshot)
|
|
Net.corrected.connect(_on_corrected)
|
|
Net.clock.connect(_on_clock)
|
|
Net.players_changed.connect(_sync_avatars)
|
|
Net.player_joined.connect(func(n): Actions.notify.emit("%s joined" % n))
|
|
Net.player_left.connect(func(n): Actions.notify.emit("%s left" % n))
|
|
if Net.world_info.has("day"):
|
|
_on_clock(int(Net.world_info.day), float(Net.world_info.time))
|
|
|
|
func _exit_tree() -> void:
|
|
for sig in [[Net.snapshot, _on_snapshot], [Net.corrected, _on_corrected], [Net.clock, _on_clock],
|
|
[Net.players_changed, _sync_avatars]]:
|
|
if (sig[0] as Signal).is_connected(sig[1]):
|
|
(sig[0] as Signal).disconnect(sig[1])
|
|
|
|
static func _now() -> float:
|
|
return Time.get_ticks_msec() / 1000.0
|
|
|
|
## Server time we draw other players at.
|
|
func render_time() -> float:
|
|
return _now() + _offset - INTERP_DELAY if _offset != INF else -INF
|
|
|
|
## Flags the next update as a legitimate jump (respawning), so the server
|
|
## checks it against the spawn instead of against walking speed.
|
|
func mark_teleport() -> void:
|
|
_teleport_next = true
|
|
|
|
func _physics_process(delta: float) -> void:
|
|
_send_left -= delta
|
|
if _send_left > 0.0:
|
|
return
|
|
_send_left = Net.SEND_INTERVAL
|
|
var cam: Camera3D = player.get_node("Camera3D")
|
|
Net.send_state(player.global_position, player.rotation.y, cam.rotation.x, anim_state(), player.held_item_id(), _teleport_next)
|
|
_teleport_next = false
|
|
|
|
## What the local player is doing, for other people's view of them.
|
|
func anim_state() -> int:
|
|
if player.global_position.y < player.water_level - player.FLOAT_DEPTH + 0.1 and not player.is_on_floor():
|
|
return AvatarScript.Anim.SWIM
|
|
if not player.is_on_floor():
|
|
return AvatarScript.Anim.AIR
|
|
var flat := Vector2(player.velocity.x, player.velocity.z).length()
|
|
if flat > 6.0:
|
|
return AvatarScript.Anim.RUN
|
|
if flat > 0.5:
|
|
return AvatarScript.Anim.WALK
|
|
return AvatarScript.Anim.IDLE
|
|
|
|
func _process(delta: float) -> void:
|
|
var rt := render_time()
|
|
if rt == -INF:
|
|
return
|
|
for a in avatars.values():
|
|
a.update_at(rt, delta)
|
|
|
|
func _on_snapshot(t: float, states: Array) -> void:
|
|
var sample := t - _now()
|
|
# Smooth the clock offset so jitter doesn't make avatars judder. Jump
|
|
# straight to it at first, or if it's way off (a long stall).
|
|
if _offset == INF or absf(sample - _offset) > 1.0:
|
|
_offset = sample
|
|
else:
|
|
_offset = lerpf(_offset, sample, 0.05)
|
|
for s in states:
|
|
var peer := int(s[0])
|
|
if peer == Net.local_peer:
|
|
continue
|
|
var a: Node3D = avatars.get(peer)
|
|
if a == null:
|
|
a = _add_avatar(peer)
|
|
if a == null:
|
|
continue
|
|
a.push(float(s[6]), s[1], s[2], s[3], s[4], s[5]) # stamped with when they moved, not when it was sent
|
|
|
|
func _add_avatar(peer: int) -> Node3D:
|
|
if not Net.players.has(peer):
|
|
return null
|
|
var a: Node3D = AvatarScript.new()
|
|
a.setup(peer, Net.players[peer])
|
|
_root.add_child(a)
|
|
avatars[peer] = a
|
|
return a
|
|
|
|
func _sync_avatars() -> void:
|
|
for peer in avatars.keys():
|
|
if not Net.players.has(peer):
|
|
avatars[peer].queue_free()
|
|
avatars.erase(peer)
|
|
|
|
func _on_corrected(pos: Vector3) -> void:
|
|
corrections += 1
|
|
player.global_position = pos
|
|
player.velocity = Vector3.ZERO
|
|
|
|
func _on_clock(day: int, time: float) -> void:
|
|
world.day_night.day = day
|
|
# Small drift: ease the clock across. A big gap (joining, a stall): jump.
|
|
var diff := wrapf(time - world.day_night.time, -0.5, 0.5)
|
|
if absf(diff) > 0.01:
|
|
world.day_night.time = time
|
|
world.day_night.apply(true)
|