So I've got this problem where the head of my my FPS_Controller is very low when running the game; making everything look taller. In the editor, everything the head and therefore the camera is at the correct height.

However, when running the game, something is dragging the head down to a child-like height.

I don't know what's the solution is, as I don't fully remember how my code work. Even though my FPS_Controller has gotten more complex( as it has a FSM), I was intially following this Youtube tutorial on how to make an FPS_Controller:
So if it helps, you can watch that tutorial to help understand how my FPS_Controller works; I'll also share with you some scripts for my FPS_Controller; starting with FPS_Controller.gd:
extends CharacterBody3D
var is_exiting_sprint := false
# --- Movement Constants ---
const WALK_SPEED = 5.0
const RUN_SPEED = 10.0
const CROUCH_SPEED = 2.5
const JUMP_VELOCITY = 5.9
# --- Head Bobbing ---
const BOB_FREQ = 2.4
const BOB_AMP = 0.08
# --- Movement State ---
var speed := WALK_SPEED
var just_jumped := false
var was_sprinting := false
var is_sprinting := false
var t_bob := 0.0
# --- Crouching Config ---
@export var crouch_height := 1.5
@export var crouch_transition := 16.0
# --- Node References ---
@onready var head: Node3D = $%Head
@onready var camera: Camera3D = $%Camera3D
@onready var guncam = $%GunCam
@onready var collision_shape: CollisionShape3D = $%CollisionShape3D
@onready var top_cast: ShapeCast3D = $%TopCast
@onready var health_bar : ProgressBar = $%Health_Bar
@onready var stand_height = collision_shape.shape.height
@export_group("Keys") #Keys for unlocking doors
@export var blue_key = false
@export var red_key = false
@export var yellow_key = false
@onready var keys = [blue_key,red_key,yellow_key]
var health := 100 # Current HP, matches ProgressBar max
func _ready():
Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
func _unhandled_input(event : InputEvent):
if event is InputEventMouseButton:
Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
elif event.is_action_pressed("ui_cancel"):
Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
if Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED:
if event is InputEventMouseMotion:
head.rotate_y(-event.relative.x * 0.01)
camera.rotate_x(-event.relative.y * 0.01)
camera.rotation.x = clamp(camera.rotation.x, deg_to_rad(-65), deg_to_rad(60))
func _process(delta):
guncam.global_transform = camera.global_transform
handle_weapon_pickup()
func handle_weapon_pickup():
if not %PlayerFSM.can_use_weapon():
return # Early exit while sprinting
if Input.is_action_just_pressed("Interact"):
var raycast = $%RayCast3D
if raycast.is_colliding():
var target = raycast.get_collider()
var hit_distance = raycast.global_transform.origin.distance_to(raycast.get_collision_point())
if target is WeaponPickup and hit_distance <= target.pickup_distance:
if has_node("%WeaponManager"):
var manager = get_node("%WeaponManager")
manager.unlock_weapon(target.weapon_index)
target.emit_signal("picked_up", self)
target.queue_free()
func take_damage(amount: int) -> void:
if health <= 0:
return # Already "dead", ignore extra damage
health = max(health - amount, 0)
health_bar.value = health
YOu don't see the code for the FSM and it's respective states but, I doubt that's important. What's more important is the PlayerState script. It consist of functions that are shared by all the states via inheritance. These functions help controll the FPS_Controller. PlayerState.gd:
extends State
class_name PlayerState
var weapon_manager: WeaponManager
@export var debugging : bool = true
@export var leaving_state_message: String = ""
@export var entering_state_message: String = ""
func _ready():
# Automatically try to get weapon manager from parent (if not already set manually)
if not weapon_manager and parent.has_node("WeaponManager"):
weapon_manager = parent.get_node("WeaponManager")
func print_debug_message(message : String):
if debugging == true:
print(message)
func move_player(delta: float):
parent.just_jumped = false
handle_gravity_and_jump(delta)
handle_speed_and_stance(delta)
handle_movement_input(delta)
apply_headbob_and_slide(delta)
func handle_gravity_and_jump(delta: float):
if not parent.is_on_floor():
parent.velocity += parent.get_gravity() * delta
else:
parent.velocity.y = 0.0
if Input.is_action_just_pressed("Jump"):
parent.velocity.y = parent.JUMP_VELOCITY
parent.just_jumped = true
parent.was_sprinting = false
func handle_speed_and_stance(delta: float):
if parent.is_on_floor():
if Input.is_action_pressed("Crouch") or parent.top_cast.is_colliding():
parent.speed = parent.CROUCH_SPEED
crouch(delta)
else:
parent.speed = parent.WALK_SPEED
crouch(delta, true)
else:
parent.speed = parent.WALK_SPEED
func handle_movement_input(delta: float):
var input_dir = Input.get_vector("Left", "Right", "Up", "Down")
var direction = (parent.head.basis * Vector3(input_dir.x, 0, input_dir.y)).normalized()
if direction:
parent.velocity.x = direction.x * parent.speed
parent.velocity.z = direction.z * parent.speed
else:
parent.velocity.x = 0.0
parent.velocity.z = 0.0
func apply_headbob_and_slide(delta: float):
parent.t_bob += delta * parent.velocity.length() * float(parent.is_on_floor())
parent.camera.transform.origin = _headbob(parent.t_bob)
parent.move_and_slide()
func _headbob(time: float) -> Vector3:
return Vector3(
cos(time * parent.BOB_FREQ / 2) * parent.BOB_AMP,
sin(time * parent.BOB_FREQ) * parent.BOB_AMP,
0
)
func crouch(delta: float, reverse := false):
var target_height = parent.stand_height if reverse else parent.crouch_height
parent.collision_shape.shape.height = lerp(parent.collision_shape.shape.height, target_height, parent.crouch_transition * delta)
parent.collision_shape.position.y = lerp(parent.collision_shape.position.y, target_height * 0.5, parent.crouch_transition * delta)
parent.head.position.y = lerp(parent.head.position.y, target_height - 1, parent.crouch_transition * delta)
I think that's all the information that you might need to help me solve this problem. Can you spot what the problem is and what I need to fix? That would be greatly appreciated.
Edit: I think the culprit is the stand_height variable but, I don't know how to do anything about. Any pseudo-code explaining the solution would be greatly appreciated.
Edit again: After testing, I've narrowed the culprit down to this line with the crouch function:
parent.collision_shape.position.y = lerp(parent.collision_shape.position.y, target_height * 0.5, parent.crouch_transition * delta)
When I remove the line, I end up with a better height for the head; the only problem is that I crouch too low without it. So instead of removing the line, I need to find some way of "fixing" it.