0 votes

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 imputevent 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)
Godot version 3.3.2
in Engine by (54 points)

2 Answers

+1 vote

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
by (4,560 points)

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 !

0 votes

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 inputevent() 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 unhandledinput 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

3) 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)
by (54 points)
Welcome to Godot Engine Q&A, where you can ask questions and receive answers from other members of the community.

Please make sure to read How to use this Q&A? before posting your first questions.
Social login is currently unavailable. If you've previously logged in with a Facebook or GitHub account, use the I forgot my password link in the login box to set a password for your account. If you still can't access your account, send an email to webmaster@godotengine.org with your username.