Fix join-floor fall-through, grab race, drop physics, and remote hand sync
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>
This commit is contained in:
+23
-1
@@ -14,12 +14,19 @@ var net_held_by: int = 0: set = _set_net_held_by
|
||||
|
||||
var _pickable: XRToolsPickable
|
||||
|
||||
# This item's own baked freeze_mode (e.g. plate.tscn bakes KINEMATIC, not the
|
||||
# RigidBody3D default of STATIC) — captured once so it can be restored when
|
||||
# this peer regains ownership, instead of getting stuck on whatever
|
||||
# apply_held_state() last forced it to while non-authority.
|
||||
var _original_freeze_mode: int
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_pickable = get_parent() as XRToolsPickable
|
||||
if not _pickable:
|
||||
push_error("NetPickable must be a child of an XRToolsPickable")
|
||||
return
|
||||
_original_freeze_mode = _pickable.freeze_mode
|
||||
_pickable.picked_up.connect(_on_picked_up)
|
||||
_pickable.dropped.connect(_on_dropped)
|
||||
# Deferred: the pickable root captures its own original_collision_mask/
|
||||
@@ -43,7 +50,22 @@ func apply_held_state() -> void:
|
||||
return
|
||||
if not NetworkManager.is_online() or is_multiplayer_authority():
|
||||
# We own this item's simulation (offline, loose+server, or currently
|
||||
# holding it): leave physics alone, XRToolsPickable manages the rest.
|
||||
# holding it). If it's not actively in our own hand right now, make
|
||||
# sure it isn't still left frozen/collision-less from a previous
|
||||
# non-authority period (e.g. right after regaining authority when a
|
||||
# client released it) — while actually held, XRToolsPickable's own
|
||||
# pick_up()/let_go() already manage these fields, so leave those be.
|
||||
if not _pickable.is_picked_up():
|
||||
_pickable.freeze_mode = _original_freeze_mode
|
||||
_pickable.collision_mask = _pickable.original_collision_mask
|
||||
return
|
||||
# A net_held_by/position sync update can race ahead of the
|
||||
# authority-handoff RPC that's about to confirm a grab we just made
|
||||
# optimistically (they travel on different channels with no ordering
|
||||
# guarantee). Don't let a stale sync value yank an item out of our own
|
||||
# hand mid-grab — only an explicit force_release_item rejection, or
|
||||
# actually losing authority for real, should end a grab we initiated.
|
||||
if _pickable.is_picked_up() and _pickable.get_picked_up_by() is XRToolsFunctionPickup:
|
||||
return
|
||||
# Someone else owns it: stop simulating locally, just follow the sync.
|
||||
if _pickable.is_picked_up():
|
||||
|
||||
+21
-8
@@ -22,6 +22,21 @@ func _enter_tree() -> void:
|
||||
|
||||
|
||||
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:
|
||||
@@ -31,16 +46,14 @@ func _ready() -> void:
|
||||
# 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 (not small sequential ones), so the offset must be
|
||||
# bounded — using the raw id directly once shifted a joining
|
||||
# player ~460 million units from the origin, which reads as an
|
||||
# empty world (everything was still there, just unreachably far
|
||||
# away) and wrecks float precision badly enough to jitter the
|
||||
# view and destabilize physics.
|
||||
# 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) % 8 + 1
|
||||
origin.position += Vector3(slot * 1.5, 0, 0)
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user