Address review feedback: Implement Godot dedicated server scene + player spawning #51
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
63f7b34db8d8cdea90c76aacccf841ec
|
||||
::res://::1787259973
|
||||
README.md::TextFile::-1::1787146493::0::1::::<><><>0<>0<><>::
|
||||
::res://docs/::1787259973
|
||||
::res://docs/architecture/::1787259973
|
||||
avatar.md::TextFile::-1::1787146493::0::1::::<><><>0<>0<><>::
|
||||
content_moderation.md::TextFile::-1::1787146493::0::1::::<><><>0<>0<><>::
|
||||
networking.md::TextFile::-1::1787146493::0::1::::<><><>0<>0<><>::
|
||||
::res://scenes/::1787259973
|
||||
player.tscn::PackedScene::-1::1787259973::0::1::::<><><>0<>0<><>::res://scripts/player_spawner.gd
|
||||
server.tscn::PackedScene::-1::1787259973::0::1::::<><><>0<>0<><>::
|
||||
::res://scripts/::1787259973
|
||||
player_spawner.gd::GDScript::3637338339553764865::1787259973::0::1::::PlayerNode<>CharacterBody3D<><>0<>0<><>::
|
||||
server.gd::GDScript::6336910781803684784::1787259973::0::1::::<>Node<><>0<>0<><>::
|
||||
@@ -0,0 +1,11 @@
|
||||
[game_view]
|
||||
|
||||
select_mode=0
|
||||
|
||||
[editor_metadata]
|
||||
|
||||
executable_path="/home/daddy/godot/godot"
|
||||
|
||||
[recent_files]
|
||||
|
||||
scenes=["res://scenes/server.tscn"]
|
||||
@@ -0,0 +1,5 @@
|
||||
[folding]
|
||||
|
||||
node_unfolds=[]
|
||||
resource_unfolds=[]
|
||||
nodes_folded=[]
|
||||
@@ -0,0 +1,9 @@
|
||||
list=[{
|
||||
"base": &"CharacterBody3D",
|
||||
"class": &"PlayerNode",
|
||||
"icon": "",
|
||||
"is_abstract": false,
|
||||
"is_tool": false,
|
||||
"language": &"GDScript",
|
||||
"path": "res://scripts/player_spawner.gd"
|
||||
}]
|
||||
Binary file not shown.
@@ -21,6 +21,29 @@ window/size/viewport_width=1920
|
||||
window/size/viewport_height=1080
|
||||
window/stretch/mode="canvas_items"
|
||||
|
||||
[input]
|
||||
|
||||
move_forward={
|
||||
"deadzone": 0.5,
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":87,"key_label":0,"unicode":119,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
move_left={
|
||||
"deadzone": 0.5,
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":65,"key_label":0,"unicode":97,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
move_right={
|
||||
"deadzone": 0.5,
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":68,"key_label":0,"unicode":100,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
move_down={
|
||||
"deadzone": 0.5,
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":83,"key_label":0,"unicode":115,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
|
||||
[rendering]
|
||||
|
||||
renderer/rendering_method="gl_compatibility"
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://scripts/server.gd" id="1_server"]
|
||||
|
||||
[node name="Server" type="Node"]
|
||||
script = ExtResource("1_server")
|
||||
@@ -0,0 +1,6 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://scripts/player_spawner.gd" id="1"]
|
||||
|
||||
[node name="Player" type="CharacterBody3D"]
|
||||
script = ExtResource("1")
|
||||
@@ -0,0 +1,232 @@
|
||||
class_name PlayerNode
|
||||
extends CharacterBody3D
|
||||
## PlayerNode - Server-side player entity.
|
||||
##
|
||||
## Represents a connected player on the server. Handles movement,
|
||||
## input processing, and state synchronization.
|
||||
|
||||
# --- Signals ---
|
||||
signal health_changed(new_health: int)
|
||||
signal player_died(player_node: PlayerNode)
|
||||
|
||||
|
||||
# --- Constants ---
|
||||
const WALK_SPEED := 5.0
|
||||
const SPRINT_MULTIPLIER := 1.6
|
||||
const JUMP_VELOCITY := 4.5
|
||||
const GRAVITY := 9.8
|
||||
const MAX_FALL_SPEED := 20.0
|
||||
const FRICTION := 8.0
|
||||
const AIR_CONTROL := 0.3
|
||||
|
||||
# --- Configurable ---
|
||||
@export var walk_speed: float = WALK_SPEED
|
||||
@export var sprint_multiplier: float = SPRINT_MULTIPLIER
|
||||
@export var jump_velocity: float = JUMP_VELOCITY
|
||||
@export var max_fall_speed: float = MAX_FALL_SPEED
|
||||
|
||||
# --- State ---
|
||||
@export var max_health: int = 100
|
||||
var current_health: int = 0
|
||||
var is_alive: bool = true
|
||||
var is_sprinting: bool = false
|
||||
var peer_id: int = -1
|
||||
var player_name: String = ""
|
||||
var entity_id: int = 0
|
||||
|
||||
# --- Input buffer ---
|
||||
var _input_queue: Array[Dictionary] = []
|
||||
var _last_processed_seq: int = -1
|
||||
|
||||
# --- Movement ---
|
||||
var _velocity: Vector3 = Vector3.ZERO
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
current_health = max_health
|
||||
velocity = _velocity
|
||||
# Start alive
|
||||
is_alive = true
|
||||
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
if not is_alive:
|
||||
return
|
||||
|
||||
# Process input from queue
|
||||
_process_input(delta)
|
||||
|
||||
# Apply gravity
|
||||
if not is_on_floor():
|
||||
velocity.y -= GRAVITY * delta
|
||||
velocity.y = clampf(velocity.y, -max_fall_speed, max_fall_speed)
|
||||
|
||||
# Apply friction on ground
|
||||
if is_on_floor():
|
||||
var move_dir := Vector2(velocity.x, velocity.z)
|
||||
if move_dir.length() > 0.01:
|
||||
var friction_factor := clampf(FRICTION * delta, 0.0, 1.0)
|
||||
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:
|
||||
velocity.x = 0.0
|
||||
velocity.z = 0.0
|
||||
|
||||
# Move and slide
|
||||
velocity.y = clampf(velocity.y, -max_fall_speed, max_fall_speed)
|
||||
move_and_slide()
|
||||
|
||||
# Update visual position (for remote clients via RPC)
|
||||
_update_visual_state()
|
||||
|
||||
|
||||
func _process_input(delta: float) -> void:
|
||||
"""Process buffered input from the server's input queue."""
|
||||
if _input_queue.is_empty():
|
||||
return
|
||||
|
||||
var input_data: Dictionary = _input_queue.pop_front()
|
||||
var input_state: Dictionary = input_data.get("input_state", {})
|
||||
|
||||
# Calculate movement direction
|
||||
var direction := Vector3.ZERO
|
||||
direction.x = input_state.get("move_x", 0.0)
|
||||
direction.z = input_state.get("move_z", 0.0)
|
||||
|
||||
# Sprint check
|
||||
is_sprinting = input_state.get("sprint", false)
|
||||
|
||||
if direction.length() > 0.01:
|
||||
direction = direction.normalized()
|
||||
var speed = walk_speed * sprint_multiplier if is_sprinting else walk_speed
|
||||
velocity.x = direction.x * speed
|
||||
velocity.z = direction.z * speed
|
||||
else:
|
||||
# On ground, apply friction (handled in physics_process)
|
||||
if not is_on_floor():
|
||||
velocity.x *= (1.0 - AIR_CONTROL * delta * 5.0)
|
||||
velocity.z *= (1.0 - AIR_CONTROL * delta * 5.0)
|
||||
|
||||
# Jump
|
||||
if input_state.get("jump", false) and is_on_floor():
|
||||
velocity.y = jump_velocity
|
||||
|
||||
|
||||
func take_damage(amount: int, source_id: int) -> Dictionary:
|
||||
"""Take damage and return result dict."""
|
||||
if not is_alive:
|
||||
return {"success": false, "reason": "already_dead"}
|
||||
|
||||
current_health = max(0, current_health - amount)
|
||||
health_changed.emit(current_health)
|
||||
|
||||
if current_health <= 0:
|
||||
die()
|
||||
return {"success": true, "health": 0, "dead": true}
|
||||
|
||||
return {"success": true, "health": current_health}
|
||||
|
||||
|
||||
func die() -> void:
|
||||
"""Handle player death."""
|
||||
is_alive = false
|
||||
velocity = Vector3.ZERO
|
||||
player_died.emit(self)
|
||||
|
||||
# Respawn after delay
|
||||
await get_tree().create_timer(3.0).timeout
|
||||
respawn()
|
||||
|
||||
|
||||
func respawn() -> void:
|
||||
"""Respawn the player at spawn point."""
|
||||
current_health = max_health
|
||||
is_alive = true
|
||||
velocity = Vector3.ZERO
|
||||
health_changed.emit(current_health)
|
||||
|
||||
# Move to spawn point (default: origin)
|
||||
position = Vector3.ZERO
|
||||
|
||||
|
||||
func set_input_queue(inputs: Array[Dictionary]) -> void:
|
||||
"""Set the input queue from the server."""
|
||||
_input_queue = inputs
|
||||
|
||||
|
||||
func get_state() -> Dictionary:
|
||||
"""Get current player state for replication."""
|
||||
return {
|
||||
"entity_id": entity_id,
|
||||
"peer_id": peer_id,
|
||||
"position": position,
|
||||
"rotation": rotation,
|
||||
"health": current_health,
|
||||
"max_health": max_health,
|
||||
"is_alive": is_alive,
|
||||
"is_sprinting": is_sprinting,
|
||||
}
|
||||
|
||||
|
||||
func get_server_state() -> Dictionary:
|
||||
"""Get current player state for server snapshot broadcast."""
|
||||
return get_state()
|
||||
|
||||
|
||||
func process_server_input(tick: int) -> void:
|
||||
"""Called by server tick to process buffered input."""
|
||||
# This is called from the server's _tick loop.
|
||||
# The input queue is populated by RPCs from clients.
|
||||
pass
|
||||
|
||||
|
||||
func _update_visual_state() -> void:
|
||||
"""Update visual state (for remote interpolation)."""
|
||||
# In a full implementation, this would update a remote player's
|
||||
# visual representation. For now, the node itself is the authority.
|
||||
pass
|
||||
|
||||
|
||||
# --- RPCs ---
|
||||
|
||||
@rpc("any_peer", "call_remote", "reliable")
|
||||
func rpc_request_input(input_state: Dictionary, seq: int) -> void:
|
||||
"""Receive input from client."""
|
||||
if not is_alive:
|
||||
return
|
||||
# Store for processing in _physics_process
|
||||
_input_queue.append({
|
||||
"input_state": input_state,
|
||||
"seq": seq,
|
||||
"timestamp": Time.get_ticks_msec(),
|
||||
})
|
||||
|
||||
|
||||
@rpc("any_peer", "call_remote", "reliable")
|
||||
func rpc_request_interact(target_id: int) -> void:
|
||||
"""Handle interaction request from client."""
|
||||
if not is_alive:
|
||||
return
|
||||
# TODO: Implement interaction logic
|
||||
print("Player %d interacted with entity %d" % [peer_id, target_id])
|
||||
|
||||
|
||||
@rpc("any_peer", "call_remote", "reliable")
|
||||
func rpc_request_attack(target_id: int, aim_dir: Vector3) -> void:
|
||||
"""Handle attack request from client."""
|
||||
if not is_alive:
|
||||
return
|
||||
# TODO: Implement attack logic
|
||||
print("Player %d attacked entity %d with aim %s" % [peer_id, target_id, aim_dir])
|
||||
|
||||
|
||||
@rpc("authority", "call_remote", "reliable")
|
||||
func rpc_set_position(pos: Vector3) -> void:
|
||||
"""Server authoritative position update."""
|
||||
position = pos
|
||||
|
||||
|
||||
@rpc("authority", "call_remote", "reliable")
|
||||
func rpc_set_rotation(rot: Vector3) -> void:
|
||||
"""Server authoritative rotation update."""
|
||||
rotation = rot
|
||||
@@ -0,0 +1 @@
|
||||
uid://br27owjh2f5xd
|
||||
@@ -0,0 +1,174 @@
|
||||
## 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
|
||||
|
||||
## Player scene resource (loaded once).
|
||||
var _player_scene: PackedScene
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
# Preload the player scene once.
|
||||
_player_scene = preload("res://scenes/server_player.tscn")
|
||||
|
||||
# Create and assign the ENet server peer.
|
||||
network_peer = _create_server_peer()
|
||||
if network_peer:
|
||||
multiplayer.multiplayer_peer = 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)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Server creation
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
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 null
|
||||
return peer
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fixed-timestep simulation loop
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if not is_running:
|
||||
return
|
||||
|
||||
# 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: Node3D = 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: Node3D = 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 (peer_id == 1).
|
||||
var peer_ids: Array = multiplayer.get_peers()
|
||||
for pid in peer_ids:
|
||||
if pid == 1: # 1 is the server itself
|
||||
continue
|
||||
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: Node3D = _player_scene.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: Node3D = players[peer_id]
|
||||
if player.is_inside_tree():
|
||||
player.queue_free()
|
||||
players.erase(peer_id)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# RPCs
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@rpc("any_peer", "call_remote", "reliable")
|
||||
func _kick(reason: String) -> void:
|
||||
print("[Server] Kicking caller: %s" % reason)
|
||||
rpc_id(multiplayer.get_remote_sender_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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://cwjv826lva2gi
|
||||
Reference in New Issue
Block a user