## Player — Replicated player state. ## ## Server-authoritative movement model (Phase 0 placeholder). ## The player who owns this node (multiplayer authority) sends input, ## server validates and broadcasts position to all peers. ## ## Phase 0: simple position RPC replication. ## Phase 1+: full input → movement → state broadcast loop. extends Node3D # --------------------------------------------------------------------------- # Exports # --------------------------------------------------------------------------- @export var movement_speed: float = 5.0 @export var sync_rate: float = 10.0 # Hz — how often to broadcast state # --------------------------------------------------------------------------- # State # --------------------------------------------------------------------------- var is_local: bool = false # True for the local player instance var is_client_controlled: bool = false var _sync_timer: float = 0.0 # Interpolation state for remote players var _remote_position: Vector3 = Vector3.ZERO var _remote_velocity: Vector3 = Vector3.ZERO # --------------------------------------------------------------------------- # Lifecycle # --------------------------------------------------------------------------- func _ready() -> void: is_local = multiplayer.get_unique_id() == multiplayer.get_multiplayer_authority() if is_local: # This is our own player — we'll send input to server is_client_controlled = not multiplayer.is_server() if is_client_controlled: print("[Player] Local player (client-controlled): %d" % multiplayer.get_unique_id()) else: # Remote player — disable direct control set_process(false) set_physics_process(false) print("[Player] Remote player: %d (authority: %d)" % [multiplayer.get_unique_id(), multiplayer.get_multiplayer_authority()]) # --------------------------------------------------------------------------- # Client-side: send position to server # --------------------------------------------------------------------------- func _process(delta: float) -> void: if not is_client_controlled: return # Simple movement for Phase 0 testing var input_dir := Vector3.ZERO if Input.is_action_pressed("ui_right"): input_dir.x += 1.0 if Input.is_action_pressed("ui_left"): input_dir.x -= 1.0 if Input.is_action_pressed("ui_up"): input_dir.z -= 1.0 if Input.is_action_pressed("ui_down"): input_dir.z += 1.0 if input_dir != Vector3.ZERO: input_dir = input_dir.normalized() position += input_dir * movement_speed * delta # Rate-limited position update to server _sync_timer += delta if _sync_timer >= (1.0 / sync_rate): _sync_timer = 0.0 _send_position.rpc_id(1, position) @rpc("unreliable", "any_peer") func _send_position(pos: Vector3) -> void: # Server receives position from client if not multiplayer.is_server(): return var peer_id: int = multiplayer.get_remote_sender_id() if peer_id != multiplayer.get_multiplayer_authority(): # Peer is not authorized for this player — reject push_warning("[Player] Unauthorized position update from peer %d" % peer_id) return # Server validates and broadcasts to all other peers position = pos _broadcast_position.rpc(pos) @rpc("unreliable", "authority", "call_local") func _broadcast_position(pos: Vector3) -> void: # All peers (including server) update this player's position if not is_local: # Remote player — store for interpolation _remote_position = pos position = pos else: # Local player — authoritative position already set via input pass