Files
tactical-shooter/scripts/network/client_main.gd
T
shawn f0c95dcfd2 fix: create local player on client so WASD movement works
client_main.gd: when server spawns our player, create a player node with authority

- Camera follows local player from 3/4 overhead view

- player.gd WASD input sends position to server via RPC

- Server validates and broadcasts to all peers
2026-07-02 00:15:08 -04:00

155 lines
5.3 KiB
GDScript

## 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()