f6ac103233
Four bugs from real cross-network play: - Player/net_player.gd: the per-peer spawn-spread offset (added to avoid stacking joiners on top of each other) could push a joining client up to 12 units from center, but the floor is only 15x15 (~7.5 unit half-extent) — landing a client off the edge. Bounded to ~2.7 units. - Player/net_player.gd: the glove scenes used for remote-hand visuals carry hand.gd (XRToolsHand), whose root node sets top_level = true and repositions itself to its parent's transform every physics frame, expecting to be parented under a live XRController3D. Parented under the plain avatar node instead, it fought both the local transform copy and the replicated sync every tick — whichever wrote last that frame won, which read as hands stuck near origin except momentarily. Disabled physics processing on those nodes; nothing else in the script matters without a real controller ancestor. - Net/net_pickable.gd: grabbing an item sends an RPC to the server to confirm authority, but the item's regular state sync travels on a different channel with no ordering guarantee against that RPC. A stale "still loose" sync packet could arrive after an optimistic local grab but before the confirmation, and was being treated as a real authority loss, force-dropping the item — explaining "first attempt fails, second succeeds". Now a sync update can't yank an item out of our own hand mid-grab; only an explicit rejection or an actual loss of authority can. - Net/net_pickable.gd: the non-authority path zeroes collision_mask and forces freeze_mode to KINEMATIC, but nothing ever restored either when a peer regained authority (e.g. the server after a client's release) — so a released item stayed collision-less forever, looking physics-less. Now both are restored from the item's own baked values whenever a peer owns a loose (not actively held) item again. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
72 lines
3.0 KiB
GDScript
72 lines
3.0 KiB
GDScript
extends Node3D
|
|
|
|
## Networked representation of one connected player. The owning peer's local
|
|
## XR rig (found via the "local_xr_origin" group) drives Head/LeftHand/
|
|
## RightHand each frame; the MultiplayerSynchronizer replicates those
|
|
## transforms so every other peer sees a matching avatar.
|
|
|
|
@onready var _head: Node3D = $Head
|
|
@onready var _left_hand: Node3D = $LeftHand
|
|
@onready var _right_hand: Node3D = $RightHand
|
|
|
|
var _local_camera: Node3D
|
|
var _local_left_hand: Node3D
|
|
var _local_right_hand: Node3D
|
|
|
|
|
|
func _enter_tree() -> void:
|
|
# MultiplayerSynchronizer resolves authority when this node enters the
|
|
# tree, so authority must be set here rather than in _ready(), or the
|
|
# first synced frames go the wrong direction.
|
|
set_multiplayer_authority(str(name).to_int())
|
|
|
|
|
|
func _ready() -> void:
|
|
# LeftHand/RightHand are the full godot-xr-tools glove scenes, whose root
|
|
# node carries hand.gd (XRToolsHand). That script sets top_level = true
|
|
# and every physics frame does global_transform = get_parent().
|
|
# global_transform * offset — i.e. it actively repositions itself to
|
|
# track a live XRController3D parent. Here the parent is just this
|
|
# NetPlayer node, not a controller, so left running it fights (and mostly
|
|
# wins, since it runs every physics tick regardless of our own _process)
|
|
# against both the local authority's transform copy below and the
|
|
# replicated values on other peers — the exact "hands stuck near origin,
|
|
# only occasionally correct" symptom. Disable it everywhere; nothing else
|
|
# in that script matters here since _controller is always null without a
|
|
# real controller ancestor (grip/trigger animation already no-ops).
|
|
_left_hand.set_physics_process(false)
|
|
_right_hand.set_physics_process(false)
|
|
|
|
if is_multiplayer_authority():
|
|
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 in the scene; spread
|
|
# joiners out along X so they don't start stacked on top of each
|
|
# other. Peer ids from ENet are large effectively-random 32-bit
|
|
# numbers, so this must be bounded, AND kept well within the
|
|
# floor's actual footprint (15x15, so ~7.5 units from center) —
|
|
# a previous version used up to 12 units and could place a
|
|
# joining player off the edge of the floor.
|
|
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/hands from the inside.
|
|
_head.visible = false
|
|
_left_hand.visible = false
|
|
_right_hand.visible = false
|
|
else:
|
|
set_process(false)
|
|
|
|
|
|
func _process(_delta: float) -> void:
|
|
if _local_camera:
|
|
_head.global_transform = _local_camera.global_transform
|
|
if _local_left_hand:
|
|
_left_hand.global_transform = _local_left_hand.global_transform
|
|
if _local_right_hand:
|
|
_right_hand.global_transform = _local_right_hand.global_transform
|