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:
161
scripts/net/avatar.gd
Normal file
161
scripts/net/avatar.gd
Normal file
@@ -0,0 +1,161 @@
|
||||
extends Node3D
|
||||
## Another player, as you see them: their body, the item in their hand and a
|
||||
## name tag, moved smoothly from the server's snapshots.
|
||||
##
|
||||
## Snapshots arrive ~20 times a second, some late and some out of order. The
|
||||
## avatar is drawn INTERP_DELAY behind the newest data and interpolates
|
||||
## between the two snapshots either side of that moment, so it moves smoothly
|
||||
## with jitter. If data stops it carries on briefly along its last velocity
|
||||
## (up to MAX_EXTRAPOLATE), then holds still.
|
||||
|
||||
const CharacterMesh = preload("res://scripts/net/character_mesh.gd")
|
||||
const MAX_EXTRAPOLATE := 0.25
|
||||
const TAG_FADE := Vector2(25.0, 40.0) ## name tags fade out over this distance range
|
||||
|
||||
enum Anim { IDLE, WALK, RUN, SWIM, AIR }
|
||||
|
||||
var peer := 0
|
||||
var info: Dictionary = {}
|
||||
var anim := Anim.IDLE
|
||||
var held := ""
|
||||
var tag: Label3D
|
||||
|
||||
var _buf: Array = [] ## [t, pos, yaw, pitch, anim, held], oldest first
|
||||
var _body: Node3D
|
||||
var _legs: Array[Node3D] = []
|
||||
var _arms: Array[Node3D] = []
|
||||
var _held_mi: MeshInstance3D
|
||||
var _phase := 0.0
|
||||
var _speed := 0.0
|
||||
var _last_pos := Vector3.INF
|
||||
|
||||
func setup(p: int, d: Dictionary) -> void:
|
||||
peer = p
|
||||
info = d
|
||||
name = "Avatar_%d" % p
|
||||
var parts: Dictionary = CharacterMesh.build(d.get("appearance", {}))
|
||||
var mat := ItemDB.item_material()
|
||||
_body = Node3D.new()
|
||||
add_child(_body)
|
||||
var mi := MeshInstance3D.new()
|
||||
mi.mesh = parts.body
|
||||
mi.material_override = mat
|
||||
_body.add_child(mi)
|
||||
for pair in [[parts.hips, parts.leg, _legs], [parts.shoulders, parts.arm, _arms]]:
|
||||
for pos: Vector3 in pair[0]:
|
||||
var pivot := Node3D.new()
|
||||
pivot.position = pos
|
||||
var limb := MeshInstance3D.new()
|
||||
limb.mesh = pair[1]
|
||||
limb.material_override = mat
|
||||
pivot.add_child(limb)
|
||||
_body.add_child(pivot)
|
||||
pair[2].append(pivot)
|
||||
_held_mi = MeshInstance3D.new()
|
||||
_held_mi.material_override = mat
|
||||
_held_mi.position = parts.hand
|
||||
_held_mi.rotation = Vector3(-PI * 0.5, 0, 0) # item handles run along +Y; point them forward
|
||||
_held_mi.scale = Vector3.ONE * 0.85
|
||||
_arms[1].add_child(_held_mi)
|
||||
tag = Label3D.new()
|
||||
tag.text = String(d.get("name", "?"))
|
||||
tag.billboard = BaseMaterial3D.BILLBOARD_ENABLED
|
||||
tag.position = Vector3(0, 2.12, 0)
|
||||
tag.font_size = 44
|
||||
tag.outline_size = 10
|
||||
tag.pixel_size = 0.004
|
||||
tag.modulate = Color(1, 0.97, 0.88)
|
||||
tag.outline_modulate = Color(0, 0, 0, 0.8)
|
||||
tag.no_depth_test = false # walls and hills hide it
|
||||
tag.double_sided = true
|
||||
add_child(tag)
|
||||
|
||||
## A snapshot for this player at server time `t`.
|
||||
func push(t: float, pos: Vector3, yaw: float, pitch: float, a: int, h: String) -> void:
|
||||
if not _buf.is_empty() and t <= _buf[-1][0]:
|
||||
# Late or duplicate: slot it in order (or drop a duplicate).
|
||||
for i in range(_buf.size()):
|
||||
if absf(_buf[i][0] - t) < 0.0001:
|
||||
return
|
||||
if _buf[i][0] > t:
|
||||
_buf.insert(i, [t, pos, yaw, pitch, a, h])
|
||||
return
|
||||
_buf.append([t, pos, yaw, pitch, a, h])
|
||||
if _buf.size() == 1 and is_inside_tree():
|
||||
global_position = pos # first sighting: appear in place, don't slide in
|
||||
rotation.y = yaw
|
||||
|
||||
## Where to draw the avatar at server time `rt` (and animate it).
|
||||
func update_at(rt: float, dt: float) -> void:
|
||||
if _buf.is_empty():
|
||||
return
|
||||
# Forget old data, keeping one sample before `rt`.
|
||||
while _buf.size() > 2 and _buf[1][0] < rt - 1.0:
|
||||
_buf.pop_front()
|
||||
var s: Array = _sample(rt)
|
||||
var p: Vector3 = s[0]
|
||||
global_position = p
|
||||
rotation.y = s[1]
|
||||
anim = s[2]
|
||||
if s[3] != held:
|
||||
held = s[3]
|
||||
_held_mi.mesh = ItemDB.mesh_for(held) if not held.is_empty() and not ItemDB.get_item(held).has("place") else null
|
||||
_animate(p, dt)
|
||||
_fade_tag()
|
||||
|
||||
## [pos, yaw, anim, held] at time `rt`.
|
||||
func _sample(rt: float) -> Array:
|
||||
var first: Array = _buf[0]
|
||||
if rt <= first[0]:
|
||||
return [first[1], first[2], first[4], first[5]]
|
||||
for i in range(_buf.size() - 1):
|
||||
var a: Array = _buf[i]
|
||||
var b: Array = _buf[i + 1]
|
||||
if rt <= b[0]:
|
||||
var k: float = (rt - a[0]) / maxf(b[0] - a[0], 0.0001)
|
||||
return [(a[1] as Vector3).lerp(b[1], k), lerp_angle(a[2], b[2], k), b[4] if k > 0.5 else a[4], b[5]]
|
||||
# Past the newest snapshot: carry on along the last velocity, briefly.
|
||||
var last: Array = _buf[-1]
|
||||
if _buf.size() >= 2:
|
||||
var prev: Array = _buf[-2]
|
||||
var span: float = maxf(last[0] - prev[0], 0.001)
|
||||
var vel: Vector3 = ((last[1] as Vector3) - prev[1]) / span
|
||||
var ahead := minf(rt - last[0], MAX_EXTRAPOLATE)
|
||||
return [(last[1] as Vector3) + vel * ahead, last[2], last[4], last[5]]
|
||||
return [last[1], last[2], last[4], last[5]]
|
||||
|
||||
func _animate(p: Vector3, dt: float) -> void:
|
||||
if _last_pos != Vector3.INF and dt > 0.0:
|
||||
var d := Vector2(p.x - _last_pos.x, p.z - _last_pos.z).length() / dt
|
||||
_speed = lerpf(_speed, d, minf(dt * 10.0, 1.0))
|
||||
_last_pos = p
|
||||
_phase += dt * (2.0 + _speed * 1.6)
|
||||
var swing := clampf(_speed / 5.0, 0.0, 1.0) * (0.75 if anim == Anim.RUN else 0.55)
|
||||
var lean := 0.0
|
||||
match anim:
|
||||
Anim.SWIM:
|
||||
lean = 1.2
|
||||
swing = 0.35
|
||||
Anim.AIR:
|
||||
swing = 0.15
|
||||
Anim.RUN:
|
||||
lean = 0.12
|
||||
_body.rotation.x = lerpf(_body.rotation.x, -lean, minf(dt * 6.0, 1.0))
|
||||
for i in range(2):
|
||||
var side := 1.0 if i == 0 else -1.0
|
||||
_legs[i].rotation.x = sin(_phase) * swing * side
|
||||
_arms[i].rotation.x = -sin(_phase) * swing * side * 0.8
|
||||
if anim == Anim.SWIM:
|
||||
_arms[0].rotation.x = -PI * 0.6 + sin(_phase * 1.5) * 0.6
|
||||
_arms[1].rotation.x = -PI * 0.6 - sin(_phase * 1.5) * 0.6
|
||||
elif not held.is_empty():
|
||||
_arms[1].rotation.x = -0.5 + sin(_phase) * swing * 0.3 # hold the item out a little
|
||||
_body.position.y = absf(sin(_phase)) * 0.03 * clampf(_speed / 5.0, 0.0, 1.0)
|
||||
|
||||
func _fade_tag() -> void:
|
||||
var cam := get_viewport().get_camera_3d() if is_inside_tree() else null
|
||||
if cam == null:
|
||||
return
|
||||
var d := cam.global_position.distance_to(global_position)
|
||||
tag.modulate.a = 1.0 - smoothstep(TAG_FADE.x, TAG_FADE.y, d)
|
||||
tag.visible = tag.modulate.a > 0.01
|
||||
Reference in New Issue
Block a user