stick-the-quick/vfx/gameplay_camera.gd

197 lines
5.8 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 target_leveling_bias: float = 20.0
@export var allow_losing_target := false
@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.5
@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')*analog_sensitivity
)
look_impetus.y += (
Input.get_axis(&'look_down', &'look_up')*analog_sensitivity
)
look_impetus.z += (
Input.get_axis(&'zoom_in', &'zoom_out')*analog_sensitivity
)
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 _find_closest_safe_spot(
where: Vector3, from: Vector3 = global_position
) -> Vector3:
_raycast_object.from = from
_raycast_object.to = where + (
(where - from).normalized()*clip_margin
)
if target:
_raycast_object.exclude = [target]
else:
_raycast_object.exclude = []
var raycast_results := (
get_world_3d().direct_space_state.intersect_ray(_raycast_object)
)
var desired_position: Vector3
if raycast_results.is_empty():
desired_position = where
else:
desired_position = raycast_results.position + (
raycast_results.normal*clip_margin
)
if target && !allow_losing_target:
allow_losing_target = true
var posn_a := _find_closest_safe_spot(
desired_position, target.global_position
)
if target is Character && target.is_node_ready():
var posn_b := _find_closest_safe_spot(
desired_position,
target.global_position + target.height*target.global_basis.y
)
if (
(posn_b - desired_position).length() <
(posn_a - desired_position).length()
):
posn_a = posn_b
allow_losing_target = false
return posn_a
else:
return desired_position
func _try_go(where: Vector3) -> void:
global_position = _find_closest_safe_spot(where)
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 leveling := (
target_leveling_bias*velocity.project(target.global_basis.y)
)*delta
var target_position := (
target.global_position +
decentering*target.global_basis.y +
velocity*delta +
leveling
)
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 !is_zero_approx(1.0 - abs(_gimbal_check())):
var desired_basis := Basis.looking_at(
target_offset - leveling,
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)