## Client Main — Client entry point for testing. ## ## Connects to the server. For the local player, spawns a full FPS ## character (from the ChaffGames FPS template) with mouse look, ## weapons, HUD, and movement. For remote players, creates simple ## box-mesh representations for visibility. ## ## The local FPS character moves independently (single-player style). ## Position is replicated to the server each tick so other clients ## see where we are. extends Node3D # --------------------------------------------------------------------------- # Exports # --------------------------------------------------------------------------- @export var server_host: String = "68.202.6.107" @export var server_port: int = 34197 @export var fps_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 FPS character var _local_player: Node = null # Position sync timer var _sync_timer: float = 0.0 const SYNC_RATE: float = 20.0 # Hz — how often to send position to server # --------------------------------------------------------------------------- # 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 connected = true # Connect replication signals from NetworkManager 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()) # --------------------------------------------------------------------------- # Local FPS character + position sync # --------------------------------------------------------------------------- func _process(delta: float) -> void: # Sync our position to the server at a fixed rate if _local_player: _sync_timer += delta if _sync_timer >= 1.0 / SYNC_RATE: _sync_timer = 0.0 var pos: Vector3 = _local_player.global_position rpc_id(1, "_send_position", pos) @rpc("unreliable") func _send_position(pos: Vector3) -> void: # Server receives our position and broadcasts to other clients pass # Server broadcasts other players' positions to us @rpc("unreliable", "authority") func _replicate_position(pos: Vector3, moving_peer_id: int) -> void: # Update the position of a remote player on our screen if moving_peer_id in remote_players: remote_players[moving_peer_id].position = pos # --------------------------------------------------------------------------- # 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 — spawn the full FPS character if _local_player: push_warning("[ClientMain] Local player already exists, despawning old") _local_player.queue_free() _local_player = fps_scene.instantiate() _local_player.name = "LocalPlayer" _local_player.global_position = pos add_child(_local_player, true) print("[ClientMain] Spawned LOCAL FPS 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 (simple box mesh) var player := Node3D.new() player.name = "RemotePlayer_%d" % peer_id player.set_multiplayer_authority(peer_id) player.position = pos # Add a simple box mesh so we can see where other players are var mesh := MeshInstance3D.new() mesh.mesh = BoxMesh.new() mesh.mesh.size = Vector3(1, 1, 1) mesh.position.y = 0.5 player.add_child(mesh) 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(): 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() if _local_player: _local_player.queue_free() for p in remote_players.values(): p.queue_free() remote_players.clear()