3D RTS Selection Box

Godot Version

4

Question

Hello, Im trying to make a RTS style selection box in my 3D project. I just cant figure out what’s not working out. It complains but I have Panel as a node to my scene… Anyone got a clue? Im sending the code as well, might be something wrong so far.

extends Camera3D

var start = Vector2()
var end = Vector2()
var isDragging = false

signal area_selected

func _ready():
	set_process_input(true)

func _input(event):
	if event is InputEventMouseButton:
		if event.button_index == MOUSE_BUTTON_LEFT:
			if event.pressed:
				# Start dragging
				isDragging = true
				start = event.position
				end = event.position
				$Panel.visible = true  # Make sure the panel is visible when dragging starts
			else:
				# Stop dragging
				isDragging = false
				end = event.position
				$Panel.visible = false  # Hide the panel when dragging stops
				draw_area(false)
				emit_signal("area_selected", start, end)  # Emit signal with the selected area coordinates
	
	elif event is InputEventMouseMotion:
		if isDragging:
			# Update the end position while dragging
			end = event.position
			draw_area()

func draw_area(s=true):
	var startVector = start
	var endVector = end

	var panel = $Panel
	panel.custom_minimum_size = Vector2(abs(startVector.x - endVector.x), abs(startVector.y - endVector.y))
	
	var pos = Vector2()
	pos.x = min(startVector.x, endVector.x)
	pos.y = min(startVector.y, endVector.y)
	panel.rect_position = pos

	# You can optionally scale the panel here if needed
	if s:
		panel.custom_minimum_size *= int(s)

func select_objects_in_box(start, end):
	# Implement your selection logic here
	pass

Screenshot 2024-05-22 120237

I suppose the script is attached to the node Camera3D, right? But Panel isn’t a child node of that node, so you cannot use the path $Panel. Something like $"../Panel" should work. Or drag the Panel node to your script to get the correct path.

4 Likes

Alternatively, you could use a scene unique name too - right click the panel, ‘Access as Unique Name’. Change the code to ‘%Panel’. It no longer matters where it is.

Now the selection box works fine! A raycast of the mouseposition and collision is also working out well.

Code in ColorRect (UI Scene):


extends ColorRect

var mouse_down : bool = false
var mouse_start_pos : Vector2
var mouse_end_pos : Vector2
var box_grid : int = 1
var nodes_in_rect : Array = []
var camera : Camera3D

func _ready():
	# Find the Camera3D node
	camera = get_tree().root.get_node("Game/Camera3D")
	assert(camera != null, "Camera3D node not found. Please check the path.")

func _input(event):
	if event is InputEventMouseButton:
		if event.button_index == MOUSE_BUTTON_LEFT and event.is_pressed():
			# Start the selection box
			if not mouse_down:
				nodes_in_rect.clear()
				mouse_down = true
				mouse_start_pos = event.global_position
				global_position = mouse_start_pos
				size = Vector2.ZERO  # Reset size initially
			
		elif event.button_index == MOUSE_BUTTON_LEFT and not event.is_pressed():
			# End the selection box
			if mouse_down:
				mouse_down = false
				mouse_end_pos = event.global_position
				update_selection()
				size = Vector2.ZERO  # Reset size after selection

	elif event is InputEventMouseMotion:
		if mouse_down:
			update_selection_box()

func update_selection_box():
	var mouse_pos = get_global_mouse_position()
	size = (mouse_pos - mouse_start_pos).abs()
	position = mouse_start_pos
	if mouse_pos.x < mouse_start_pos.x:
		position.x = mouse_pos.x
	if mouse_pos.y < mouse_start_pos.y:
		position.y = mouse_pos.y

func update_selection():
	# Find all nodes within the selection box
	var nodes_to_get = get_tree().get_nodes_in_group("Selectables")
	if nodes_to_get:
		for node in nodes_to_get:
			var node_global_pos = node.global_transform.origin
			var node_2d_pos = camera.unproject_position(node_global_pos)
			if get_global_rect().has_point(node_2d_pos):
				nodes_in_rect.append(node)
				print("Selected node: ", node.name)  # Add this line to print the name of the selected node


# Utility function for snapping to grid
func snapped(value, grid_size):
	return round(value / grid_size) * grid_size


Code in Camera3D (Game scene):

extends Camera3D

@onready var ray_cast_3d: RayCast3D = $RayCast3D

var is_panning = false
var last_mouse_position = Vector2()

# Adjust these values to control panning speed
var pan_speed = 0.1

func _unhandled_input(event):
	if event is InputEventMouseButton:
		if event.button_index == MOUSE_BUTTON_MIDDLE:
			is_panning = event.pressed
			if is_panning:
				last_mouse_position = event.position

	if event is InputEventMouseMotion and is_panning:
		var mouse_delta = event.position - last_mouse_position
		var move_vector = Vector3(mouse_delta.x, 0, mouse_delta.y) * pan_speed
		global_translate(move_vector)
		last_mouse_position = event.position

func _process(delta: float) -> void:
	var mouse_position : Vector2 = get_viewport().get_mouse_position()
	ray_cast_3d.target_position = project_local_ray_normal(mouse_position) * 100.0
	ray_cast_3d.force_raycast_update()
	print(ray_cast_3d.get_collider())
	print(ray_cast_3d.get_collision_point())
	

Screenshot 2024-05-24 182111