class_name RunnerObserver extends Camera3D signal area_entered(where: Area3D) signal area_exited(where: Area3D) const RADIUS = 1.0 const BIRDS_EYE_FACTOR: float = 1.5 const DECENTERING_FACTOR: float = 0.4 const UP_LERP_WEIGHT: float = 0.03125 const FOLLOW_LERP_WEIGHT: float = 1.0 const FOV_LERP_WEIGHT: float = 0.0625 const DEFAULT_DISTANCE: float = 5.0 const TARGET_OVERRIDE_TURN_LERP_WEIGHT: float = 0.03125 const TARGET_OVERRIDE_TURN_TIMEOUT: float = 3.0 const TURN_LERP_WEIGHT: float = 1.0 const ANTICLIP: float = 0.125 const TARGET_OVERRIDE_DECENTERING_METAFACTOR: float = 0.25 @onready var runner := $'..' as Runner var maintain_distance: float var maintain_direction: Vector3 var up_normal := Vector3.UP var _target_override_turn_timeout: float = -1.0 @onready var _wall_raycast = PhysicsRayQueryParameters3D.new() var _target_override: Node3D = null var target_override: Node3D: get: return _target_override set(target): if target != _target_override: _target_override_turn_timeout = TARGET_OVERRIDE_TURN_TIMEOUT _target_override = target @onready var _hitbox := $Area3D as Area3D func _ready() -> void: top_level = true _hitbox.area_entered.connect(_emit_area_entered) _hitbox.area_exited.connect(_emit_area_exited) if runner: switch_runner(runner) func switch_runner(whom: Runner, distance: float = DEFAULT_DISTANCE) -> void: runner = whom if runner: maintain_distance = distance maintain_direction = runner.basis.z - runner.basis.y/2.0 reorient(false, true) func detach() -> void: switch_runner(null) func _try_go(where: Vector3) -> void: _wall_raycast.from = global_position _wall_raycast.to = where var raycast_results := ( get_world_3d().direct_space_state.intersect_ray(_wall_raycast) ) if raycast_results.is_empty(): global_position = where else: global_position = raycast_results.position + ( global_position - raycast_results.position + raycast_results.normal ).normalized()*ANTICLIP func reorient(directional_follow: bool = true, snap: bool = false) -> void: var target := runner as Node3D var decentering_factor: float = DECENTERING_FACTOR if target_override: target = target_override decentering_factor = -decentering_factor*( TARGET_OVERRIDE_DECENTERING_METAFACTOR ) var decentering := ( BIRDS_EYE_FACTOR*maintain_distance*decentering_factor*up_normal ) if target: var lv := Vector3.ZERO var mlv: float = 30.0 if target is Runner: lv = (target as Runner).linear_velocity mlv = (target as Runner).top_run_speed elif target is RigidBody3D: lv = (target as RigidBody3D).linear_velocity if directional_follow: maintain_direction = ( target.global_position - global_position + decentering ).normalized() up_normal = up_normal.lerp( (target as Runner).up_normal if target is Runner else Vector3.UP, 1.0 if snap else UP_LERP_WEIGHT ).normalized() _try_go(global_position.lerp( target.global_position - maintain_distance*maintain_direction + decentering, 1.0 if snap else (1.0 - 1.0/(1.0 + lv.length())) )) if (( target.global_position - global_position ).cross(up_normal).length() >= 0.1): var desired_basis := Basis.looking_at( target.global_position + ( target.global_position - global_position ).length()*decentering_factor*up_normal - global_position, up_normal ) basis = basis.slerp( desired_basis, lerpf( TURN_LERP_WEIGHT, TARGET_OVERRIDE_TURN_LERP_WEIGHT, clampf( _target_override_turn_timeout / TARGET_OVERRIDE_TURN_TIMEOUT, 0.0, 1.0 ) )*( (1.0 + target_override.linear_velocity.length()) if target_override is RigidBody3D else 1.0 ) ) fov = lerp(fov, min(75.0 + 15.0*( lv.project(basis.z).length()/mlv ), 179.0), FOV_LERP_WEIGHT) func _physics_process(delta: float) -> void: reorient() _target_override_turn_timeout -= delta func _emit_area_entered(where: Area3D) -> void: area_entered.emit(where) func _emit_area_exited(where: Area3D) -> void: area_exited.emit(where)