fix(multiplayer): replicate per-player avatar loadout and preserve original materials
checkpoint / checkpoint (push) Successful in 5m57s
checkpoint / checkpoint (push) Successful in 5m57s
This commit is contained in:
+79
-40
@@ -26,6 +26,7 @@ var _leaving := false # a scene change is already in flight
|
||||
var _aligned := false # snapped to the server spawn yet?
|
||||
var _remotes: Dictionary = {} # peer_id -> Node3D
|
||||
var _remote_prev: Dictionary = {} # peer_id -> Vector3 (last snapshot position)
|
||||
var _remote_loadouts: Dictionary = {} # peer_id -> Dictionary (last known loadout)
|
||||
var _status_label: Label = null
|
||||
|
||||
## Build-mode UI, gated behind toggle_build_mode (F2) -- only ever
|
||||
@@ -408,10 +409,30 @@ func _setup_networked() -> void:
|
||||
return
|
||||
_networked = true
|
||||
_hide_status()
|
||||
|
||||
# Replicate local avatar to server and remote peers
|
||||
var avatars := get_node_or_null("/root/AvatarSystem")
|
||||
if avatars != null:
|
||||
if avatars.has_method("ensure_catalog"):
|
||||
avatars.ensure_catalog()
|
||||
if avatars.has_method("get_current_loadout_dict"):
|
||||
_net.send_loadout(avatars.get_current_loadout_dict())
|
||||
if avatars.has_signal("loadout_changed") and not avatars.loadout_changed.is_connected(_on_local_loadout_changed):
|
||||
avatars.loadout_changed.connect(_on_local_loadout_changed)
|
||||
|
||||
# Physics-process AFTER the player so net_input reflects this frame.
|
||||
process_physics_priority = 10
|
||||
|
||||
|
||||
func _on_local_loadout_changed(lo: Variant) -> void:
|
||||
if _networked and _net != null and is_instance_valid(_net):
|
||||
if lo != null and lo.has_method("to_dict"):
|
||||
_net.send_loadout(lo.to_dict())
|
||||
elif lo is Dictionary:
|
||||
_net.send_loadout(lo)
|
||||
|
||||
|
||||
|
||||
func _physics_process(_delta: float) -> void:
|
||||
if not _networked or _net == null or not _net.is_connected_to_server():
|
||||
return
|
||||
@@ -441,11 +462,15 @@ func _sync_remotes(states: Dictionary, delta: float) -> void:
|
||||
for pid in states:
|
||||
var st: Dictionary = states[pid]
|
||||
var node: Node3D = _remotes.get(pid)
|
||||
var loadout: Variant = st.get("loadout", null)
|
||||
if node == null:
|
||||
node = _make_remote(pid)
|
||||
node = _make_remote(pid, loadout)
|
||||
_remotes[pid] = node
|
||||
add_child(node)
|
||||
node.global_position = st.get("position", Vector3.ZERO)
|
||||
elif loadout is Dictionary and not (loadout as Dictionary).is_empty() and _remote_loadouts.get(pid) != loadout:
|
||||
_setup_remote_visual(node, pid, loadout)
|
||||
|
||||
var target: Vector3 = st.get("position", node.global_position)
|
||||
node.global_position = node.global_position.lerp(target, clampf(REMOTE_LERP * delta, 0.0, 1.0))
|
||||
node.rotation.y = st.get("rotation_y", node.rotation.y)
|
||||
@@ -463,46 +488,17 @@ func _sync_remotes(states: Dictionary, delta: float) -> void:
|
||||
_remotes[pid].queue_free()
|
||||
_remotes.erase(pid)
|
||||
_remote_prev.erase(pid)
|
||||
_remote_loadouts.erase(pid)
|
||||
|
||||
|
||||
func _make_remote(pid: int) -> Node3D:
|
||||
func _make_remote(pid: int, loadout: Variant = null) -> Node3D:
|
||||
var root := Node3D.new()
|
||||
root.name = "Remote_%d" % pid
|
||||
|
||||
# Other players get the assembled avatar too (everyone shares the default
|
||||
# look for now -- per-player loadout replication is the follow-up), tinted
|
||||
# by peer id so they're still tellable apart, plus a name tag.
|
||||
var tint := Color.from_hsv(fmod(float(pid) * 0.61803, 1.0), 0.55, 0.95)
|
||||
var avatars := get_node_or_null("/root/AvatarSystem")
|
||||
var visual: Node3D = null
|
||||
if avatars and avatars.has_method("build_current_avatar"):
|
||||
visual = avatars.build_current_avatar()
|
||||
if visual != null:
|
||||
visual.position = Vector3(0, -0.85, 0) # feet -> ground (root is at hip height from server)
|
||||
for mi in _all_mesh_instances(visual):
|
||||
var m := StandardMaterial3D.new()
|
||||
m.albedo_color = tint
|
||||
mi.material_override = m
|
||||
root.add_child(visual)
|
||||
var skel := visual.find_child("Skeleton3D", true, false)
|
||||
if skel is Skeleton3D:
|
||||
var anim := preload("res://scripts/avatar/limb_animator.gd").new()
|
||||
anim.name = "LimbAnim"
|
||||
root.add_child(anim)
|
||||
anim.call("setup", skel)
|
||||
else:
|
||||
var body := MeshInstance3D.new()
|
||||
var capsule := CapsuleMesh.new()
|
||||
capsule.radius = 0.35
|
||||
capsule.height = 1.7
|
||||
body.mesh = capsule
|
||||
body.position = Vector3(0, 0.85, 0)
|
||||
var mat := StandardMaterial3D.new()
|
||||
mat.albedo_color = tint
|
||||
body.material_override = mat
|
||||
root.add_child(body)
|
||||
_setup_remote_visual(root, pid, loadout)
|
||||
|
||||
var tag := Label3D.new()
|
||||
tag.name = "NameTag"
|
||||
tag.text = "Player %d" % pid
|
||||
tag.position = Vector3(0, 2.1, 0)
|
||||
tag.billboard = BaseMaterial3D.BILLBOARD_ENABLED
|
||||
@@ -513,12 +509,55 @@ func _make_remote(pid: int) -> Node3D:
|
||||
return root
|
||||
|
||||
|
||||
func _all_mesh_instances(node: Node, out: Array = []) -> Array:
|
||||
if node is MeshInstance3D:
|
||||
out.append(node)
|
||||
for c in node.get_children():
|
||||
_all_mesh_instances(c, out)
|
||||
return out
|
||||
func _setup_remote_visual(root: Node3D, pid: int, loadout: Variant) -> void:
|
||||
var old_vis := root.get_node_or_null("Visual")
|
||||
if old_vis != null and is_instance_valid(old_vis):
|
||||
root.remove_child(old_vis)
|
||||
old_vis.queue_free()
|
||||
var old_anim := root.get_node_or_null("LimbAnim")
|
||||
if old_anim != null and is_instance_valid(old_anim):
|
||||
root.remove_child(old_anim)
|
||||
old_anim.queue_free()
|
||||
|
||||
var avatars := get_node_or_null("/root/AvatarSystem")
|
||||
var visual: Node3D = null
|
||||
if avatars != null:
|
||||
if avatars.has_method("ensure_catalog"):
|
||||
avatars.ensure_catalog()
|
||||
if loadout is Dictionary and not (loadout as Dictionary).is_empty():
|
||||
if avatars.has_method("assemble_avatar"):
|
||||
visual = avatars.assemble_avatar(loadout)
|
||||
if visual == null and avatars.has_method("get_assets_for_slot") and avatars.has_method("assemble_avatar"):
|
||||
var bases: Array = avatars.get_assets_for_slot("base_body")
|
||||
if not bases.is_empty():
|
||||
visual = avatars.assemble_avatar({"base_body": bases[0], "slots": {}})
|
||||
|
||||
if visual != null:
|
||||
visual.name = "Visual"
|
||||
visual.position = Vector3(0, -0.85, 0) # feet -> ground (root is at hip height from server)
|
||||
root.add_child(visual)
|
||||
var skel := visual.find_child("Skeleton3D", true, false)
|
||||
if skel is Skeleton3D:
|
||||
var anim := preload("res://scripts/avatar/limb_animator.gd").new()
|
||||
anim.name = "LimbAnim"
|
||||
root.add_child(anim)
|
||||
anim.call("setup", skel)
|
||||
else:
|
||||
# Fallback capsule if assets/catalog are unavailable (e.g. headless)
|
||||
var body := MeshInstance3D.new()
|
||||
body.name = "Visual"
|
||||
var capsule := CapsuleMesh.new()
|
||||
capsule.radius = 0.35
|
||||
capsule.height = 1.7
|
||||
body.mesh = capsule
|
||||
body.position = Vector3(0, 0.85, 0)
|
||||
var tint := Color.from_hsv(fmod(float(pid) * 0.61803, 1.0), 0.55, 0.95)
|
||||
var mat := StandardMaterial3D.new()
|
||||
mat.albedo_color = tint
|
||||
body.material_override = mat
|
||||
root.add_child(body)
|
||||
|
||||
_remote_loadouts[pid] = (loadout as Dictionary).duplicate(true) if (loadout is Dictionary) else loadout
|
||||
|
||||
|
||||
func _on_server_lost() -> void:
|
||||
|
||||
Reference in New Issue
Block a user