class_name PlayerHead extends Node3D const ACTION_CAMERA_UP: StringName = &"camera_up" const ACTION_CAMERA_DOWN: StringName = &"camera_down" const ACTION_CAMERA_LEFT: StringName = &"camera_left" const ACTION_CAMERA_RIGHT: StringName = &"camera_right" const MOUSE_SENSITIVITY_PATH: StringName = &"game/input/sensitivity" const CONTROLLER_SENSITIVITY_PATH: StringName = &"game/input/controller_sensitivity" const INVERT_X_PATH: StringName = &"game/input/camera_invert_x" const INVERT_Y_PATH: StringName = &"game/input/camera_invert_y" const CONTROLLER_INVERT_X_PATH: StringName = &"game/input/camera_controller_invert_x" const CONTROLLER_INVERT_Y_PATH: StringName = &"game/input/camera_controller_invert_y" @export var camera: ShakingCamera @export_range(-360, 360, 0.5, "suffix:°") var look_min_vertical_angle: float = -85.0 @export_range(-360, 360, 0.5, "suffix:°") var look_max_vertical_angle: float = 85.0 @export_group("Acceleration") @export var mouse_acceleration: bool = false @export_exp_easing("attenuation") var mouse_friction: float = 15.0 @export var controller_acceleration: bool = true @export_exp_easing("attenuation") var controller_friction: float = 15.0 @export_group("Input") @export_subgroup("Sensitivity") @export_range(0.0, 100.0, 0.001, "or_greater") var mouse_sensitivity: float = 1.0 @export_range(0.0, 100.0, 0.001, "or_greater") var controller_sensitivity: float = 0.75 @export_subgroup("Invert") @export var invert_x: bool = false @export var invert_y: bool = false @export var controller_invert_x: bool = false @export var controller_invert_y: bool = false @export_group("Headbobbing", "headbob_") @export var headbob_range: float = 0.25 @export var headbob_frequency: float = 0.5 @export var headbob_multiplier: float = 1.0 @export var headbob_character: Character3D @export var headbob_target: Node3D @export_group("Step Smoothing", "step_") @export var step_smoothing_target: Node3D @export var step_speed: float = 8.0 var rotational_direction: Vector2 var rotational_velocity: Vector2 var input_event: InputEvent var using_controller: bool = false # Headbobbing var headbob_time: float = 0.0 var headbob_enabled: bool = true # Stair smooting var stair_camera_offset_height: float = 0.0 var _previous_position: Vector3 var _stair_camera_target_height: float = 0.0 var _stair_camera_step_smoothing: bool = false func _ready() -> void: # Update values based on project settings. var _update_settings: Callable = func() -> void: mouse_sensitivity = ProjectSettings.get_setting(MOUSE_SENSITIVITY_PATH, 1.6) controller_sensitivity = ProjectSettings.get_setting(CONTROLLER_SENSITIVITY_PATH, 5.0) invert_x = ProjectSettings.get_setting(INVERT_X_PATH, false) invert_y = ProjectSettings.get_setting(INVERT_Y_PATH, false) controller_invert_x = ProjectSettings.get_setting(CONTROLLER_INVERT_X_PATH, false) controller_invert_y = ProjectSettings.get_setting(CONTROLLER_INVERT_Y_PATH, false) headbob_enabled = SettingsManager.get_setting(&"misc", &"headbobbing") SettingsManager.settings_changed.connect(_update_settings) ProjectSettings.settings_changed.connect(_update_settings) _update_settings.call() _setup_step_smoothing() func _process(delta: float) -> void: if not GameGlobals.in_cutscene: _process_input(delta) _perform_head_bob(delta) _process_step_smoothing(delta) func _unhandled_input(event: InputEvent) -> void: if event is InputEventMouseMotion or event is InputEventJoypadMotion: input_event = event #region Mouse/Controller rotation func apply_rotation(rot: Vector2) -> void: rotate_y(-rot.x) rotation.x = rotation.x - rot.y rotation_degrees.x = clampf(rotation_degrees.x, look_min_vertical_angle, look_max_vertical_angle) func get_sensitivity() -> float: var sensitivity: float = mouse_sensitivity if not using_controller else controller_sensitivity var frame_rate: float = Engine.get_frames_per_second() / 60.0 sensitivity *= frame_rate #print(frame_rate) return sensitivity func _process_input(delta: float) -> void: if Input.mouse_mode != Input.MOUSE_MODE_CAPTURED or not InputManager.window_focused(): return if input_event is InputEventMouseMotion: _process_mouse(input_event, delta) using_controller = false rotational_direction = Vector2.ZERO input_event = null elif input_event is InputEventJoypadMotion: _process_controller(delta) using_controller = true rotational_direction = Vector2.ZERO # Decelerating when nothing is being pressed. elif not rotational_velocity.is_zero_approx() and rotational_direction.is_zero_approx(): input_event = null if not using_controller and mouse_acceleration: rotational_velocity = _lerp_rotational_velocity(get_sensitivity() / 10, mouse_friction, delta) elif using_controller and controller_acceleration: rotational_velocity = _lerp_rotational_velocity(get_sensitivity(), controller_friction,delta) apply_rotation(rotational_velocity) else: rotational_velocity = Vector2.ZERO apply_rotation(rotational_velocity) func _process_mouse(event: InputEventMouseMotion, delta: float) -> void: # TODO: Somehow fix the fps-dependent mouse :/ # https://yosoyfreeman.github.io/godot/achieving-better-mouse-input-in-godot-4-the-perfect-camera-controller-full-project/ var viewport_transform: Transform2D = get_tree().get_root().get_final_transform() rotational_direction = event.xformed_by(viewport_transform).get_relative() if invert_x: rotational_direction.x = -rotational_direction.x if invert_y: rotational_direction.y = -rotational_direction.y if mouse_acceleration: rotational_velocity = _lerp_rotational_velocity(deg_to_rad(get_sensitivity() / 10.0), mouse_friction, delta) apply_rotation(rotational_velocity) else: apply_rotation(rotational_direction * deg_to_rad(get_sensitivity() / 10.0)) func _process_controller(delta: float) -> void: rotational_direction = Input.get_vector(ACTION_CAMERA_LEFT, ACTION_CAMERA_RIGHT, ACTION_CAMERA_UP, ACTION_CAMERA_DOWN) if controller_invert_x: rotational_direction.x = -rotational_direction.x if controller_invert_y: rotational_direction.y = -rotational_direction.y if controller_acceleration: rotational_velocity = _lerp_rotational_velocity(deg_to_rad(get_sensitivity()), controller_friction, delta) apply_rotation(rotational_velocity) else: apply_rotation(rotational_direction * deg_to_rad(get_sensitivity())) func _lerp_rotational_velocity(sensitivity: float, friction: float, delta: float) -> Vector2: return rotational_velocity.lerp(rotational_direction * sensitivity, friction * delta) #endregion #region Headbobbing func _perform_head_bob(delta: float) -> void: if not headbob_enabled: headbob_target.position = Vector3.ZERO headbob_target.rotation.z = 0.0 headbob_time = 0.0 return var headbob_speed: float = maxf((headbob_character.velocity * Utils.VEC3_HOR).length(), 1.0) SPrint.print_msgf(str("Headbob-Speed: ", headbob_speed), true) SPrint.print_msgf(str("Headbob-Clock: ", headbob_time), true) headbob_time += delta * headbob_speed * headbob_multiplier headbob_time = fposmod(headbob_time, TAU) headbob_target.position.y = sin(headbob_time * headbob_frequency) * headbob_range headbob_target.position.x = cos(headbob_time * headbob_frequency / 2) * headbob_range headbob_target.rotation.z = deg_to_rad(lerp(headbob_target.position.x, headbob_target.position.y, 0.5)) * TAU SPrint.print_msgf(str("Head Z-Rotation: ", headbob_target.rotation.z), true) #endregion #region Step Smoothing func smooth_step(height_change: float) -> void: _stair_camera_target_height -= height_change _stair_camera_step_smoothing = true func _setup_step_smoothing() -> void: headbob_character.on_stair_step.connect(_on_stair_step) stair_camera_offset_height = step_smoothing_target.position.y func _on_stair_step() -> void: smooth_step(global_position.y - _previous_position.y) func _process_step_smoothing(delta: float) -> void: if _stair_camera_step_smoothing and is_instance_valid(step_smoothing_target): _stair_camera_target_height = lerp(_stair_camera_target_height, 0.0, step_speed * delta) if absf(_stair_camera_target_height) < 0.0025: _stair_camera_target_height = 0.0 _stair_camera_step_smoothing = false var target_pos := Vector3.UP * (stair_camera_offset_height + _stair_camera_target_height) step_smoothing_target.position = target_pos.rotated(Vector3.LEFT, rotation.x) _previous_position = global_position #endregion