## 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 = "68.202.6.107" @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 # Our local player node (created when server spawns us) var _local_player: Node = null var _camera: Camera3D = null # --------------------------------------------------------------------------- # 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 (fixed overhead until a player is spawned) _camera = _make_camera() 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) add_child(light) light.look_at(Vector3.ZERO) 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 _make_camera() -> Camera3D: var cam := Camera3D.new() cam.current = true cam.position = Vector3(0, 16, 12) cam.rotation_degrees.x = -55 return cam func _process(_delta: float) -> void: # Make camera follow local player if we have one if _local_player and _camera: _camera.position = _local_player.position + Vector3(0, 16, 12) # Always look down at the player var look_target := _local_player.position look_target.y = _local_player.position.y + 2.0 _camera.look_at(look_target) # --------------------------------------------------------------------------- # 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 — create a local player node and attach camera. # The player.gd script handles WASD input and sends position to server. if _local_player: push_warning("[ClientMain] Local player already exists, respawning") _local_player.queue_free() _local_player = player_scene.instantiate() _local_player.name = "LocalPlayer" _local_player.position = pos # Set authority so player.gd detects is_local = true and enables input _local_player.set_multiplayer_authority(peer_id) add_child(_local_player, true) print("[ClientMain] Spawned LOCAL player at (%.1f, %.1f)" % [pos.x, pos.z]) 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(): # Our player was despawned if _local_player: _local_player.queue_free() _local_player = null print("[ClientMain] Local player despawned") 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 players if _local_player: _local_player.queue_free() for p in remote_players.values(): p.queue_free() remote_players.clear()