Files
tactical-shooter/scripts/network/player.gd
T
shawn e9dc05983c Save workspace artifacts: character-controller, gdextension scaffold, network scripts, map-pipeline, server config
Includes code from task workspaces that was never pushed:
- client/characters/ — fps_character_controller.gd (400 lines), fps_camera.gd, input_handler.gd
- gdextension/simulation/ — C++ GDExtension scaffold: entity, movement, hit detection, simulation server, state serializer, bitstream (14 files)
- scripts/network/ — ENet-based network_manager.gd, server/client_main.gd, player.gd
- scripts/map_packaging/ — PCK pipeline: pack_map.gd, map_downloader.gd, map_registry_server.py, README
- scripts/config/ — server_config.gd (480 lines)
- scenes/ — client_main, server_main, player, test_range
- project.godot, export_presets.cfg
2026-07-01 18:30:44 -04:00

101 lines
3.5 KiB
GDScript

## 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