157 lines
4.0 KiB
GDScript
157 lines
4.0 KiB
GDScript
## ServerMain - Dedicated server root node.
|
|
## Accepts ENet connections, spawns Player nodes for each peer,
|
|
## and runs the simulation tick loop at 20 Hz.
|
|
extends Node
|
|
|
|
## Maximum number of concurrent players.
|
|
const MAX_PLAYERS := 16
|
|
|
|
## Server tick rate (Hz).
|
|
const TICK_RATE := 20
|
|
|
|
## Tick duration in seconds.
|
|
const TICK_DURATION := 1.0 / TICK_RATE
|
|
|
|
## ENet server port.
|
|
const SERVER_PORT := 7777
|
|
|
|
## Network peer used for ENet communication.
|
|
var network_peer: ENetMultiplayerPeer
|
|
|
|
## Map of peer_id -> Player node.
|
|
var players: Dictionary = {}
|
|
|
|
## Current tick counter (server-authoritative).
|
|
var tick: int = 0
|
|
|
|
## Whether the server is running.
|
|
var is_running: bool = false
|
|
|
|
## Accumulated real time for fixed timestep simulation.
|
|
var accumulator: float = 0.0
|
|
|
|
|
|
func _ready() -> void:
|
|
# Set this node as the multiplayer authority.
|
|
multiplayer.multiplayer_peer = _create_server_peer()
|
|
multiplayer.server = self
|
|
|
|
if network_peer:
|
|
is_running = true
|
|
print("[Server] Started on port %d" % SERVER_PORT)
|
|
else:
|
|
printerr("[Server] Failed to create ENet peer.")
|
|
get_tree().quit(1)
|
|
|
|
|
|
func _create_server_peer() -> ENetMultiplayerPeer:
|
|
var peer := ENetMultiplayerPeer.new()
|
|
var error := peer.create_server(SERVER_PORT, MAX_PLAYERS)
|
|
if error != OK:
|
|
printerr("[Server] create_server error: %d" % error)
|
|
return ENetMultiplayerPeer()
|
|
return peer
|
|
|
|
|
|
func _process(delta: float) -> void:
|
|
if not is_running:
|
|
return
|
|
|
|
# Handle disconnect events.
|
|
if multiplayer.peer_disconnected.is_connected(_on_peer_disconnected):
|
|
pass # already connected
|
|
|
|
# Fixed timestep accumulator.
|
|
accumulator += delta
|
|
while accumulator >= TICK_DURATION:
|
|
accumulator -= TICK_DURATION
|
|
_tick()
|
|
|
|
|
|
func _tick() -> void:
|
|
"""Main server simulation tick (called at TICK_RATE)."""
|
|
tick += 1
|
|
|
|
# Process player inputs and update state.
|
|
for peer_id in players:
|
|
var player: Node = players[peer_id]
|
|
if player and player.has_method("process_server_input"):
|
|
player.process_server_input(tick)
|
|
|
|
# Broadcast snapshot to all clients.
|
|
_broadcast_snapshot()
|
|
|
|
|
|
func _broadcast_snapshot() -> void:
|
|
"""Broadcast the current authoritative state to all connected clients."""
|
|
var snapshot_data := {
|
|
"tick": tick,
|
|
"players": {}
|
|
}
|
|
|
|
for peer_id in players:
|
|
var player: Node = players[peer_id]
|
|
if player and player.has_method("get_server_state"):
|
|
snapshot_data.players[peer_id] = player.get_server_state()
|
|
|
|
# Send snapshot to all peers except the server itself.
|
|
var peer_ids: Array = multiplayer.get_peers()
|
|
for pid in peer_ids:
|
|
rpc_id(pid, "_receive_snapshot", snapshot_data)
|
|
|
|
|
|
# Remote: clients receive snapshots.
|
|
@rpc("any_peer", "call_remote", "reliable")
|
|
func _receive_snapshot(snapshot_data: Dictionary) -> void:
|
|
pass # Handled by client scene.
|
|
|
|
|
|
# --- Connection lifecycle ---
|
|
|
|
func _on_peer_connected(peer_id: int) -> void:
|
|
print("[Server] Peer %d connected." % peer_id)
|
|
|
|
if players.size() >= MAX_PLAYERS:
|
|
print("[Server] Max players reached. Disconnecting peer %d." % peer_id)
|
|
rpc_id(peer_id, "_kick", "Server is full.")
|
|
return
|
|
|
|
# Spawn a Player node for this peer.
|
|
var player := preload("res://scripts/player.tscn").instantiate()
|
|
player.name = "Player_%d" % peer_id
|
|
player.peer_id = peer_id
|
|
add_child(player)
|
|
players[peer_id] = player
|
|
|
|
print("[Server] Spawned player node for peer %d." % peer_id)
|
|
|
|
|
|
func _on_peer_disconnected(peer_id: int) -> void:
|
|
print("[Server] Peer %d disconnected." % peer_id)
|
|
if players.has(peer_id):
|
|
var player: Node = players[peer_id]
|
|
if player.is_inside_tree():
|
|
player.queue_free()
|
|
players.erase(peer_id)
|
|
|
|
|
|
# --- RPCs ---
|
|
|
|
@rpc("any_peer", "call_remote", "reliable")
|
|
func _kick(peer_id: int, reason: String) -> void:
|
|
print("[Server] Kicking peer %d: %s" % [peer_id, reason])
|
|
rpc_id(peer_id, "_disconnect", reason)
|
|
|
|
|
|
@rpc("call_remote", "reliable")
|
|
func _disconnect(reason: String) -> void:
|
|
print("[Server] Disconnecting: %s" % reason)
|
|
get_tree().quit(0)
|
|
|
|
|
|
# --- Signal connections ---
|
|
|
|
func _enter_tree() -> void:
|
|
multiplayer.peer_connected.connect(_on_peer_connected)
|
|
multiplayer.peer_disconnected.connect(_on_peer_disconnected)
|