extends Node3D ## Networked representation of one connected player. ## ## The owning peer's local XR rig drives the three transforms below every frame; ## the generated NetSync replicates them, and every other peer applies them to ## the avatar's head and hands. They are ordinary script variables rather than an ## authored replication config, so the avatar goes through exactly the same ## convention as every other replicated object. @onready var _head: Node3D = $Head @onready var _left_hand: Node3D = $LeftHand @onready var _right_hand: Node3D = $RightHand ## Replicated pose. Underscore-free by design — that is what marks a variable as ## replicated (see NetReplication). Written by the owning peer, applied by ## everyone else through the setters. var head_xform: Transform3D = Transform3D.IDENTITY: set = _set_head_xform var left_xform: Transform3D = Transform3D.IDENTITY: set = _set_left_xform var right_xform: Transform3D = Transform3D.IDENTITY: set = _set_right_xform var _local_camera: Node3D var _local_left_hand: Node3D var _local_right_hand: Node3D func _enter_tree() -> void: # The avatar's name is the peer id it belongs to. Authority has to be claimed # here rather than in _ready(): NetReplication reads it when it attaches the # synchronizers, which happens as this node enters the tree. set_multiplayer_authority(str(name).to_int()) func _ready() -> void: # LeftHand/RightHand are the full godot-xr-tools glove scenes, whose root # carries hand.gd. That script sets top_level = true and every physics frame # repositions itself to track a live XRController3D parent. Here the parent is # just this avatar, not a controller, so leaving it running fights both the # local copy below and the replicated values on other peers — and mostly wins, # since it runs every physics tick regardless of our _process. That was the # "hands stuck near the origin, only occasionally correct" symptom. Nothing # else in that script matters here; _controller is always null without a real # controller ancestor, so the grip animation already no-ops. _left_hand.set_physics_process(false) _right_hand.set_physics_process(false) if not is_multiplayer_authority(): set_process(false) # State that arrived in the spawn packet was applied before these @onready # references existed, so render it now. _apply_pose() return var origin := get_tree().get_first_node_in_group("local_xr_origin") if origin: _local_camera = origin.get_node_or_null("XRCamera3D") _local_left_hand = origin.get_node_or_null("XRControllerLeftHand") _local_right_hand = origin.get_node_or_null("XRControllerRightHand") # Every peer's rig is baked at the same spot, so spread joiners along X # rather than starting them stacked on top of each other. ENet peer ids # are large effectively-random 32-bit numbers, so this has to be bounded # AND kept well inside the floor's footprint — an earlier version used up # to 12 units and could drop a joining player off the edge. var peer_id := str(name).to_int() if peer_id != 1: var slot := absi(peer_id) % 4 origin.position += Vector3((slot - 1.5) * 1.2, 0, 0) # Don't render your own floating head and hands from the inside. _head.visible = false _left_hand.visible = false _right_hand.visible = false func _process(_delta: float) -> void: if _local_camera: head_xform = _local_camera.global_transform if _local_left_hand: left_xform = _local_left_hand.global_transform if _local_right_hand: right_xform = _local_right_hand.global_transform # The setters are null-guarded because a spawn-replicated property fires its # setter BEFORE the node is in the tree, when @onready references are still null. func _set_head_xform(value: Transform3D) -> void: head_xform = value if _head: _head.global_transform = value func _set_left_xform(value: Transform3D) -> void: left_xform = value if _left_hand: _left_hand.global_transform = value func _set_right_xform(value: Transform3D) -> void: right_xform = value if _right_hand: _right_hand.global_transform = value func _apply_pose() -> void: _head.global_transform = head_xform _left_hand.global_transform = left_xform _right_hand.global_transform = right_xform