stick-the-quick/vfx/gameplay_camera.gd

154 lines
4.7 KiB
GDScript

class_name GameplayCamera extends Camera3D
@export var target: Node3D
@export var target_distance: float = 5.0
@export var target_min_distance: float = 2.0
@export var target_max_distance: float = 10.0
@export var target_decentering_factor: float = 0.4
@export var follow_lerp_speed: float = 0.999
@export var turn_lerp_speed: float = 0.999
@export var fov_lerp_speed: float = 0.999
@export var clip_margin: float = 0.5
@export var gimbal_margin: float = 0.75
@export var player_control := false
@export var look_speed: float = 1.0
@export var look_lerp_speed: float = 0.99
@export var zoom_speed: float = 4.0
@export var mouse_sensitivity: float = 0.03125
@export var mouse_wheel_sensitivity: float = 5.0
@export var analog_sensitivity: float = 1.0
@export var analog_zoom_sensitivity: float = 1.0
var target_direction := Vector3.ZERO
var look_impetus := Vector3.ZERO
var look_velocity := Vector3.ZERO
@onready var _raycast_object := PhysicsRayQueryParameters3D.new()
func _ready() -> void:
snap_to_target()
func _update_mouse_mode() -> void:
if player_control && !Engine.is_embedded_in_editor():
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
else:
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
func _unhandled_input(event: InputEvent) -> void:
if !player_control || Engine.is_embedded_in_editor(): return
var move_event := event as InputEventMouseMotion
var button_event := event as InputEventMouseButton
if move_event:
look_impetus.x -= move_event.relative.x*mouse_sensitivity
look_impetus.y += move_event.relative.y*mouse_sensitivity
elif button_event:
match button_event.button_index:
MOUSE_BUTTON_WHEEL_UP:
look_impetus.z -= mouse_wheel_sensitivity
MOUSE_BUTTON_WHEEL_DOWN:
look_impetus.z += mouse_wheel_sensitivity
func _handle_analog_input() -> void:
if !player_control: return
look_impetus.x -= Input.get_axis(&'look_left', &'look_right')
look_impetus.y += Input.get_axis(&'look_down', &'look_up')
look_impetus.z += Input.get_axis(&'zoom_in', &'zoom_out')
func _respond_to_impetus(delta: float) -> void:
if !target: return
var gck: float = _gimbal_check()
if (
(look_impetus.y > 0.0 and gck < -gimbal_margin) ||
(look_impetus.y < 0.0 and gck > gimbal_margin)
):
look_impetus.y = 0.0
var desired_look_velocity := Vector3(
look_impetus.x*look_speed*target_distance,
look_impetus.y*look_speed*target_distance,
look_impetus.z*zoom_speed
)
look_impetus = look_impetus.limit_length()
look_velocity = lerp(look_velocity, desired_look_velocity, look_lerp_speed)
_try_go(
global_position +
global_basis.x*look_velocity.x*delta +
global_basis.y*look_velocity.y*delta
)
target_distance = clamp(
target_distance + look_velocity.z*delta,
target_min_distance, target_max_distance
)
look_impetus = Vector3.ZERO
func _try_go(where: Vector3) -> void:
_raycast_object.from = global_position
_raycast_object.to = where + (
(where - global_position).normalized()*clip_margin
)
var raycast_results := (
get_world_3d().direct_space_state.intersect_ray(_raycast_object)
)
if raycast_results.is_empty():
global_position = where
else:
global_position = raycast_results.position + (
raycast_results.normal*clip_margin
)
func snap_to_target() -> void:
if !target: return
target_direction = (
-target.global_basis.z - target.global_basis.y/2.0
).normalized()
_follow_target(0.0, true)
func _gimbal_check() -> float:
return target_direction.dot(target.global_basis.y)
func _follow_target(
delta: float,
snap: bool = false
) -> void:
if !target: return
var velocity := Vector3.ZERO
var top_speed: float = 30.0
if target is RigidBody3D:
velocity = (target as RigidBody3D).linear_velocity
if target is Character:
top_speed = (target as Character).get_top_speed()
var decentering: float = target_distance*target_decentering_factor
var target_position := (
target.global_position + decentering*target.global_basis.y
)
var target_offset := target_position - global_position
if !snap:
target_direction = target_offset.normalized()
var desired_position := target_position - target_distance*target_direction
_try_go(desired_position if snap else lerp(
global_position, desired_position,
1.0 - (1.0 - follow_lerp_speed)**delta
))
if abs(_gimbal_check()) <= lerp(gimbal_margin, 1.0, 0.5):
var desired_basis := Basis.looking_at(
target_offset,
target.global_basis.y
)
global_basis = desired_basis if snap else global_basis.slerp(
desired_basis,
1.0 - (1.0 - turn_lerp_speed)**delta
)
var desired_fov: float = min(
75.0 + 15.0*velocity.project(global_basis.z).length()/top_speed,
179.0
)
fov = desired_fov if snap else lerp(
fov, desired_fov,
1.0 - (1.0 - fov_lerp_speed)**delta
)
func _physics_process(delta: float) -> void:
_update_mouse_mode()
_handle_analog_input()
_respond_to_impetus(delta)
_follow_target(delta)