141 lines
4.8 KiB
GDScript
141 lines
4.8 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))
|
|
# An admin's /weather (otherwise the weather follows from seed and clock).
|
|
Net.weather_changed.connect(_on_weather)
|
|
if not String(Net.world_info.get("weather", "")).is_empty():
|
|
_on_weather(String(Net.world_info.weather))
|
|
|
|
func _on_weather(state: String) -> void:
|
|
world.weather.force(state)
|
|
|
|
func _exit_tree() -> void:
|
|
for sig in [[Net.snapshot, _on_snapshot], [Net.corrected, _on_corrected], [Net.clock, _on_clock],
|
|
[Net.players_changed, _sync_avatars], [Net.weather_changed, _on_weather]]:
|
|
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)
|
|
world.weather.snap() # (a night slept through: the weather jumps too)
|