## Scrolls the camera when the mouse cursor reaches the screen edge. ## Attach to a Camera2D node. Requires an OrthographicCamera2D or ViewportContainer ## to determine screen bounds at runtime. @tool extends Camera2D ## Speed at which the camera scrolls (pixels per second). @export var scroll_speed: float = 400.0 ## Distance from the edge (in pixels) that triggers scrolling. @export var edge_margin: float = 40.0 ## Whether to clamp the camera position to a specific region. @export var clamp_enabled: bool = false ## Minimum bounds when clamping (in world space). @export var clamp_min: Vector2 = Vector2.ZERO ## Maximum bounds when clamping (in world space). @export var clamp_max: Vector2 = Vector2.ZERO ## Zoom sensitivity per mouse wheel tick. @export var zoom_sensitivity: float = 0.1 ## Minimum zoom level. @export var zoom_min: float = 0.25 ## Maximum zoom level. @export var zoom_max: float = 3.0 var _screen_rect: Rect2 func _ready() -> void: _update_screen_rect() var viewport: Viewport = get_viewport() if viewport != null: viewport.size_changed.connect(_update_screen_rect) func _process(delta: float) -> void: if Engine.is_editor_hint(): queue_redraw() return if is_zero_approx(scroll_speed): return var mouse_pos: Vector2 = get_viewport().get_mouse_position() var scroll_dir: Vector2 = _get_scroll_direction(mouse_pos) if scroll_dir != Vector2.ZERO: position += scroll_dir * scroll_speed * delta if clamp_enabled: _clamp_position() func _get_scroll_direction(mouse_pos: Vector2) -> Vector2: var direction: Vector2 = Vector2.ZERO if mouse_pos.x < _screen_rect.position.x + edge_margin: direction.x -= 1.0 elif mouse_pos.x > _screen_rect.position.x + _screen_rect.size.x - edge_margin: direction.x += 1.0 if mouse_pos.y < _screen_rect.position.y + edge_margin: direction.y -= 1.0 elif mouse_pos.y > _screen_rect.position.y + _screen_rect.size.y - edge_margin: direction.y += 1.0 return direction.normalized() func _draw() -> void: if not Engine.is_editor_hint(): return if not clamp_enabled: return var rect_size: Vector2 = clamp_max - clamp_min draw_rect(Rect2(clamp_min, rect_size), Color(1, 0.4, 0, 0.35)) draw_rect(Rect2(clamp_min, rect_size), Color(1, 0.6, 0.2, 0.8), false, 2.0) func _clamp_position() -> void: position.x = clampf(position.x, clamp_min.x, clamp_max.x) position.y = clampf(position.y, clamp_min.y, clamp_max.y) func _unhandled_input(event: InputEvent) -> void: if Engine.is_editor_hint(): return if event is InputEventMouseButton: var mouse_event: InputEventMouseButton = event if mouse_event.button_index == MOUSE_BUTTON_WHEEL_UP: _zoom(zoom_sensitivity, mouse_event.position) elif mouse_event.button_index == MOUSE_BUTTON_WHEEL_DOWN: _zoom(-zoom_sensitivity, mouse_event.position) func _zoom(change: float, mouse_pos: Vector2) -> void: var new_zoom_value: float = clampf(zoom.x + change, zoom_min, zoom_max) if is_equal_approx(new_zoom_value, zoom.x): return var viewport_center: Vector2 = get_viewport_rect().size / 2.0 var offset: Vector2 = mouse_pos - viewport_center var world_point: Vector2 = position + offset / zoom zoom = Vector2(new_zoom_value, new_zoom_value) position = world_point - offset / zoom func _update_screen_rect() -> void: var viewport: Viewport = get_viewport() if viewport != null: _screen_rect = viewport.get_visible_rect()