WIP checkpoint: Implement Godot dedicated server scene + player spawning

This commit is contained in:
2026-08-17 18:15:38 +00:00
parent 11e812cb6b
commit 37a311b0c5
3 changed files with 45 additions and 28 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
[gd_scene load_steps=2 format=3] [gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://scripts/player_node.gd" id="1"] [ext_resource type="Script" path="res://scripts/player_spawner.gd" id="1"]
[node name="Player" type="Node3D"] [node name="Player" type="Node3D"]
script = ExtResource("1") script = ExtResource("1")
+6 -7
View File
@@ -4,9 +4,6 @@ extends CharacterBody3D
## Represents a connected player on the server. Handles movement, ## Represents a connected player on the server. Handles movement,
## input processing, and state synchronization. ## input processing, and state synchronization.
class_name PlayerNode
# --- Signals --- # --- Signals ---
signal health_changed(new_health: int) signal health_changed(new_health: int)
signal player_died(player_node: PlayerNode) signal player_died(player_node: PlayerNode)
@@ -65,12 +62,14 @@ func _physics_process(delta: float) -> void:
# Apply friction on ground # Apply friction on ground
if is_on_floor(): if is_on_floor():
var move_dir = velocity.xz var move_dir := Vector2(velocity.x, velocity.z)
if move_dir.length() > 0.01: if move_dir.length() > 0.01:
var friction_factor = clampf(FRICTION * delta, 0.0, 1.0) var friction_factor := clampf(FRICTION * delta, 0.0, 1.0)
velocity.xz = move_dir.normalized() * move_dir.length() * (1.0 - friction_factor) velocity.x = move_dir.normalized().x * move_dir.length() * (1.0 - friction_factor)
velocity.z = move_dir.normalized().y * move_dir.length() * (1.0 - friction_factor)
else: else:
velocity.xz = Vector2.ZERO velocity.x = 0.0
velocity.z = 0.0
# Move and slide # Move and slide
velocity.y = clampf(velocity.y, -max_fall_speed, max_fall_speed) velocity.y = clampf(velocity.y, -max_fall_speed, max_fall_speed)
+38 -20
View File
@@ -3,6 +3,7 @@
## and runs the simulation tick loop at 20 Hz. ## and runs the simulation tick loop at 20 Hz.
extends Node extends Node
## Maximum number of concurrent players. ## Maximum number of concurrent players.
const MAX_PLAYERS := 16 const MAX_PLAYERS := 16
@@ -30,13 +31,18 @@ var is_running: bool = false
## Accumulated real time for fixed timestep simulation. ## Accumulated real time for fixed timestep simulation.
var accumulator: float = 0.0 var accumulator: float = 0.0
## Player scene resource (loaded once).
var _player_scene: PackedScene
func _ready() -> void: func _ready() -> void:
# Set this node as the multiplayer authority. # Preload the player scene once.
multiplayer.multiplayer_peer = _create_server_peer() _player_scene = preload("res://scenes/player.tscn")
multiplayer.server = self
# Create and assign the ENet server peer.
network_peer = _create_server_peer()
if network_peer: if network_peer:
multiplayer.multiplayer_peer = network_peer
is_running = true is_running = true
print("[Server] Started on port %d" % SERVER_PORT) print("[Server] Started on port %d" % SERVER_PORT)
else: else:
@@ -44,23 +50,27 @@ func _ready() -> void:
get_tree().quit(1) get_tree().quit(1)
# ---------------------------------------------------------------------------
# Server creation
# ---------------------------------------------------------------------------
func _create_server_peer() -> ENetMultiplayerPeer: func _create_server_peer() -> ENetMultiplayerPeer:
var peer := ENetMultiplayerPeer.new() var peer := ENetMultiplayerPeer.new()
var error := peer.create_server(SERVER_PORT, MAX_PLAYERS) var error := peer.create_server(SERVER_PORT, MAX_PLAYERS)
if error != OK: if error != OK:
printerr("[Server] create_server error: %d" % error) printerr("[Server] create_server error: %d" % error)
return ENetMultiplayerPeer() return null
return peer return peer
# ---------------------------------------------------------------------------
# Fixed-timestep simulation loop
# ---------------------------------------------------------------------------
func _process(delta: float) -> void: func _process(delta: float) -> void:
if not is_running: if not is_running:
return return
# Handle disconnect events.
if multiplayer.peer_disconnected.is_connected(_on_peer_disconnected):
pass # already connected
# Fixed timestep accumulator. # Fixed timestep accumulator.
accumulator += delta accumulator += delta
while accumulator >= TICK_DURATION: while accumulator >= TICK_DURATION:
@@ -74,7 +84,7 @@ func _tick() -> void:
# Process player inputs and update state. # Process player inputs and update state.
for peer_id in players: for peer_id in players:
var player: Node = players[peer_id] var player: Node3D = players[peer_id]
if player and player.has_method("process_server_input"): if player and player.has_method("process_server_input"):
player.process_server_input(tick) player.process_server_input(tick)
@@ -86,17 +96,19 @@ func _broadcast_snapshot() -> void:
"""Broadcast the current authoritative state to all connected clients.""" """Broadcast the current authoritative state to all connected clients."""
var snapshot_data := { var snapshot_data := {
"tick": tick, "tick": tick,
"players": {} "players": {},
} }
for peer_id in players: for peer_id in players:
var player: Node = players[peer_id] var player: Node3D = players[peer_id]
if player and player.has_method("get_server_state"): if player and player.has_method("get_server_state"):
snapshot_data.players[peer_id] = player.get_server_state() snapshot_data.players[peer_id] = player.get_server_state()
# Send snapshot to all peers except the server itself. # Send snapshot to all peers except the server itself (peer_id == 1).
var peer_ids: Array = multiplayer.get_peers() var peer_ids: Array = multiplayer.get_peers()
for pid in peer_ids: for pid in peer_ids:
if pid == 1: # 1 is the server itself
continue
rpc_id(pid, "_receive_snapshot", snapshot_data) rpc_id(pid, "_receive_snapshot", snapshot_data)
@@ -106,7 +118,9 @@ func _receive_snapshot(snapshot_data: Dictionary) -> void:
pass # Handled by client scene. pass # Handled by client scene.
# --- Connection lifecycle --- # ---------------------------------------------------------------------------
# Connection lifecycle
# ---------------------------------------------------------------------------
func _on_peer_connected(peer_id: int) -> void: func _on_peer_connected(peer_id: int) -> void:
print("[Server] Peer %d connected." % peer_id) print("[Server] Peer %d connected." % peer_id)
@@ -117,7 +131,7 @@ func _on_peer_connected(peer_id: int) -> void:
return return
# Spawn a Player node for this peer. # Spawn a Player node for this peer.
var player := preload("res://scripts/player.tscn").instantiate() var player: Node3D = _player_scene.instantiate()
player.name = "Player_%d" % peer_id player.name = "Player_%d" % peer_id
player.peer_id = peer_id player.peer_id = peer_id
add_child(player) add_child(player)
@@ -129,18 +143,20 @@ func _on_peer_connected(peer_id: int) -> void:
func _on_peer_disconnected(peer_id: int) -> void: func _on_peer_disconnected(peer_id: int) -> void:
print("[Server] Peer %d disconnected." % peer_id) print("[Server] Peer %d disconnected." % peer_id)
if players.has(peer_id): if players.has(peer_id):
var player: Node = players[peer_id] var player: Node3D = players[peer_id]
if player.is_inside_tree(): if player.is_inside_tree():
player.queue_free() player.queue_free()
players.erase(peer_id) players.erase(peer_id)
# --- RPCs --- # ---------------------------------------------------------------------------
# RPCs
# ---------------------------------------------------------------------------
@rpc("any_peer", "call_remote", "reliable") @rpc("any_peer", "call_remote", "reliable")
func _kick(peer_id: int, reason: String) -> void: func _kick(reason: String) -> void:
print("[Server] Kicking peer %d: %s" % [peer_id, reason]) print("[Server] Kicking caller: %s" % reason)
rpc_id(peer_id, "_disconnect", reason) rpc_id(get_rpc_sender_id(), "_disconnect", reason)
@rpc("call_remote", "reliable") @rpc("call_remote", "reliable")
@@ -149,7 +165,9 @@ func _disconnect(reason: String) -> void:
get_tree().quit(0) get_tree().quit(0)
# --- Signal connections --- # ---------------------------------------------------------------------------
# Signal connections
# ---------------------------------------------------------------------------
func _enter_tree() -> void: func _enter_tree() -> void:
multiplayer.peer_connected.connect(_on_peer_connected) multiplayer.peer_connected.connect(_on_peer_connected)