Fix: players fall through the floor in multiplayer
checkpoint / checkpoint (push) Failing after 1m34s

scenes/server_player.tscn was a bare CharacterBody3D with NO CollisionShape3D,
so on the server move_and_slide() never hit the ground and is_on_floor() was
always false -> the server player fell forever, and the client (which snaps to
the server's authoritative position past a drift threshold) fell with it.

Give the server player the same CapsuleShape3D (r=0.35, h=1.6) as the client.
The 2-player test now also asserts the server keeps the player on the ground
(on_floor / min_y), which is how this slipped through -- the tests only
checked "position changes" and "sees the other player".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015jtxAiu5dAW4WxCNdGkrkJ
This commit is contained in:
2026-09-05 22:58:55 +00:00
parent 67473dddcd
commit 7ccdd3063a
3 changed files with 20 additions and 5 deletions
+8 -1
View File
@@ -1,6 +1,13 @@
[gd_scene load_steps=2 format=3] [gd_scene load_steps=3 format=3]
[ext_resource type="Script" path="res://scripts/player_spawner.gd" id="1"] [ext_resource type="Script" path="res://scripts/player_spawner.gd" id="1"]
[sub_resource type="CapsuleShape3D" id="CapsuleShape_server_player"]
radius = 0.35
height = 1.6
[node name="Player" type="CharacterBody3D"] [node name="Player" type="CharacterBody3D"]
script = ExtResource("1") script = ExtResource("1")
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
shape = SubResource("CapsuleShape_server_player")
+9 -3
View File
@@ -45,14 +45,20 @@ func _run() -> void:
quit(0) quit(0)
return return
# role a: move forward, watch for a remote and our own displacement. # role a: move forward, watch for a remote and our own displacement, and
# confirm the server keeps us on the ground (not falling through it).
var start: Vector3 = net.position var start: Vector3 = net.position
var max_remotes := 0 var max_remotes := 0
var min_server_y := 1000.0
for i in 90: for i in 90:
net.predict_and_send(1.0 / 30.0, 0.0, -1.0, false, 0.0) net.predict_and_send(1.0 / 30.0, 0.0, -1.0, false, 0.0)
await process_frame await process_frame
max_remotes = maxi(max_remotes, net.get_remote_states().size()) max_remotes = maxi(max_remotes, net.get_remote_states().size())
var st: Dictionary = net.get_own_server_state()
if st.has("position"):
min_server_y = minf(min_server_y, st["position"].y)
var moved: bool = net.position.distance_to(start) > 1.0 var moved: bool = net.position.distance_to(start) > 1.0
print("RESULT a remotes=%d moved=%s" % [max_remotes, str(moved)]) var on_floor: bool = min_server_y > -2.0
print("RESULT a remotes=%d moved=%s on_floor=%s (min_y=%.2f)" % [max_remotes, str(moved), str(on_floor), min_server_y])
net.disconnect_from_server() net.disconnect_from_server()
quit(0 if (moved and max_remotes >= 1) else 1) quit(0 if (moved and max_remotes >= 1 and on_floor) else 1)
+3 -1
View File
@@ -37,7 +37,9 @@ echo "server: $(grep -c 'joined' /tmp/mp2p_server.log) joins seen"
echo "A: ${A_LINE:-<none>}" echo "A: ${A_LINE:-<none>}"
echo "B: ${B_LINE:-<none>}" echo "B: ${B_LINE:-<none>}"
if echo "$A_LINE" | grep -q 'moved=true' && echo "$A_LINE" | grep -qE 'remotes=[1-9]'; then if echo "$A_LINE" | grep -q 'moved=true' \
&& echo "$A_LINE" | grep -qE 'remotes=[1-9]' \
&& echo "$A_LINE" | grep -q 'on_floor=true'; then
echo "=== SMOKE RESULT: PASS ===" echo "=== SMOKE RESULT: PASS ==="
exit 0 exit 0
fi fi