rnd(M02): extract third-person camera rig
Work-Package: M02-RND-CAMERA-001
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
class_name ThirdPersonCameraRig
|
||||
extends Node3D
|
||||
|
||||
## Third-person orbit/zoom state and Camera3D presentation adapter.
|
||||
## The rig owns camera input state and local transforms. Character movement,
|
||||
## terrain, rendering and collision-policy decisions remain external boundaries.
|
||||
|
||||
const NO_CAMERA_COLLISION_POLICY_SCRIPT := preload("res://src/scenes/player/no_camera_collision_policy.gd")
|
||||
|
||||
## Relative Camera3D child used by this rig.
|
||||
@export var camera_path: NodePath = NodePath("Camera3D")
|
||||
## Mouse radians applied per input pixel.
|
||||
@export var mouse_sensitivity_radians_per_pixel: float = 0.003
|
||||
## Minimum orbit pitch in radians.
|
||||
@export var minimum_pitch_radians: float = deg_to_rad(-65.0)
|
||||
## Maximum orbit pitch in radians.
|
||||
@export var maximum_pitch_radians: float = deg_to_rad(35.0)
|
||||
## Pivot height above the character origin in Godot units.
|
||||
@export var pivot_height_units: float = 1.7
|
||||
## Initial requested camera distance in Godot units.
|
||||
@export var initial_distance_units: float = 8.0
|
||||
## Minimum user-requested zoom distance in Godot units.
|
||||
@export var minimum_distance_units: float = 2.0
|
||||
## Maximum user-requested zoom distance in Godot units.
|
||||
@export var maximum_distance_units: float = 18.0
|
||||
## Distance change per zoom action in Godot units.
|
||||
@export var zoom_step_units: float = 1.0
|
||||
## Camera far plane retained from the renderer sandbox baseline.
|
||||
@export var camera_far_plane_units: float = 50000.0
|
||||
|
||||
var yaw_radians: float:
|
||||
get:
|
||||
return _yaw_radians
|
||||
|
||||
var pitch_radians: float:
|
||||
get:
|
||||
return _pitch_radians
|
||||
|
||||
var requested_distance_units: float:
|
||||
get:
|
||||
return _requested_distance_units
|
||||
|
||||
var resolved_distance_units: float:
|
||||
get:
|
||||
return _resolved_distance_units
|
||||
|
||||
var is_mouse_captured: bool:
|
||||
get:
|
||||
return _is_mouse_captured
|
||||
|
||||
var _character_body: Node3D
|
||||
var _camera: Camera3D
|
||||
var _collision_policy: CameraCollisionPolicy
|
||||
var _yaw_radians := 0.0
|
||||
var _pitch_radians := deg_to_rad(-18.0)
|
||||
var _requested_distance_units := 8.0
|
||||
var _resolved_distance_units := 8.0
|
||||
var _is_mouse_captured := false
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_camera = get_node_or_null(camera_path) as Camera3D
|
||||
if _camera == null:
|
||||
push_error("ThirdPersonCameraRig: Camera3D not found at %s" % camera_path)
|
||||
if _collision_policy == null:
|
||||
_collision_policy = NO_CAMERA_COLLISION_POLICY_SCRIPT.new()
|
||||
_pitch_radians = clampf(_pitch_radians, minimum_pitch_radians, maximum_pitch_radians)
|
||||
_requested_distance_units = clampf(
|
||||
initial_distance_units,
|
||||
minimum_distance_units,
|
||||
maximum_distance_units
|
||||
)
|
||||
if _camera != null:
|
||||
_camera.current = true
|
||||
_camera.far = camera_far_plane_units
|
||||
refresh_camera_transform()
|
||||
|
||||
|
||||
func _physics_process(_delta_seconds: float) -> void:
|
||||
refresh_camera_transform()
|
||||
|
||||
|
||||
func _exit_tree() -> void:
|
||||
if _is_mouse_captured:
|
||||
_is_mouse_captured = false
|
||||
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||
|
||||
|
||||
## Attaches the character transform rotated by orbit input and captures its initial yaw.
|
||||
func initialize_for_character(character_body: Node3D) -> void:
|
||||
_character_body = character_body
|
||||
if _character_body != null:
|
||||
_yaw_radians = _character_body.rotation.y
|
||||
refresh_camera_transform()
|
||||
|
||||
|
||||
## Replaces camera-distance collision behavior and immediately refreshes the camera.
|
||||
## Passing null is rejected and preserves the current policy.
|
||||
func set_collision_policy(collision_policy: CameraCollisionPolicy) -> void:
|
||||
if collision_policy == null:
|
||||
push_error("ThirdPersonCameraRig: collision policy cannot be null")
|
||||
return
|
||||
_collision_policy = collision_policy
|
||||
refresh_camera_transform()
|
||||
|
||||
|
||||
## Applies remappable camera actions and orbit mouse motion.
|
||||
## Returns true when the event changed or consumed camera state.
|
||||
func handle_camera_input(event: InputEvent) -> bool:
|
||||
if event.is_action_pressed(PlayerInputActions.CAMERA_ROTATE):
|
||||
_is_mouse_captured = true
|
||||
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
||||
return true
|
||||
if event.is_action_released(PlayerInputActions.CAMERA_ROTATE):
|
||||
_is_mouse_captured = false
|
||||
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||
return true
|
||||
if event.is_action_pressed(PlayerInputActions.CAMERA_ZOOM_IN):
|
||||
_requested_distance_units = clampf(
|
||||
_requested_distance_units - zoom_step_units,
|
||||
minimum_distance_units,
|
||||
maximum_distance_units
|
||||
)
|
||||
refresh_camera_transform()
|
||||
return true
|
||||
if event.is_action_pressed(PlayerInputActions.CAMERA_ZOOM_OUT):
|
||||
_requested_distance_units = clampf(
|
||||
_requested_distance_units + zoom_step_units,
|
||||
minimum_distance_units,
|
||||
maximum_distance_units
|
||||
)
|
||||
refresh_camera_transform()
|
||||
return true
|
||||
if event.is_action_pressed(PlayerInputActions.RELEASE_CURSOR):
|
||||
_is_mouse_captured = false
|
||||
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||
return true
|
||||
if _is_mouse_captured and event is InputEventMouseMotion:
|
||||
_yaw_radians -= event.relative.x * mouse_sensitivity_radians_per_pixel
|
||||
_pitch_radians = clampf(
|
||||
_pitch_radians - event.relative.y * mouse_sensitivity_radians_per_pixel,
|
||||
minimum_pitch_radians,
|
||||
maximum_pitch_radians
|
||||
)
|
||||
if _character_body != null:
|
||||
_character_body.rotation.y = _yaw_radians
|
||||
refresh_camera_transform()
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
## Returns the pivot's Godot-world basis for camera-relative sandbox flight.
|
||||
func godot_world_flight_movement_basis() -> Basis:
|
||||
return global_basis
|
||||
|
||||
|
||||
## Applies pivot and Camera3D local transforms using the active collision policy.
|
||||
func refresh_camera_transform() -> void:
|
||||
position = Vector3(0.0, pivot_height_units, 0.0)
|
||||
rotation.x = _pitch_radians
|
||||
if _camera == null:
|
||||
return
|
||||
var policy_distance := _collision_policy.resolve_camera_distance(
|
||||
self,
|
||||
_requested_distance_units
|
||||
) if _collision_policy != null else _requested_distance_units
|
||||
_resolved_distance_units = clampf(policy_distance, 0.0, _requested_distance_units)
|
||||
_camera.position = Vector3(0.0, 0.0, _resolved_distance_units)
|
||||
_camera.rotation = Vector3.ZERO
|
||||
Reference in New Issue
Block a user