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:
@@ -70,12 +70,12 @@ var _weapon_rest: Transform3D
|
|||||||
var _base_fov: float = 75.0
|
var _base_fov: float = 75.0
|
||||||
var _current_fov_offset: float = 0.0
|
var _current_fov_offset: float = 0.0
|
||||||
|
|
||||||
## Sway state.
|
## Sway state (used as both current value and interpolation target).
|
||||||
var _sway_current: Vector2 = Vector2.ZERO
|
var _sway_current: Vector2 = Vector2.ZERO
|
||||||
var _sway_target: Vector2 = Vector2.ZERO
|
var _sway_target: Vector2 = Vector2.ZERO
|
||||||
|
|
||||||
## Sprint state cache (detect transition for FOV kick).
|
## Base eye Y set by parent controller's crouch logic (for additive bob).
|
||||||
var _was_sprinting: bool = false
|
var _base_eye_y: float = 0.0
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Lifecycle
|
# Lifecycle
|
||||||
@@ -100,6 +100,10 @@ func _process(delta: float) -> void:
|
|||||||
if _controller == null:
|
if _controller == null:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Capture the eye height set by parent controller's crouch logic
|
||||||
|
# before we apply view bob offsets (which must be additive).
|
||||||
|
_base_eye_y = position.y
|
||||||
|
|
||||||
# FOV management
|
# FOV management
|
||||||
_update_fov(delta)
|
_update_fov(delta)
|
||||||
|
|
||||||
@@ -119,16 +123,15 @@ func _process(delta: float) -> void:
|
|||||||
func _update_fov(delta: float) -> void:
|
func _update_fov(delta: float) -> void:
|
||||||
var target_offset: float = 0.0
|
var target_offset: float = 0.0
|
||||||
|
|
||||||
# Sprint FOV kick
|
# Sprint FOV kick — sustained while sprinting (not just on transition)
|
||||||
if _controller.is_sprinting() and not _was_sprinting:
|
if _controller.is_sprinting():
|
||||||
target_offset = sprint_fov_kick
|
target_offset = sprint_fov_kick
|
||||||
|
|
||||||
# Shoot FOV kick is triggered externally via trigger_shoot_fov()
|
# Shoot FOV kick is triggered externally via trigger_shoot_fov()
|
||||||
|
# and decays toward the sprint/walk target naturally via move_toward.
|
||||||
_current_fov_offset = move_toward(_current_fov_offset, target_offset, fov_recovery_speed * delta)
|
_current_fov_offset = move_toward(_current_fov_offset, target_offset, fov_recovery_speed * delta)
|
||||||
fov = _base_fov + _current_fov_offset
|
fov = _base_fov + _current_fov_offset
|
||||||
|
|
||||||
_was_sprinting = _controller.is_sprinting()
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# View bobbing
|
# View bobbing
|
||||||
@@ -157,7 +160,9 @@ func _update_bob(delta: float) -> void:
|
|||||||
|
|
||||||
# Apply bob to camera position (relative to parent's eye offset)
|
# Apply bob to camera position (relative to parent's eye offset)
|
||||||
position.x = _bob_offset.x
|
position.x = _bob_offset.x
|
||||||
position.y = _bob_offset.y
|
# Bob is additive on Y so it does NOT overwrite the crouch eye height
|
||||||
|
# set by the parent FPSCharacterController._update_crouch().
|
||||||
|
position.y = _base_eye_y + _bob_offset.y
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -186,7 +186,7 @@ func _unhandled_input(event: InputEvent) -> void:
|
|||||||
func _physics_process(delta: float) -> void:
|
func _physics_process(delta: float) -> void:
|
||||||
# 1. Capture input state
|
# 1. Capture input state
|
||||||
var input_dir := _get_move_direction()
|
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 sprint_pressed := Input.is_action_pressed(&"sprint")
|
||||||
var crouch_pressed := Input.is_action_pressed(&"crouch")
|
var crouch_pressed := Input.is_action_pressed(&"crouch")
|
||||||
var shoot_pressed := _mouse_clicked_this_frame or Input.is_action_pressed(&"shoot")
|
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
|
# 6. Send input to simulation server
|
||||||
if _server != null and entity_id >= 0:
|
if _server != null and entity_id >= 0:
|
||||||
# Build input for SimulationServer
|
# Build input for SimulationServer (mutate cached dict to avoid alloc)
|
||||||
_input_dict.merge({
|
_input_dict["move_direction"] = input_dir
|
||||||
"move_direction": input_dir,
|
_input_dict["look_yaw"] = rad_to_deg(_yaw)
|
||||||
"look_yaw": rad_to_deg(_yaw),
|
_input_dict["look_pitch"] = rad_to_deg(_pitch)
|
||||||
"look_pitch": rad_to_deg(_pitch),
|
_input_dict["jump"] = jump_pressed
|
||||||
"jump": jump_pressed,
|
_input_dict["crouch"] = _crouch_active
|
||||||
"crouch": _crouch_active,
|
_input_dict["sprint"] = _sprint_active
|
||||||
"sprint": _sprint_active,
|
_input_dict["shoot"] = shoot_pressed
|
||||||
"shoot": shoot_pressed,
|
_input_dict["aim"] = aim_pressed
|
||||||
"aim": aim_pressed,
|
_input_dict["input_sequence"] = _input_sequence
|
||||||
"input_sequence": _input_sequence,
|
|
||||||
}, true)
|
|
||||||
|
|
||||||
_server.apply_input(entity_id, _input_dict)
|
_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)
|
# 7. Read server state back (for local simulation / listen server)
|
||||||
# The SimulationServer.tick() call happens in the game manager / network layer.
|
# 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:
|
if _server != null and entity_id >= 0:
|
||||||
var entity = _server.get_entity(entity_id)
|
var entity = _server.get_entity(entity_id)
|
||||||
if entity != null and entity.is_alive():
|
if entity != null and entity.is_alive():
|
||||||
# Server position is ground truth — apply it to the visual body
|
# 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
|
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:
|
else:
|
||||||
# Standalone mode: do local CharacterBody3D physics
|
# Standalone mode: do local CharacterBody3D physics
|
||||||
_move_local(input_dir, delta, jump_pressed)
|
_move_local(input_dir, delta, jump_pressed)
|
||||||
|
|||||||
Reference in New Issue
Block a user