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)
This commit is contained in:
@@ -1,10 +1,13 @@
|
||||
## 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.
|
||||
## 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.
|
||||
##
|
||||
## Phase 0: simple position replication via RPC + box mesh player nodes.
|
||||
## 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
|
||||
|
||||
@@ -13,7 +16,7 @@ extends Node3D
|
||||
# ---------------------------------------------------------------------------
|
||||
@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")
|
||||
@export var fps_scene: PackedScene = preload("res://client/template/player_character.tscn")
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# State
|
||||
@@ -22,9 +25,12 @@ extends Node3D
|
||||
var remote_players: Dictionary = {}
|
||||
var connected: bool = false
|
||||
|
||||
# Our local player node (created when server spawns us)
|
||||
# Our local FPS character
|
||||
var _local_player: Node = null
|
||||
var _camera: Camera3D = 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
|
||||
@@ -49,80 +55,73 @@ func _connect_to_server() -> void:
|
||||
_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
|
||||
# ---------------------------------------------------------------------------
|
||||
# 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)
|
||||
|
||||
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)
|
||||
@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 — create a local player node and attach camera.
|
||||
# The player.gd script handles WASD input and sends position to server.
|
||||
# THIS IS OUR PLAYER — spawn the full FPS character
|
||||
if _local_player:
|
||||
push_warning("[ClientMain] Local player already exists, respawning")
|
||||
push_warning("[ClientMain] Local player already exists, despawning old")
|
||||
_local_player.queue_free()
|
||||
|
||||
_local_player = player_scene.instantiate()
|
||||
_local_player = fps_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)
|
||||
_local_player.global_position = pos
|
||||
add_child(_local_player, true)
|
||||
|
||||
print("[ClientMain] Spawned LOCAL player at (%.1f, %.1f)" % [pos.x, pos.z])
|
||||
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
|
||||
var player: Node3D
|
||||
if player_scene:
|
||||
player = player_scene.instantiate()
|
||||
else:
|
||||
player = Node3D.new()
|
||||
player.set_script(preload("res://scripts/network/player.gd"))
|
||||
|
||||
# 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
|
||||
|
||||
@@ -130,7 +129,6 @@ func _on_remote_player_spawned(peer_id: int, pos: Vector3) -> void:
|
||||
|
||||
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
|
||||
@@ -146,7 +144,6 @@ 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():
|
||||
|
||||
@@ -215,3 +215,23 @@ func _physics_process(delta: float) -> void:
|
||||
|
||||
# Future: authoritative physics tick, state broadcast, etc.
|
||||
pass
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Client position replication (receives from FPS character clients)
|
||||
# ---------------------------------------------------------------------------
|
||||
@rpc("unreliable", "any_peer")
|
||||
func _send_position(pos: Vector3) -> void:
|
||||
# Called by remote clients to update their position on the server.
|
||||
# Broadcast to all other peers so they see the player move.
|
||||
var peer_id: int = multiplayer.get_remote_sender_id()
|
||||
if peer_id in players:
|
||||
players[peer_id].position = pos
|
||||
# Re-broadcast to all other peers
|
||||
_replicate_position.rpc(pos, peer_id)
|
||||
|
||||
@rpc("unreliable", "authority")
|
||||
func _replicate_position(pos: Vector3, exclude_peer: int) -> void:
|
||||
# All clients receive this and update the specific player's position.
|
||||
# We need to find which remote player this belongs to by excluding our own.
|
||||
# For now, the non-authority clients will need to filter — handled in client_main.gd
|
||||
pass
|
||||
|
||||
Reference in New Issue
Block a user