179 lines
6.1 KiB
GDScript
179 lines
6.1 KiB
GDScript
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 hitbox: AnimatableBody3D
|
|
|
|
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)
|
|
# A hitbox for swinging at them when the server has PvP on (hands.gd
|
|
# looks for it; the server decides what the hit does). Layer 5 (value
|
|
# 16): nothing walks into it.
|
|
hitbox = AnimatableBody3D.new()
|
|
hitbox.sync_to_physics = false
|
|
hitbox.collision_layer = 16
|
|
hitbox.collision_mask = 0
|
|
hitbox.set_meta("avatar_peer", p)
|
|
var cs := CollisionShape3D.new()
|
|
var cap := CapsuleShape3D.new()
|
|
cap.radius = 0.4
|
|
cap.height = 1.8
|
|
cs.shape = cap
|
|
cs.position = Vector3(0, 0.9, 0)
|
|
hitbox.add_child(cs)
|
|
add_child(hitbox)
|
|
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
|