fix: FPS character controller review fixes

- Camera: Make view bob additive to crouch eye height (was overwriting position.y)
- Camera: Sustain sprint FOV kick while sprinting (was recovering immediately)
- Camera: Remove unused _was_sprinting variable
- Controller: Use just_pressed for jump (was sending every frame while held)
- Controller: Direct input dict assignment instead of merge() to avoid alloc
- Controller: Add clearer comments on server-authoritative readback mode
This commit is contained in:
2026-07-01 18:35:16 -04:00
parent e9dc05983c
commit 589b90d886
2 changed files with 30 additions and 24 deletions
@@ -186,7 +186,7 @@ func _unhandled_input(event: InputEvent) -> void:
func _physics_process(delta: float) -> void:
# 1. Capture input state
var input_dir := _get_move_direction()
var jump_pressed := Input.is_action_just_pressed(&"jump") or Input.is_action_pressed(&"jump")
var jump_pressed := Input.is_action_just_pressed(&"jump")
var sprint_pressed := Input.is_action_pressed(&"sprint")
var crouch_pressed := Input.is_action_pressed(&"crouch")
var shoot_pressed := _mouse_clicked_this_frame or Input.is_action_pressed(&"shoot")
@@ -229,18 +229,16 @@ func _physics_process(delta: float) -> void:
# 6. Send input to simulation server
if _server != null and entity_id >= 0:
# Build input for SimulationServer
_input_dict.merge({
"move_direction": input_dir,
"look_yaw": rad_to_deg(_yaw),
"look_pitch": rad_to_deg(_pitch),
"jump": jump_pressed,
"crouch": _crouch_active,
"sprint": _sprint_active,
"shoot": shoot_pressed,
"aim": aim_pressed,
"input_sequence": _input_sequence,
}, true)
# Build input for SimulationServer (mutate cached dict to avoid alloc)
_input_dict["move_direction"] = input_dir
_input_dict["look_yaw"] = rad_to_deg(_yaw)
_input_dict["look_pitch"] = rad_to_deg(_pitch)
_input_dict["jump"] = jump_pressed
_input_dict["crouch"] = _crouch_active
_input_dict["sprint"] = _sprint_active
_input_dict["shoot"] = shoot_pressed
_input_dict["aim"] = aim_pressed
_input_dict["input_sequence"] = _input_sequence
_server.apply_input(entity_id, _input_dict)
@@ -251,14 +249,17 @@ func _physics_process(delta: float) -> void:
# 7. Read server state back (for local simulation / listen server)
# The SimulationServer.tick() call happens in the game manager / network layer.
# Here we read the entity's canonical position from the server.
# Here we optionally read the entity's canonical position from the server.
# NOTE: On a dedicated client, position comes from the network replication
# layer (server snapshot), not from direct server entity access.
# On a listen server (host+client same process) or in standalone mode,
# reading position directly from the server entity is correct.
if _server != null and entity_id >= 0:
var entity = _server.get_entity(entity_id)
if entity != null and entity.is_alive():
# Server position is ground truth — apply it to the visual body
# (only correct on listen-server; pure clients must use replication)
global_position = entity.position
# Note: rotation is already set from yaw/pitch above (client-side)
# On a pure client the server snapshot override is applied differently.
else:
# Standalone mode: do local CharacterBody3D physics
_move_local(input_dir, delta, jump_pressed)