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
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
## Client Main — Client entry point for testing.
|
||||
##
|
||||
## Connects to the server. Receives broadcast_spawn_player /
|
||||
## broadcast_despawn_player RPCs from the server and creates
|
||||
## visual player nodes locally so each client sees every other player.
|
||||
##
|
||||
## Phase 0: simple position replication via RPC + box mesh player nodes.
|
||||
|
||||
extends Node3D
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Exports
|
||||
# ---------------------------------------------------------------------------
|
||||
@export var server_host: String = "127.0.0.1"
|
||||
@export var server_port: int = 34197
|
||||
@export var player_scene: PackedScene = preload("res://scenes/player.tscn")
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# State
|
||||
# ---------------------------------------------------------------------------
|
||||
# remote_players[peer_id] = Node3D — visual representation of other players
|
||||
var remote_players: Dictionary = {}
|
||||
var connected: bool = false
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Lifecycle
|
||||
# ---------------------------------------------------------------------------
|
||||
func _ready() -> void:
|
||||
# Connect to server after a short delay
|
||||
await get_tree().create_timer(0.5).timeout
|
||||
_connect_to_server()
|
||||
|
||||
func _connect_to_server() -> void:
|
||||
if OS.has_environment("SERVER_HOST"):
|
||||
server_host = OS.get_environment("SERVER_HOST")
|
||||
if OS.has_environment("SERVER_PORT"):
|
||||
server_port = int(OS.get_environment("SERVER_PORT"))
|
||||
|
||||
print("[ClientMain] Connecting to %s:%d ..." % [server_host, server_port])
|
||||
|
||||
var err: Error = NetworkManager.join_server(server_host, server_port)
|
||||
if err != OK:
|
||||
push_error("[ClientMain] Connection failed: %s" % error_string(err))
|
||||
await get_tree().create_timer(2.0).timeout
|
||||
_connect_to_server()
|
||||
return
|
||||
|
||||
# Setup camera and lights
|
||||
_setup_scene()
|
||||
|
||||
connected = true
|
||||
|
||||
# Connect replication signals from NetworkManager
|
||||
# These fire when the server broadcasts spawn_player / despawn_player
|
||||
NetworkManager.remote_player_spawned.connect(_on_remote_player_spawned)
|
||||
NetworkManager.remote_player_despawned.connect(_on_remote_player_despawned)
|
||||
|
||||
print("[ClientMain] Connected to server. Peer ID: %d" % multiplayer.get_unique_id())
|
||||
|
||||
func _setup_scene() -> void:
|
||||
# Simple 3/4 top-down camera for Phase 0 testing
|
||||
var camera := Camera3D.new()
|
||||
camera.current = true
|
||||
camera.position = Vector3(0, 16, 12)
|
||||
camera.rotation_degrees.x = -55
|
||||
add_child(camera)
|
||||
|
||||
# Ambient light so we can see the box meshes
|
||||
var light := DirectionalLight3D.new()
|
||||
light.light_energy = 1.0
|
||||
light.shadow_enabled = true
|
||||
light.position = Vector3(10, 20, 10)
|
||||
light.look_at(Vector3.ZERO)
|
||||
add_child(light)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Player replication handlers (called when server broadcasts via RPC)
|
||||
# ---------------------------------------------------------------------------
|
||||
func _on_remote_player_spawned(peer_id: int, pos: Vector3) -> void:
|
||||
if peer_id == multiplayer.get_unique_id():
|
||||
# This is OUR player — the server has authority over it,
|
||||
# but we don't need to create a duplicate. Our local player
|
||||
# node is managed by the server's authority system.
|
||||
# The player.gd script will handle input on this peer.
|
||||
return
|
||||
|
||||
if peer_id in remote_players:
|
||||
push_warning("[ClientMain] Remote player %d already exists, skipping" % peer_id)
|
||||
return
|
||||
|
||||
# Create a remote player node for visualization
|
||||
var player: Node3D
|
||||
if player_scene:
|
||||
player = player_scene.instantiate()
|
||||
else:
|
||||
player = Node3D.new()
|
||||
player.set_script(preload("res://scripts/network/player.gd"))
|
||||
|
||||
player.name = "RemotePlayer_%d" % peer_id
|
||||
player.set_multiplayer_authority(peer_id)
|
||||
player.position = pos
|
||||
|
||||
add_child(player, true)
|
||||
remote_players[peer_id] = player
|
||||
|
||||
print("[ClientMain] Spawned remote player %d at (%.1f, %.1f)" % [peer_id, pos.x, pos.z])
|
||||
|
||||
func _on_remote_player_despawned(peer_id: int) -> void:
|
||||
if peer_id == multiplayer.get_unique_id():
|
||||
return
|
||||
|
||||
if peer_id in remote_players:
|
||||
remote_players[peer_id].queue_free()
|
||||
remote_players.erase(peer_id)
|
||||
print("[ClientMain] Despawned remote player %d" % peer_id)
|
||||
|
||||
func _exit_tree() -> void:
|
||||
if connected:
|
||||
NetworkManager.stop()
|
||||
|
||||
# Clean up any remaining remote players
|
||||
for p in remote_players.values():
|
||||
p.queue_free()
|
||||
remote_players.clear()
|
||||
Reference in New Issue
Block a user