e9dc05983c
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
101 lines
3.5 KiB
GDScript
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
|