extends Control class_name SelectionManager @export_category("Controls") @export var hover:Control @export var selecteddisplay:Control @export_category("Settings") @export var rect_style: StyleBoxFlat var draw_selection := false var drag_start: Vector2 var select_box: Rect2 var perform_selection := false # Flag to trigger selection in _physics_process var mouse_position:Vector2 # Storing mous eposition to use it on differents processes @onready var box_color: Color = ProjectSettings.get_setting("game/interface/selection_rectangle_color", Color("#00ff0066")) @onready var box_color_outline: Color = ProjectSettings.get_setting("game/interface/selection_rectangle_color_outline", Color("#00ff00")) @onready var optimized_rect: bool = ProjectSettings.get_setting("game/interface/optimized_rectangle", false) @onready var cam = get_viewport().get_camera_3d() @onready var debug_mode = get_tree().debug_collisions_hint #ProjectSettings.get_setting("debug/collisions/show_mouse_trace", false) func _ready() -> void: if !hover: hover = %HoverLabel func _unhandled_input(e: InputEvent) -> void: if e is InputEventMouseButton and e.button_index == MOUSE_BUTTON_LEFT: if e.pressed: draw_selection = true drag_start = e.position else: draw_selection = false if drag_start.is_equal_approx(e.position): # Single click: set a zero-sized selection box select_box = Rect2(e.position, Vector2.ZERO) print("one click") else: # Drag: calculate the final selection box var x_min = min(drag_start.x, e.position.x) var y_min = min(drag_start.y, e.position.y) select_box = Rect2(x_min, y_min, max(drag_start.x, e.position.x) - x_min, max(drag_start.y, e.position.y) - y_min) perform_selection = true queue_redraw() elif draw_selection and e is InputEventMouseMotion: # Update selection box for drawing during drag var x_min = min(drag_start.x, e.position.x) var y_min = min(drag_start.y, e.position.y) select_box = Rect2(x_min, y_min, max(drag_start.x, e.position.x) - x_min, max(drag_start.y, e.position.y) - y_min) queue_redraw() if !draw_selection and e is InputEventMouseMotion: mouse_position = e.position func _physics_process(_delta: float) -> void: if perform_selection: update_selected_units() perform_selection = false else: check_hover(mouse_position) func check_hover(mouse_pos:Vector2): # TODO: Hovering and display names var raycast = check_raycast(mouse_pos) if raycast.is_empty(): hover.text = "" elif raycast.collider: var hovered_entity = raycast.collider.get_parent() hover.text = hovered_entity.name var selected = get_selected() selecteddisplay.text = "" if selected.size() > 0: for s in selected: selecteddisplay.text += ","+s.name else: selecteddisplay.text = "" selecteddisplay.queue_redraw() ## Returns an array of entities selected by the player, using multiplayer-unique group names func get_selected() -> Array[Entity]: var selected: Array[Node] = get_tree().get_nodes_in_group('selected-by-' + str(get_tree().get_multiplayer().get_unique_id())) var selected_entity: Array[Entity] selected_entity.assign(selected) return selected_entity ## [param collision_mask] is 32768 by default, so the collision channel 16 func check_raycast(mouse_pos: Vector2, collision_mask: int = 32768) -> Dictionary: var from = cam.project_ray_origin(mouse_pos) var to = from + cam.project_ray_normal(mouse_pos) * 1000 #FIXME: Magic number var query = PhysicsRayQueryParameters3D.create(from,to,collision_mask) query.collide_with_areas = true query.collide_with_bodies = false var space_state = cam.get_world_3d().direct_space_state if space_state: return space_state.intersect_ray(query) return {} ## Deselects all currently selected entities func deselect_all() -> void: var selected = get_selected() for entity: Entity in selected: entity.deselect() ## Updates selection state of entities based on single-click (raycast) or drag (box) selection [br] ## Iterate on every entities inside the [param selectable-entity] group func update_selected_units() -> void: ## FIXME: Could be optimized if we store an array of visible entity by adding[br] ## an [Area3D] child of the camera OR changing the group based on if the 3d model is loaded var units_array = get_tree().get_nodes_in_group("selectable-entity") var raycast = {} # Only perform raycast if it's a single click (zero-sized select_box) if select_box.size == Vector2.ZERO: raycast = check_raycast(select_box.position) if raycast.is_empty(): deselect_all() return if raycast.collider: if debug_mode: DebugTools.DrawCube(raycast.position,0.02,3.0,Color()) # select entity under mouse # maybe can be extracted from hover instead? var entity = raycast.collider.get_parent() deselect_all() entity.select() return for entity: Entity in units_array: # TODO: Optimize by searching only in spawned chunks # select entities within the selection/draggin box if entity.is_in_selection(select_box, cam): entity.select() else: entity.deselect() ## Draws the selection rectangle during drag or selection, using optimized or styled rendering func _draw() -> void: if not (draw_selection or perform_selection):return if select_box and rect_style: if optimized_rect: draw_rect(select_box,box_color) draw_rect(select_box,box_color_outline,false,2.0,true) elif rect_style: draw_style_box(rect_style,select_box)