From da4e1420eeb2c5db85de0694421db3c5602536af Mon Sep 17 00:00:00 2001 From: daddy Date: Sun, 6 Sep 2026 00:00:27 +0000 Subject: [PATCH] Fix: rejoining stacked players on top of each other (one fell through) All players spawned at the exact same PlayerSpawn point and were on the same collision layer, so two overlapping CharacterBody3Ds triggered a physics depenetration shove that could launch one through the 1-unit-thick floor. - server.gd: golden-angle spawn spread -- each join lands ~1m+ from the others, growing outward, never exactly repeating. - server_player.tscn + player.tscn: players on collision layer 2, mask 1 (world only) -- players no longer collide with each other at all (fine for a social game; also removes the shove). - player_spawner.gd + player.gd: hard safety net -- if y < -10 (any tunnel-through), snap back to the spawn instead of falling forever. Verified: 3 server players spawn 1.15m+ apart, none fall, all rest on the floor. 2p multiplayer + network_client smokes green. Client-side -- rebuild the exe. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_015jtxAiu5dAW4WxCNdGkrkJ --- scenes/player.tscn | 2 ++ scenes/server_player.tscn | 2 ++ scripts/player.gd | 7 +++++++ scripts/player_spawner.gd | 11 +++++++++++ scripts/server.gd | 18 +++++++++++++++++- 5 files changed, 39 insertions(+), 1 deletion(-) diff --git a/scenes/player.tscn b/scenes/player.tscn index 68faebe..af9bfa6 100644 --- a/scenes/player.tscn +++ b/scenes/player.tscn @@ -11,6 +11,8 @@ radius = 0.35 height = 1.6 [node name="Player" type="CharacterBody3D"] +collision_layer = 2 +collision_mask = 1 script = ExtResource("1_player") [node name="CollisionShape3D" type="CollisionShape3D" parent="."] diff --git a/scenes/server_player.tscn b/scenes/server_player.tscn index cbd6858..2136920 100644 --- a/scenes/server_player.tscn +++ b/scenes/server_player.tscn @@ -7,6 +7,8 @@ radius = 0.35 height = 1.6 [node name="Player" type="CharacterBody3D"] +collision_layer = 2 +collision_mask = 1 script = ExtResource("1") [node name="CollisionShape3D" type="CollisionShape3D" parent="."] diff --git a/scripts/player.gd b/scripts/player.gd index cefcb2e..eb78528 100644 --- a/scripts/player.gd +++ b/scripts/player.gd @@ -26,6 +26,7 @@ const MOUSE_SENSITIVITY: float = 0.003 var _input_direction := Vector2.ZERO var _camera_pitch: float = -0.12 +var _spawn_position: Vector3 = Vector3(0, 1.5, 0) ## Vertical offset applied to the assembled avatar so its feet (origin y=0) ## line up with the bottom of the player's collision capsule. @@ -69,6 +70,7 @@ func _ready() -> void: else: push_warning("player.gd: no PlayerSpawn marker found; spawning at origin") global_position = Vector3(0, 1.0, 0) + _spawn_position = global_position # The spring arm must not collide with the player's own capsule (they're # on the same layer), or the camera gets yanked right up against the @@ -192,6 +194,11 @@ func _physics_process(delta: float) -> void: move_and_slide() + # Safety net -- never fall forever if something goes wrong. + if global_position.y < -10.0: + global_position = _spawn_position + velocity = Vector3.ZERO + net_input = { "move_x": direction.x, "move_z": direction.z, "jump": jumped, "look_yaw": rotation.y, diff --git a/scripts/player_spawner.gd b/scripts/player_spawner.gd index 104b6ff..f574cd9 100644 --- a/scripts/player_spawner.gd +++ b/scripts/player_spawner.gd @@ -14,6 +14,10 @@ const GRAVITY := 9.8 var peer_id: int = -1 +## Where the server spawned this player (set by server.gd). Used as the +## respawn point if the player somehow ends up below the world. +var spawn_position: Vector3 = Vector3(0, 1.5, 0) + ## Highest input seq applied authoritatively so far (echoed in snapshots so ## the client can drop acked inputs and replay the rest). var last_applied_seq: int = 0 @@ -38,6 +42,13 @@ func _physics_process(delta: float) -> void: rotation.y = _look_yaw + # Safety net: if a player ends up below the world (physics shove, tunnel + # through the floor), put them back on their spawn instead of falling + # forever. + if global_position.y < -10.0: + global_position = spawn_position + velocity = Vector3.ZERO + func _integrate(move_x: float, move_z: float, jump: bool, delta: float) -> void: if not is_on_floor(): diff --git a/scripts/server.gd b/scripts/server.gd index 5850b91..2061ce1 100644 --- a/scripts/server.gd +++ b/scripts/server.gd @@ -30,6 +30,9 @@ var status_file_path: String = "" var players: Dictionary = {} var tick: int = 0 var is_running: bool = false +## Monotonic count of joins ever -- drives spawn spread so two players never +## land on the same spot (which used to shove one through the floor). +var _join_count: int = 0 var _player_scene: PackedScene = preload("res://scenes/server_player.tscn") var _level_scene: PackedScene = preload("res://scenes/worlds/test_level.tscn") @@ -65,6 +68,16 @@ func _ready() -> void: print("[Server] world=%s listening on ENet %d (%d Hz)" % [world_id, port, PHYSICS_HZ]) +## A spawn point offset from the level's marker so joins don't stack. Golden- +## angle spiral: evenly spread, never exactly repeats, grows slowly outward. +func _spawn_for(index: int) -> Vector3: + if index == 0: + return _spawn_point + var angle := index * 2.399963 # golden angle (radians) + var radius := 0.9 + 0.25 * float(index) + return _spawn_point + Vector3(cos(angle) * radius, 0.0, sin(angle) * radius) + + func _parse_args() -> void: var args := OS.get_cmdline_user_args() var i := 0 @@ -127,7 +140,10 @@ func _on_peer_connected(peer_id: int) -> void: player.name = "Player_%d" % peer_id player.peer_id = peer_id add_child(player) - player.global_position = _spawn_point + var spawn := _spawn_for(_join_count) + _join_count += 1 + player.global_position = spawn + player.spawn_position = spawn players[peer_id] = player print("[Server] peer %d joined (%d players)" % [peer_id, players.size()]) _write_status()