Files
tactical-shooter/scripts/network/client_main.gd
T
shawn ad48f38ca5 feat: integrate ChaffGames FPS template as local player controller
Replaces the overhead box-mesh view with a full FPS character:

- mouse look, WASD movement, jump (Space), crouch (C), sprint (Shift), lean (Q/E)

- 6 weapons (LMB shoot, R reload, scroll switch, F melee, G drop)

- Crosshair HUD, ammo counter, sprint bar

- Client sends position to server at 20Hz for multiplayer visibility

- Server broadcasts positions to all peers

- Remote players shown as boxes (placeholder)

source: https://godotengine.org/asset-library/asset/2652 (MIT license)
2026-07-02 00:26:45 -04:00

152 lines
5.3 KiB
GDScript

## 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://client/template/player_character.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()