RTS Style Selection (select units one by one and with a rectangle) in 2d

:information_source: Attention Topic was automatically imported from the old Question2Answer platform.
:bust_in_silhouette: Asked By SabrinaAsimov

Hello to everyone ! I I was trying to replicate RTS Style selection of units. For example, in Age of Empires you can select units by clicking on them one by one or with a selection rectangle, clicking and dragging with mouse. I learnt about selection with a rectangle in Youtube tutorials:

Kids can Code Tutorial

LegionGames Tutorial

Both tutorials are amazing but if I try to implement “one by one” selection, it doesnt work, when I click on a unit, it never deselects…
I´ve tried using signals and _imput_event but in both cases, the outcome is the same.

I realize that it’s the first time that I have a project with multiple objects requiring input, so any suggestios are welcome

Thank you very much in advance !

Just in case, here’s my code:

Main Scene is a Node2D

 extends Node2D

var dragging:bool = false
var selected_units:Array =[]
#Start of selection rectangle
var drag_start:Vector2

#RectangleShape2D is a built in type of rectangle that can detect collisions
var selection_rectangle:RectangleShape2D = RectangleShape2D.new()

# Reference to Node that will draw rectangular selection
onready var selection_draw:Node2D = $DrawSelection


# Called when the node enters the scene tree for the first time.
func _ready():
	pass # Replace with function body.

# WITH THIS APROACH I CANT SELECT UNITS ONE BY ONE AND WITH RECTANGLE. IF I USE INPUT EVENT HERE IN MAIN, THE FUNCTION DOESN'T
# WORK, IF I USE _UNHANDLED_INPUT IN UNITS, SOMETHING GOES WRONG

func _unhandled_input(event):
	if event is InputEventMouseButton and event.button_index == BUTTON_LEFT:
		# When the mouse button is pressed, then the dragging starts
		if event.pressed == true:
			# In case there are selected units, a new selection cancels the previous one
			for unit in selected_units:
				#unit.collider.deselect()
				unit.collider.toggle_selected(false)
			selected_units = []
			dragging = true
			drag_start = event.position
		# If I'm already dragging and release mouse button
		elif dragging == true:
			dragging = false
			var drag_end:Vector2 = event.position
			selection_draw.update_selection_rect(drag_start,event.position,dragging)
			drag_end = event.position
			# Godot Docs says that RectangleShape2D extents are half its size, so...
			selection_rectangle.extents = (drag_end - drag_start) / 2
			#Now collision check starts. Query space...
			var space = get_world_2d().direct_space_state
			var query = Physics2DShapeQueryParameters.new()
			#Because my Units are Area2d
			query.collide_with_areas = true
			#Assing the RectangleShape2D
			query.set_shape(selection_rectangle)
			#Position
			query.transform = Transform2D(0, (drag_end + drag_start)/2)
			#Selected units will be those intersected by the RectangleShape2D
			selected_units = space.intersect_shape(query)
			#print(selected_units)
			for unit in selected_units:
				unit.collider.toggle_selected(true)
	if dragging == true:
		if event is InputEventMouseMotion:
			selection_draw.update_selection_rect(drag_start,event.position,dragging)

This is the scrypt of Main’s child node that draws selection rectangle:

extends Node2D

var start:Vector2
var end:Vector2
var is_dragging:bool = false


func update_selection_rect(from:Vector2,to:Vector2,drag:bool):
	start = from
	end = to
	is_dragging = drag
	update()


# Called when the node enters the scene tree for the first time.
func _ready():
	pass # Replace with function body.


func _draw():
	if is_dragging == true:
		draw_rect(Rect2(start,end - start),Color.blue,false)

This is the scrypt attached to Units

extends Area2D

class_name Unit

    # Units should be selected with left click (or with selection rectangle) and moved with right click
    
    var selected:bool = false
    
    onready var selection_icon:Sprite = $Selected
    onready var tween:Tween = $Tween
    
    
    func move(to:Vector2):
    	var destination:Vector2 = to
    	tween.interpolate_property(self,"position",self.position,destination,2.0,Tween.TRANS_LINEAR,Tween.EASE_IN_OUT)
    
    func toggle_selected(value:bool):
    	selected = value
    	selection_icon.visible = value
    	
    
    func _input_event(_viewport, event, _shape_idx):
    	if event is InputEventMouseButton and event.button_index == BUTTON_LEFT and event.pressed == true:
    		self.toggle_selected(not selected)
    		print(selected)
	
:bust_in_silhouette: Reply From: Wakatta

The following is not the only way to do this and is more a matter of personal preference.

On release of the mouse if selection_draw is not shown deselect all selected_unitsand add the unit under the mouse to be selected and add to the selected_units var then hide the selection_draw

if not event.pressed:
    if selection_draw.is_visible():
        #send raycast from mouse
        #if collision select unit
    selected_units.append(unit_under_mouse)
    unit_under_mouse.toggle_selected(true)
selection_draw.hide()

There are some oddities with your code but it’s not necessary for you to change them

  • There exists an InputEventMouseMotion class
  • The draw selection is always shown, should only be when dragging starts

First of all, thank you very much for taking the time to answer my question.
Even when I have a little experience with coding (I used GML long time ago ) I’m still a Godot Newbie. You told me “There are some oddities with your code” and I realized that the big problem/oddity was that I was trying to make units to handle input events and at the same time I was triying to make a Main node handle the same input events, so I re-writed my code to make only Main Node handle input, and voilà ! problem solved ! Thank you very much !

SabrinaAsimov | 2021-10-01 04:28

:bust_in_silhouette: Reply From: SabrinaAsimov

I want to help others in my same situation, so I’ll try to explain my new code before pasting it:

  1. Units don’t have _input_event() functions any more. They only have methods to mark them as selected, to move them and so on
  2. Only Main node listens for input in an _unhandled_input method

If you clicked in an empty space, you can drag and use a rectangular selection, but if your click was on a unit, you select/deselect it

  1. Drawing of the selection rectangle is made at the Main Node itself

Here’s is Main Node script:

extends Node2D

# IN THIS EXAMPLE I WILL TRY TO HANDLE ALL INPUT FROM MAIN, NOT IN EVERY UNIT...
# (UNITS WON'T HAVE _INPUT FUNCTIONS)

var dragging:bool = false

var selected_units:Array =[]
#Start of selection rectangle
var drag_start:Vector2

#var can_drag:bool = false

#RectangleShape2D is a built in type of rectangle that can detect collisions
var selection_rectangle:RectangleShape2D = RectangleShape2D.new()



# This function returns if there's a unit in mouse click position. Units are in collision layer 2
# This function mimics a little bit GML instance_position(x,y)
func check_if_something_at_position(target:Vector2):
	var space_state = get_world_2d().direct_space_state
	var result:Array = space_state.intersect_point(target,32,[self],2,false,true)
	#Result is an array of dictionaries, right?
	if result:
		return result
	
# This function handles Physics2DShapeQueryParameters and try to intercept 
# units under RectangleShape2D
func rectangular_selection(from:Vector2, to:Vector2):
	var intercepted_units
	selection_rectangle.extents = (to - from)/2
	var space = get_world_2d().direct_space_state
	var query = Physics2DShapeQueryParameters.new()
	#Because my Units are Area2d
	query.collide_with_areas = true
	#Assing the RectangleShape2D
	query.set_shape(selection_rectangle)
	#Position
	query.transform = Transform2D(0, (to + from)/2)
	#Selected units will be those intersected by the RectangleShape2D
	intercepted_units = space.intersect_shape(query)
	return intercepted_units
 
func _unhandled_input(event):
	if event is InputEventMouseButton:
		#Select / Deselect units with left mouse button
		if event.button_index == BUTTON_LEFT:
			if event.pressed == true:
				var result= check_if_something_at_position(event.position)
				#When there are no units, you can drag-select... ##############
				if result == null:
					print ("Nothing here !!!")
					#First Deselect current selected units
					for unit in selected_units:
						unit.collider.toggle_selection(false)
					selected_units = []
					# Drag starts here
					dragging = true
					drag_start = event.position
				else:
					#There's something here,let's select it or deselect it !!  ==========
					#Result is an array of dictionaries, #I used index 0 because it's a single unit selection
					#I used index 0 because it's a single unit selection
					var selected:bool = result[0].collider.selected
					result[0].collider.toggle_selection(not selected)
		# Left mouse button is released, stop dragging !!!
			elif dragging == true:
				dragging = false
				update()
				var drag_end = event.position
				# I'll refactor this because this function is huge, to hard to read ! Make query in helper function
				selected_units = rectangular_selection(drag_start,drag_end)
				for unit in selected_units:
					unit.collider.toggle_selection(true)	
				
			
	if event is InputEventMouseMotion and dragging == true:
		#Call update to draw selection rectangle properly
		update()


func _draw():
	#Draws selection rectangle only if dragging
	if dragging == true:
		draw_rect(Rect2(drag_start,get_global_mouse_position() - drag_start),Color.blue,false,1.5,false)