Okay, while this thread is done, just in case anybody comes poking around wanting to play with selection boxes like I have, this is some extra code I've been doing up for my own game but given it's standard click and move code and there's little up to date information on how to use Godot 4's I thought I'd throw this in as well. I also have a bit of a treat for people as I managed to figure out a way to deal with any pathfinding issues when it comes to buildings if you place them in your game.
For cleanest results with the selection box itself, recommend you keep all selectable units on their own individual layer, otherwise the engine will just blindly grab everything and put it into your array which isn't clean at all.
extends Area3D
@export var camera: Camera3D
const near_far_margin = .1 # frustum near/far planes distance from camera near/far planes
var selection = []
# RefRect = Reference Rectangle
# CS = Collision Shape 3D
@onready var RefRect = $ReferenceRect
@onready var CS = $CollisionShape3D
var mouse_down_pos
var mouse_current_pos
var mouseClickPosition3DResult = Vector3()
func _ready():
# initial reference rect setup
RefRect.editor_only = false
RefRect.visible = false
func _physics_process(delta):
if selection.size() >= 1:
if Input.is_action_just_pressed("rightclick"):
for select in selection:
RaycastPosition()
select.beanNA.set_target_position(mouseClickPosition3DResult)
select.speed = 600
func _input(event):
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT:
if event.is_pressed():
mouse_down_pos = event.position
mouse_current_pos = event.position
RefRect.size = Vector2.ZERO
RefRect.visible = true
elif has_overlapping_bodies():
for selected in selection:
if selected.is_in_group("Selectable"):
selected.DisableSelectionRing()
RefRect.visible = false
selection.clear()
select()
else:
RefRect.size.x = max(1, RefRect.size.x)
RefRect.size.y = max(1, RefRect.size.y)
RefRect.visible = false
# make a selection when mouse is released
select()
if event is InputEventMouseMotion and event.button_mask and MOUSE_BUTTON_MASK_LEFT and not Input.is_action_pressed("MiddleMouseButton"):
mouse_current_pos = event.position
RefRect.position.x = min(mouse_current_pos.x, mouse_down_pos.x)
RefRect.position.y = min(mouse_current_pos.y, mouse_down_pos.y)
RefRect.size = (mouse_current_pos - mouse_down_pos).abs()
func select():
# get frustum mesh and assign it as a collider and assignit to the area 3d
CS.shape = make_frustum_collision_mesh(Rect2(RefRect.position, RefRect.size))
# wait for collider asignment to take effect
await get_tree().physics_frame
await get_tree().physics_frame
# actually get areas that intersest the frustum
selection = get_overlapping_bodies()
for selected in selection:
if selected.is_in_group("Selectable"):
selected.EnableSelectionRing()
# YOUR CODE THAT DECIDES WHAT TO DO WITH THE SELECTION GOES HERE
# function that construct a frustum mesh collider
func make_frustum_collision_mesh(rect: Rect2) -> ConvexPolygonShape3D:
# create a convex polygon collision shape
var shape = ConvexPolygonShape3D.new()
# project 4 corners of the rect to the camera near plane
var pnear = project_rect(rect, camera, camera.near + near_far_margin)
# project 4 corners of the rect to the camera far plane
var pfar = project_rect(rect, camera, camera.far - near_far_margin)
# create a frustum mesh from 8 projected points (6 faces in total, with 2 triangles per face and 3 vertices per triangle)
shape.points = PackedVector3Array([
# near face
pnear[0], pnear[1], pnear[2],
pnear[1], pnear[2], pnear[3],
# far face
pfar[2], pfar[1], pfar[0],
pfar[2], pfar[0], pfar[3],
#top face
pnear[0], pfar[0], pfar[1],
pnear[0], pfar[1], pnear[1],
#bottom face
pfar[2], pfar[3], pnear[3],
pfar[2], pnear[3], pnear[2],
#left face
pnear[0], pnear[3], pfar[3],
pnear[0], pfar[3], pfar[0],
#right face
pnear[1], pfar[1], pfar[2],
pnear[1], pfar[2], pnear[2]
])
return shape
# Helper function that projects 4 rect corners into space, onto a viewing plane at z distance from the given camera
# Projection is done using camera's perspective projection
func project_rect(rect: Rect2, cam: Camera3D, z: float) -> PackedVector3Array:
var p = PackedVector3Array() # our projected points
p.resize(4)
p[0] = cam.project_position(rect.position, z)
p[1] = cam.project_position(rect.position + Vector2(rect.size.x, 0.0), z)
p[2] = cam.project_position(rect.position + Vector2(rect.size.x, rect.size.y), z)
p[3] = cam.project_position(rect.position + Vector2(0.0, rect.size.y), z)
return p
func RaycastPosition():
var spaceState = get_world_3d().direct_space_state
var mousePosition = get_viewport().get_mouse_position()
var raycastOrigin = camera.project_ray_origin(mousePosition)
var raycastTarget = raycastOrigin + camera.project_ray_normal(mousePosition) * 5000
var physicsRaycastQuery = PhysicsRayQueryParameters3D.create(raycastOrigin, raycastTarget)
var raycastResult = spaceState.intersect_ray(physicsRaycastQuery)
if raycastResult.is_empty():
var mouseClickPosition3D = raycastTarget
else:
mouseClickPosition3DResult = raycastResult["position"]
@xyz has provided us with a selection box, that's great! But wait, oh no, your pathfinding is constantly breaking when you finally get those awesome base building mechanics and your units won't calculate around obstacles? No problem, this might seem really weird, but if you place a transparent sphere mesh it will likely fix the majority of issues. I got the idea off Starcraft 2 when I was doing a playthrough to check out their mechanics and I noticed pretty much every structure of theirs followed this pattern, the Starcraft devs seem to have decided since they had a corner problem, they would simply get rid of the corners lol. Don't believe me? You can try playing the game yourself and checking, I'm 99% sure this is how they solved their pathfinding issues.


Hope this extra bit of information helps anyone, the sphere pretty much forces the navmesh to calculate a circular gap for the units. Given that Godot 4 now has things like runtime baking and no problem with navmesh linking getting everything else running has been a breeze so far with 3D. You should be able adapt these techniques to any kind of game where you've got point and click navigation going on and need to use a selection box generally. I've even been able to setup ramps and have had the units going up and down very smoothly.