Files
tactical-shooter/scripts/network/network_manager.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

169 lines
5.9 KiB
GDScript

## NetworkManager — ENet Transport + Player Replication Singleton
##
## Autoload that wraps Godot 4's ENetMultiplayerPeer and provides
## player spawn/despawn broadcasting to all connected clients.
## Server creates RPC broadcasts; clients receive and emit signals.
##
## Architecture:
## server mode → start_server(port) → ENetMultiplayerPeer server
## client mode → join_server(host,port)→ ENetMultiplayerPeer client
## replication → _broadcast_spawn_player / _broadcast_despawn_player
## (server→all clients RPC)
##
## Channels (3-lane layout per Phase 0 research):
## 0 unreliable-ordered → 128Hz input / transform deltas
## 1 reliable-ordered → game events, spawn, damage, chat
## 2 unreliable → telemetry / VOIP metadata
extends Node
# ---------------------------------------------------------------------------
# Signals
# ---------------------------------------------------------------------------
signal server_started(port: int)
signal server_stopped()
signal player_connected(peer_id: int)
signal player_disconnected(peer_id: int)
signal connection_succeeded()
signal connection_failed(error_message: String)
# --- Player replication signals (emitted on all peers after RPC broadcast) ---
signal remote_player_spawned(peer_id: int, pos: Vector3)
signal remote_player_despawned(peer_id: int)
# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------
const DEFAULT_PORT: int = 34197
const CHANNELS: int = 3 # 0=input, 1=events, 2=telemetry
# Max clients is read from ServerConfig at start_server() time.
# This default is used before ServerConfig is available.
var max_clients: int = 16
# Channel indices
enum Chan {
INPUT = 0,
EVENTS = 1,
TELEMETRY = 2,
}
# ---------------------------------------------------------------------------
# State
# ---------------------------------------------------------------------------
var is_server: bool = false : get = _is_server
var is_client: bool = false : get = _is_client
var peer: ENetMultiplayerPeer = null
func _is_server() -> bool:
return is_server
func _is_client() -> bool:
return is_client
# ---------------------------------------------------------------------------
# Server API
# ---------------------------------------------------------------------------
## Start a dedicated server on [port].
## Uses max_clients (which should be set from ServerConfig before calling).
## Returns OK or ERR_* on failure.
func start_server(port: int = DEFAULT_PORT) -> Error:
if peer and peer.get_connection_status() != MultiplayerPeer.CONNECTION_DISCONNECTED:
stop()
# If ServerConfig is available, use it for max_clients
if ServerConfig and ServerConfig.has_method(&"get_config_path"):
max_clients = ServerConfig.max_players
peer = ENetMultiplayerPeer.new()
peer.set_bind_ip("*")
var err: Error = peer.create_server(port, max_clients, CHANNELS, 0, 0)
if err != OK:
peer = null
return err
multiplayer.multiplayer_peer = peer
multiplayer.multiplayer_peer.peer_connected.connect(_on_peer_connected)
multiplayer.multiplayer_peer.peer_disconnected.connect(_on_peer_disconnected)
is_server = true
server_started.emit(port)
print("[NetworkManager] Server started on port %d" % port)
return OK
## Stop the server / disconnect.
func stop() -> void:
if not peer:
return
if is_server:
multiplayer.multiplayer_peer.peer_connected.disconnect(_on_peer_connected)
multiplayer.multiplayer_peer.peer_disconnected.disconnect(_on_peer_disconnected)
peer.close()
multiplayer.multiplayer_peer = null
peer = null
is_server = false
is_client = false
server_stopped.emit()
print("[NetworkManager] Stopped")
# ---------------------------------------------------------------------------
# Client API
# ---------------------------------------------------------------------------
## Connect to a remote server.
func join_server(host: String, port: int = DEFAULT_PORT) -> Error:
if peer and peer.get_connection_status() != MultiplayerPeer.CONNECTION_DISCONNECTED:
stop()
peer = ENetMultiplayerPeer.new()
var err: Error = peer.create_client(host, port, CHANNELS, 0, 0)
if err != OK:
peer = null
return err
multiplayer.multiplayer_peer = peer
connection_succeeded.emit()
print("[NetworkManager] Connecting to %s:%d ..." % [host, port])
is_client = true
return OK
# ---------------------------------------------------------------------------
# Player Replication RPCs (broadcast server → all clients)
# ---------------------------------------------------------------------------
## Server calls this when a new player joins.
## Broadcasts to all clients so they can create a visual player node.
@rpc("authority", "call_local", "reliable")
func broadcast_spawn_player(peer_id: int, pos: Vector3, is_team_a: bool) -> void:
if not multiplayer.is_server():
print("[NetworkManager] Client received spawn: peer=%d at (%.1f, %.1f)" % [peer_id, pos.x, pos.z])
remote_player_spawned.emit(peer_id, pos)
## Server calls this when a player leaves.
@rpc("authority", "call_local", "reliable")
func broadcast_despawn_player(peer_id: int) -> void:
if not multiplayer.is_server():
print("[NetworkManager] Client received despawn: peer=%d" % peer_id)
remote_player_despawned.emit(peer_id)
# ---------------------------------------------------------------------------
# Event handlers
# ---------------------------------------------------------------------------
func _on_peer_connected(id: int) -> void:
print("[NetworkManager] Peer connected: %d" % id)
player_connected.emit(id)
func _on_peer_disconnected(id: int) -> void:
print("[NetworkManager] Peer disconnected: %d" % id)
player_disconnected.emit(id)
func _process(_delta: float) -> void:
# Godot does internal ENet polling via MultiplayerAPI;
# explicit polling is reserved for future custom packet handling.
pass
func _exit_tree() -> void:
stop()