Camera work

This commit is contained in:
2026-06-11 21:07:03 -05:00
parent 0b3ea48cb8
commit 8c90db2d69
5 changed files with 129 additions and 21 deletions
+84 -20
View File
@@ -7,30 +7,95 @@ const COLLISION_MARGIN:float = 0.3
@export var pivotOffset:Vector3 = Vector3(0, 1.2, 0) @export var pivotOffset:Vector3 = Vector3(0, 1.2, 0)
@export_category("Orbit") @export_category("Orbit")
@export var distance:float = 10.0 @export var distance:float = 7.0
@export var minDistance:float = 1.5 @export var minDistance:float = 1.5
@export var orbitSensitivity:float = 120.0 @export var orbitSensitivity:float = 144.0
@export var orbitAcceleration:float = 600.0
@export var pitchMin:float = -10.0 @export var pitchMin:float = -10.0
@export var pitchMax:float = 70.0 @export var pitchMax:float = 70.0
@export_category("Mouse")
@export var mouseSensitivity:float = 0.3
@export_category("Auto-center")
@export var autoReturnDelay:float = 2.0
@export var autoReturnSpeed:float = 90.0
@export var autoReturnPitch:float = 30.0
@export_category("Collision") @export_category("Collision")
@export_flags_3d_physics var collisionMask:int = 1 @export_flags_3d_physics var collisionMask:int = 1
@export var distanceReturnSpeed:float = 5.0
var _yaw:float = 0.0 var _yaw:float = 0.0
var _pitch:float = 30.0 var _pitch:float = 30.0
var _currentDistance:float = 0.0
var _camVelocity:Vector2 = Vector2.ZERO
var _idleTimer:float = 0.0
var _mouseDelta:Vector2 = Vector2.ZERO
var _rightMouseHeld:bool = false
func _ready() -> void:
_currentDistance = distance
func _input(event:InputEvent) -> void:
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_RIGHT:
_rightMouseHeld = event.pressed
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED if _rightMouseHeld else Input.MOUSE_MODE_VISIBLE
elif event is InputEventMouseMotion and _rightMouseHeld:
_mouseDelta += event.relative
func _process(delta:float) -> void: func _process(delta:float) -> void:
if targetNode == null: if targetNode == null:
return return
var xMult:float = -1.0 if SETTINGS.invertCameraX else 1.0
var yMult:float = 1.0 if SETTINGS.invertCameraY else -1.0
var orbitInput:Vector2 = Input.get_vector( var orbitInput:Vector2 = Input.get_vector(
"camera_orbit_left", "camera_orbit_right", "camera_orbit_left", "camera_orbit_right",
"camera_orbit_up", "camera_orbit_down" "camera_orbit_up", "camera_orbit_down"
) )
var xMult:float = -1.0 if SETTINGS.invertCameraX else 1.0
var yMult:float = 1.0 if SETTINGS.invertCameraY else -1.0 # Controller input with speed ramp; instant stop on release
_yaw += orbitInput.x * orbitSensitivity * delta * xMult var controllerActive:bool = orbitInput.length() > 0.01
_pitch += orbitInput.y * orbitSensitivity * delta * yMult if controllerActive:
_camVelocity = _camVelocity.move_toward(
orbitInput * orbitSensitivity * SETTINGS.cameraSpeedController, orbitAcceleration * delta
)
_yaw += _camVelocity.x * delta * xMult
_pitch += _camVelocity.y * delta * yMult
else:
_camVelocity = Vector2.ZERO
# Right-click mouse drag
var mouseActive:bool = _mouseDelta.length_squared() > 0.0
if mouseActive:
_yaw += _mouseDelta.x * mouseSensitivity * SETTINGS.cameraSpeedMouse * xMult
_pitch += _mouseDelta.y * mouseSensitivity * SETTINGS.cameraSpeedMouse * yMult
_mouseDelta = Vector2.ZERO
# Auto-center behind player: only when player is moving and camera has been idle
var centerPressed:bool = Input.is_action_just_pressed("center_camera")
if centerPressed:
_idleTimer = autoReturnDelay
elif controllerActive or mouseActive:
_idleTimer = 0.0
else:
var playerBody := targetNode as CharacterBody3D
if playerBody != null and playerBody.velocity.length_squared() > 0.1:
_idleTimer += delta
else:
_idleTimer = 0.0
if _idleTimer >= autoReturnDelay:
_pitch = move_toward(_pitch, autoReturnPitch, autoReturnSpeed * delta)
var behindYaw:float = rad_to_deg(atan2(
targetNode.global_transform.basis.z.x,
targetNode.global_transform.basis.z.z
))
var yawDiff:float = fposmod(behindYaw - _yaw + 180.0, 360.0) - 180.0
_yaw += move_toward(0.0, yawDiff, autoReturnSpeed * delta)
_pitch = clamp(_pitch, pitchMin, pitchMax) _pitch = clamp(_pitch, pitchMin, pitchMax)
var pivot:Vector3 = targetNode.global_transform.origin + pivotOffset var pivot:Vector3 = targetNode.global_transform.origin + pivotOffset
@@ -42,26 +107,25 @@ func _process(delta:float) -> void:
cos(yawRad) * cos(pitchRad) cos(yawRad) * cos(pitchRad)
) )
var desired:Vector3 = pivot + dir * distance # Snap in instantly when terrain blocks; restore smoothly when clear
var actual:Vector3 = _resolveCollision(pivot, desired) var collisionDist:float = _resolveDistance(pivot, dir)
if collisionDist < _currentDistance:
_currentDistance = collisionDist
else:
_currentDistance = move_toward(_currentDistance, distance, distanceReturnSpeed * delta)
global_transform.origin = actual global_transform.origin = pivot + dir * _currentDistance
look_at(pivot, Vector3.UP) look_at(pivot, Vector3.UP)
# Cast a ray from the pivot to the desired camera position. # Ray from pivot outward; returns the safe camera distance along dir.
# If terrain blocks the shot, pull the camera in to just in front of the hit. func _resolveDistance(pivot:Vector3, dir:Vector3) -> float:
func _resolveCollision(pivot:Vector3, desired:Vector3) -> Vector3:
var space:PhysicsDirectSpaceState3D = get_world_3d().direct_space_state var space:PhysicsDirectSpaceState3D = get_world_3d().direct_space_state
var query:PhysicsRayQueryParameters3D = PhysicsRayQueryParameters3D.create(pivot, desired) var query:PhysicsRayQueryParameters3D = PhysicsRayQueryParameters3D.create(pivot, pivot + dir * distance)
query.collision_mask = collisionMask query.collision_mask = collisionMask
var hit:Dictionary = space.intersect_ray(query) var hit:Dictionary = space.intersect_ray(query)
if hit.is_empty(): if hit.is_empty():
return desired return distance
var hitDir:Vector3 = (desired - pivot).normalized() var hitDist:float = (hit["position"] - pivot).length() - COLLISION_MARGIN
var pulled:Vector3 = hit["position"] - hitDir * COLLISION_MARGIN return maxf(hitDist, minDistance)
var pulledDist:float = (pulled - pivot).length()
if pulledDist < minDistance:
return pivot + hitDir * minDistance
return pulled
+6
View File
@@ -158,6 +158,12 @@ tab_next={
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":10,"pressure":0.0,"pressed":false,"script":null) , Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":10,"pressure":0.0,"pressed":false,"script":null)
] ]
} }
center_camera={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":71,"key_label":0,"unicode":103,"location":0,"echo":false,"script":null)
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":9,"pressure":0.0,"pressed":false,"script":null)
]
}
ui_accept={ ui_accept={
"deadzone": 0.5, "deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194309,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194309,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
+2
View File
@@ -2,3 +2,5 @@ extends Node
var invertCameraX:bool = false var invertCameraX:bool = false
var invertCameraY:bool = false var invertCameraY:bool = false
var cameraSpeedController:float = 1.0
var cameraSpeedMouse:float = 1.0
+6
View File
@@ -4,6 +4,8 @@ class_name SettingsMenu extends Control
@export var tabControls:Array[Control] @export var tabControls:Array[Control]
@export var checkInvertX:CheckBox @export var checkInvertX:CheckBox
@export var checkInvertY:CheckBox @export var checkInvertY:CheckBox
@export var sliderControllerSpeed:HSlider
@export var sliderMouseSpeed:HSlider
func _ready() -> void: func _ready() -> void:
tabs.tab_changed.connect(onTabChanged) tabs.tab_changed.connect(onTabChanged)
@@ -11,6 +13,10 @@ func _ready() -> void:
checkInvertY.button_pressed = SETTINGS.invertCameraY checkInvertY.button_pressed = SETTINGS.invertCameraY
checkInvertX.toggled.connect(func(v:bool): SETTINGS.invertCameraX = v) checkInvertX.toggled.connect(func(v:bool): SETTINGS.invertCameraX = v)
checkInvertY.toggled.connect(func(v:bool): SETTINGS.invertCameraY = v) checkInvertY.toggled.connect(func(v:bool): SETTINGS.invertCameraY = v)
sliderControllerSpeed.value = SETTINGS.cameraSpeedController
sliderMouseSpeed.value = SETTINGS.cameraSpeedMouse
sliderControllerSpeed.value_changed.connect(func(v:float): SETTINGS.cameraSpeedController = v)
sliderMouseSpeed.value_changed.connect(func(v:float): SETTINGS.cameraSpeedMouse = v)
onTabChanged(tabs.current_tab) onTabChanged(tabs.current_tab)
func _notification(what:int) -> void: func _notification(what:int) -> void:
+31 -1
View File
@@ -2,7 +2,7 @@
[ext_resource type="Script" uid="uid://efmr0xkbw1py" path="res://ui/settings/SettingsMenu.gd" id="1_4lnig"] [ext_resource type="Script" uid="uid://efmr0xkbw1py" path="res://ui/settings/SettingsMenu.gd" id="1_4lnig"]
[node name="SettingsMenu" type="Control" node_paths=PackedStringArray("tabs", "tabControls", "checkInvertX", "checkInvertY")] [node name="SettingsMenu" type="Control" node_paths=PackedStringArray("tabs", "tabControls", "checkInvertX", "checkInvertY", "sliderControllerSpeed", "sliderMouseSpeed")]
layout_mode = 3 layout_mode = 3
anchors_preset = 15 anchors_preset = 15
anchor_right = 1.0 anchor_right = 1.0
@@ -14,6 +14,8 @@ tabs = NodePath("VBoxContainer/TabBar")
tabControls = [NodePath("VBoxContainer/ScrollContainer/LabelGameplay"), NodePath("VBoxContainer/ScrollContainer/LabelSound"), NodePath("VBoxContainer/ScrollContainer/LabelGraphics"), NodePath("VBoxContainer/ScrollContainer/PanelControls")] tabControls = [NodePath("VBoxContainer/ScrollContainer/LabelGameplay"), NodePath("VBoxContainer/ScrollContainer/LabelSound"), NodePath("VBoxContainer/ScrollContainer/LabelGraphics"), NodePath("VBoxContainer/ScrollContainer/PanelControls")]
checkInvertX = NodePath("VBoxContainer/ScrollContainer/PanelControls/CheckInvertX") checkInvertX = NodePath("VBoxContainer/ScrollContainer/PanelControls/CheckInvertX")
checkInvertY = NodePath("VBoxContainer/ScrollContainer/PanelControls/CheckInvertY") checkInvertY = NodePath("VBoxContainer/ScrollContainer/PanelControls/CheckInvertY")
sliderControllerSpeed = NodePath("VBoxContainer/ScrollContainer/PanelControls/SliderControllerSpeed")
sliderMouseSpeed = NodePath("VBoxContainer/ScrollContainer/PanelControls/SliderMouseSpeed")
[node name="VBoxContainer" type="VBoxContainer" parent="."] [node name="VBoxContainer" type="VBoxContainer" parent="."]
layout_mode = 1 layout_mode = 1
@@ -68,3 +70,31 @@ text = "Invert Camera X"
layout_mode = 2 layout_mode = 2
focus_mode = 2 focus_mode = 2
text = "Invert Camera Y" text = "Invert Camera Y"
[node name="LabelCameraSpeed" type="Label" parent="VBoxContainer/ScrollContainer/PanelControls"]
layout_mode = 2
text = "Camera Speed"
[node name="LabelControllerSpeed" type="Label" parent="VBoxContainer/ScrollContainer/PanelControls"]
layout_mode = 2
text = "Controller"
[node name="SliderControllerSpeed" type="HSlider" parent="VBoxContainer/ScrollContainer/PanelControls"]
layout_mode = 2
focus_mode = 2
min_value = 0.25
max_value = 2.0
step = 0.25
value = 1.0
[node name="LabelMouseSpeed" type="Label" parent="VBoxContainer/ScrollContainer/PanelControls"]
layout_mode = 2
text = "Mouse"
[node name="SliderMouseSpeed" type="HSlider" parent="VBoxContainer/ScrollContainer/PanelControls"]
layout_mode = 2
focus_mode = 2
min_value = 0.25
max_value = 2.0
step = 0.25
value = 1.0