extends Camera3D class_name RTSCamera3D @export var dragging_enabled: bool = true @export var panning_enabled: bool = true @export var edge_panning_enabled: bool = true @export var rotating_enabled: bool = true @export var zooming_enabled: bool = true @export var dragging_intersect_plane_normal: Vector3 = Vector3.UP @export var dragging_intersect_plane_distance: float = 0.0 @export_range(0.0, 1000.0, 10.0) var pan_speed: float = 20.0 @export_range(0.0, 1000.0, 10.0) var edge_pan_speed: float = 20.0 @export_range(0, 1.0, 0.01) var edge_trigger_percentage: float = 0.005 @export var lock_onto_position: bool = false @export var lockon_position: Vector3 = Vector3.ZERO @export var rotate_min_angle: float = 15.0 @export var rotate_max_angle: float = 80.0 @export var min_camera_distance: float = 1.0 @export var max_camera_distance: float = 50.0 @export var bounding_box: Vector2 = Vector2(100.0, 100.0) @export var world_center: Vector2 = Vector2.ZERO var dragging: bool = false var drag_cam_position: Vector3 = Vector3.ZERO var drag_2d_position: Vector2 = Vector2.ZERO var rotating: bool = false var orbit_angles: Vector2 = Vector2.ZERO #(deg_to_rad(-60.0), 0.0) var temp_orbit_angles: Vector2 = Vector2.ZERO var orbit_distance: float = 20.0 var orbitPosition: Vector3 = Vector3.ZERO var rotate_3d_position: Vector3 = Vector3.ZERO var rotate_2d_position: Vector2 = Vector2.ZERO var rotate_2d_position_old: Vector2 = Vector2.ZERO var panning_move_vector: Vector3 = Vector3.ZERO var edge_panning_move_vector: Vector3 = Vector3.ZERO var zoom_3d_position: Vector3 = Vector3.ZERO var camera_distance: float = 0.0 var camera_distance_old: float = 0.0 var _camera_distance: float = 30.0 # TODO: Fix look_at() errors func _ready(): await get_tree().process_frame #set correct camera distance rotate_3d_position = get_3d_pos(get_viewport().size / 2.0) look_at(lockon_position) if lock_onto_position: rotate_3d_position = lockon_position camera_distance = rotate_3d_position.distance_to(global_position) camera_distance_old = camera_distance orbit_angles = Vector2(global_rotation.x, global_rotation.y) # This is to prevent edge_panning from panning when # the mouse never entered the viewport before var mouse_has_entered_once: bool = false func _set_zoom_level(value: float) -> void: _camera_distance = clamp(value, min_camera_distance, max_camera_distance) var tween = get_tree().create_tween() tween.tween_property( self, "camera_distance", _camera_distance, 0.15 ) func _unhandled_input(event: InputEvent) -> void: var pos_3d = get_3d_pos(get_viewport().get_mouse_position()) if pos_3d: var pos_3d_rounded = "%.2f, %.2f, %.2f" % [pos_3d.x, pos_3d.y, pos_3d.z] if event.is_action_pressed("drag_camera") and not rotating and not lock_onto_position: _camera_distance = camera_distance drag_2d_position = get_viewport().get_mouse_position() drag_cam_position = position dragging = true elif event.is_action_released("drag_camera"): camera_distance = _camera_distance _camera_distance = 0 dragging = false if event.is_action_pressed("rotate_camera") and not dragging: rotate_3d_position = get_3d_pos(get_viewport().size / 2.0) if lock_onto_position: look_at(lockon_position) rotate_3d_position = lockon_position rotate_2d_position = get_viewport().get_mouse_position() rotate_2d_position_old = get_viewport().get_mouse_position() temp_orbit_angles = orbit_angles rotating = true elif event.is_action_released("rotate_camera"): orbit_angles = temp_orbit_angles rotating = false if zooming_enabled: if event.is_action_pressed("zoom_camera_in") and not dragging: _set_zoom_level(camera_distance - 1) zoom_3d_position = get_3d_pos(get_viewport().size / 2.0) elif event.is_action_pressed("zoom_camera_out") and not dragging: _set_zoom_level(camera_distance + 1) zoom_3d_position = get_3d_pos(get_viewport().size / 2.0) func get_3d_pos(position2D: Vector2): return Plane(dragging_intersect_plane_normal, dragging_intersect_plane_distance).intersects_ray(project_ray_origin(position2D), project_ray_normal(position2D)) func _notification(what: int) -> void: match what: NOTIFICATION_WM_MOUSE_ENTER: mouse_has_entered_once = true func _process(delta: float) -> void: panning_move_vector = Vector3.ZERO edge_panning_move_vector = Vector3.ZERO var mouse_position: Vector2 = get_viewport().get_mouse_position() if panning_enabled and not lock_onto_position: var camera_move_vector = Vector2(Input.get_action_strength("camera_right") - Input.get_action_strength("camera_left"), Input.get_action_strength("camera_down") - Input.get_action_strength("camera_up")) panning_move_vector = Vector3(camera_move_vector.x, 0.0, camera_move_vector.y).rotated(Vector3.UP, rotation.y) if edge_panning_enabled and mouse_has_entered_once and not lock_onto_position: var percentage = mouse_position / Vector2(get_viewport().size) if percentage.x <= edge_trigger_percentage: edge_panning_move_vector = Vector3(-1, 0.0, 0.0).rotated(Vector3.UP, rotation.y) elif percentage.x >= 1.0 - edge_trigger_percentage: edge_panning_move_vector = Vector3(1, 0.0, 0.0).rotated(Vector3.UP, rotation.y) elif percentage.y <= edge_trigger_percentage: edge_panning_move_vector = Vector3(0.0, 0.0, -1).rotated(Vector3.UP, rotation.y) elif percentage.y >= 1.0 - edge_trigger_percentage: edge_panning_move_vector = Vector3(0.0, 0.0, 1).rotated(Vector3.UP, rotation.y) # TODO: When reaching the limits, reset the rotation diff # Currently: If you rotate up/down and go out of limits, you have to move back all the overshoot # to move the camera back again if rotating_enabled and rotating and not dragging: rotate_2d_position = get_viewport().get_mouse_position() var mouse_diff = rotate_2d_position - rotate_2d_position_old temp_orbit_angles = Vector2(orbit_angles.x - mouse_diff.y * 0.01, orbit_angles.y - mouse_diff.x * 0.01) temp_orbit_angles.x = clamp(temp_orbit_angles.x, -deg_to_rad(rotate_max_angle), -deg_to_rad(rotate_min_angle)) var lookRotation = Quaternion.from_euler(Vector3(temp_orbit_angles.x, temp_orbit_angles.y, 0.0)) var lookDirection = lookRotation * Vector3.FORWARD var lookPosition = rotate_3d_position - lookDirection * camera_distance look_at_from_position(lookPosition, rotate_3d_position) if not is_equal_approx(camera_distance, camera_distance_old) and not rotating: var lookRotation = Quaternion.from_euler(Vector3(orbit_angles.x, orbit_angles.y, 0.0)) var lookDirection = lookRotation * Vector3.FORWARD var lookPosition = rotate_3d_position - lookDirection * camera_distance look_at_from_position(lookPosition, rotate_3d_position) camera_distance_old = camera_distance # TODO: Always apply zooming and figure out how to reset initial drag position # Probably just "stop" and start a new drag when zoom is different? # Or always stop/start a new drag? if dragging_enabled and dragging and not rotating and not lock_onto_position: var new_3d_pos = get_3d_pos(mouse_position) if new_3d_pos: var drag_world_position = get_3d_pos(drag_2d_position) if drag_world_position: var mouse_diff = drag_world_position - new_3d_pos var new_cam_pos = drag_cam_position + mouse_diff position = new_cam_pos rotate_3d_position = get_3d_pos(get_viewport().size / 2.0) else: position += ((panning_move_vector * pan_speed) + (edge_panning_move_vector * edge_pan_speed)) * delta