173 lines
5.3 KiB
GDScript
173 lines
5.3 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.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, from: Vector3 = global_position) -> void:
|
|
_raycast_object.from = from
|
|
_raycast_object.to = where + (
|
|
(where - global_position).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
|
|
_try_go(desired_position, target.global_position)
|
|
allow_losing_target = false
|
|
else:
|
|
global_position = desired_position
|
|
|
|
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)
|