Files
modbison/scripts/player_spawner.gd
daddy da4e1420ee
checkpoint / checkpoint (push) Successful in 1m40s
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015jtxAiu5dAW4WxCNdGkrkJ
2026-09-06 00:00:27 +00:00

87 lines
2.6 KiB
GDScript

class_name PlayerNode
extends CharacterBody3D
## Server-authoritative player entity.
##
## One per connected peer, spawned as a child of the Server node. Movement runs
## in _physics_process at the project's fixed physics rate (30 Hz, matching
## scripts/server.gd's snapshot rate and the client's prediction step).
## Movement constants -- mirror scripts/server.gd (the source of truth) and
## scripts/network_client.gd (the client's prediction).
const WALK_SPEED := 5.0
const JUMP_VELOCITY := 4.5
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
## Queue of {seq:int, move_x:float, move_z:float, jump:bool, look_yaw:float}
## appended by rpc_request_input, drained each physics frame.
var _input_queue: Array[Dictionary] = []
var _look_yaw: float = 0.0
func _physics_process(delta: float) -> void:
if _input_queue.is_empty():
_integrate(0.0, 0.0, false, delta)
else:
# Reliable ENet preserves order; sort defensively anyway.
_input_queue.sort_custom(func(a, b): return a["seq"] < b["seq"])
for input in _input_queue:
_look_yaw = input.get("look_yaw", _look_yaw)
_integrate(input.get("move_x", 0.0), input.get("move_z", 0.0), input.get("jump", false), delta)
last_applied_seq = input["seq"]
_input_queue.clear()
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():
velocity.y -= GRAVITY * delta
elif jump:
velocity.y = JUMP_VELOCITY
var dir := Vector3(move_x, 0.0, move_z)
if dir.length() > 0.01:
dir = dir.normalized()
velocity.x = dir.x * WALK_SPEED
velocity.z = dir.z * WALK_SPEED
else:
velocity.x = 0.0
velocity.z = 0.0
move_and_slide()
if is_on_floor() and velocity.y < 0.0:
velocity.y = 0.0
func get_server_state() -> Dictionary:
return {
"position": global_position,
"rotation_y": rotation.y,
"velocity": velocity,
"on_floor": is_on_floor(),
}
@rpc("any_peer", "call_remote", "reliable")
func rpc_request_input(input: Dictionary, seq: int) -> void:
var d := input.duplicate()
d["seq"] = seq
_input_queue.append(d)