xyz x, y, z - you know those
x, y, z, w - when dealing with something 4D.
xyz x, y, z - you know those
x, y, z, w - when dealing with something 4D.
Ok. Here's the implementation that uses a convex collider for frustum. It needs some setup but it's not much. We use convenient ReferenceRect
node for the selection rectangle. I kept the default node names for clarity. Rest of it is self evident I hope:
And here's the script attached to the area node. I commented it heavily:
extends Area3D
@export var camera: Camera3D
const near_far_margin = .1 # frustum near/far planes distance from camera near/far planes
# mouse dragging position
var mouse_down_pos: Vector2
var mouse_current_pos: Vector2
func _ready():
# initial reference rect setup
$ReferenceRect.editor_only = false
$ReferenceRect.visible = false
func _input(event):
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT:
if event.is_pressed():
# initialize the rect when mouse is pressed
mouse_down_pos = event.position
mouse_current_pos = event.position
$ReferenceRect.position = event.position
$ReferenceRect.size = Vector2.ZERO
$ReferenceRect.visible = true
else:
$ReferenceRect.visible = false
# make a scelection when mouse is released
select()
if event is InputEventMouseMotion and event.button_mask & MOUSE_BUTTON_MASK_LEFT:
# set rect size when mouse is dragged
mouse_current_pos = event.position
$ReferenceRect.position.x = min(mouse_current_pos.x, mouse_down_pos.x)
$ReferenceRect.position.y = min(mouse_current_pos.y, mouse_down_pos.y)
$ReferenceRect.size = (event.position - mouse_down_pos).abs()
func select():
# get frustum mesh and assign it as a collider and assignit to the area 3d
$ReferenceRect.size.x = max(1, $ReferenceRect.size.x)
$ReferenceRect.size.y = max(1, $ReferenceRect.size.y)
$CollisionShape3D.shape = make_frustum_collision_mesh(Rect2($ReferenceRect.position, $ReferenceRect.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
var selection = get_overlapping_areas()
print("SELECTION: ", selection)
# YOUR CODE THAT DECIDES WHAT TO DO WITH THE SELECTION GOES HERE
# function that construct 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 rext 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 planes, 2 triangles per plane, 3 vertices per triangle
shape.points = PackedVector3Array([
# near plane
pnear[0], pnear[1], pnear[2],
pnear[1], pnear[2], pnear[3],
# far plane
pfar[2], pfar[1], pfar[0],
pfar[2], pfar[0], pfar[3],
#top plane
pnear[0], pfar[0], pfar[1],
pnear[0], pfar[1], pnear[1],
#bottom plane
pfar[2], pfar[3], pnear[3],
pfar[2], pnear[3], pnear[2],
#left plane
pnear[0], pnear[3], pfar[3],
pnear[0], pfar[3], pfar[0],
#right plane
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 given camera's perspective projection settings
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
Any Area3D node in the scene that intersects rect/frustum will be reported. So just assign the camera to our Area3D, put some colliders into the scene and you're good to go. I don't know if this can get any simpler.
The script now uses Godot's collision system to test for intersections between frustum and area colliders. The good thing is it can handle any collision shape. There are couple of drawbacks:
I think my job in this thread is more than done .
A bit off topic.
@Lethn, you'll hate me for this but someone has to tell you. And I'll be that guy . I looked at your code linked earlier and... well... you won't get far with your project doing things like this. Excessively long overexplaining names that repeat on and on aside, you treat you code merely as unstructured data storage. In coding we aim to automate and avoid repetition as much as possible. Otherwise you end up typing same things over and over. This is precisely what will happen to your project if you don't start approaching it from automation point of view. Let me give you an example. In your system you have several villager classes. In each class you define same properties. This is excessive by itself as you should use class inheritance to avoid code repetition. But that's only the start of problems. So in every villager class, amongst other things, you have:
var isWondering = false
var isWalking = false
var isChillingOut = false
var isIdle = false
var isHoused = false
var isSleeping = false
var hasNothingToDo = false
var isWorshipping = false
var isGoingToVillageStore = false
var isGoingToWorshipSite = false
var goingForWood = false
var goingForGrain = false
var isNavMeshPathFinished = false
Now since those are mutually exclusive, you need to have a method for each state change looking like this (which is how you handle state all over your code):
setVilagerActivityToWalking():
isWondering = false
isWalking = true
isChillingOut = false
isIdle = false
isHoused = false
isSleeping = false
hasNothingToDo = false
isWorshipping = false
isGoingToVillageStore = false
isGoingToWorshipSite = false
goingForWood = false
goingForGrain = false
isNavMeshPathFinished = false
This is horrible as you'll need hundreds of lines of code just to switch states around. And whenever you choose to add a new state to the system, you'll need to intervene in all those functions, and add new such functions on top of it, in all of your villager classes. It saddens me to say it but this is an antithesis of good code.
You need to structure your data in a way that minimizes repetition and total amount of code: Hundreds of lines of the above code can be replaced with several lines. It doesn't only make it short but also scalable as adding a new state would require minimal intervention:
enum Activity {IDLE, WONDERING, WALKING, CHILLING, HOUSED, SLEEPING, WORSHIPPING, GOING_TO }
enum ActivityTarget {STORE, WOODS, WORSHIP_SITE, GRAIN, NONE}
var activity: Activity = Activity.IDLE
var activity_target: ActivityTarget
func set_activity(new_activity: Activity, new_target: ActivityTarget = ActivityTarget.NONE) -> void:
activity = new_activity
activity_target = new_target
xyz Believe it or not Xyz I'm not bothered at all LOL and this is something I was chatting about awhile back but I'd go too off topic here as well I had made a topic on this and one of the reasons I was looking at selection boxes was because I was taking a break from my By the gods project to have a look at stuff like enums and dictionaries which I don't know enough about. The examples I often see are far too basic for the code that I'm using so I need to poke around the internet a ton and maybe practice writing stuff up on a stand alone project. It's something I had already been discussing with people generally about getting things way more efficient precisely because of the reasons you've posted.
At the very least, I'd still write a damn glossary on my github because not enough people do that and I want my games to be as accessible to modders as possible using the Godot engine itself because as we know mods drastically increase the lifespan of a game. I'm resting my brain at the moment but I will take a look at your other post in full, it already looks a hell of a lot better just because you commented everything like mad, I realise it might come across as silly and unnecessary but I'm coming at this from the perspective of a learner, you should definitely contribute that code to the Godot documentation because it will help a lot of people. Either this will work really well with RTS style games or party RPGs and so on, but really any kind of situation where you need a proper selection box too, it will also mean you won't have go through the tedium of dealing with extremely confused noobs constantly and they can just look this up easier.
God this was such a rabbit hole I wasn't prepared for I should have paid more attention to peoples' warnings but I've always been a lunatic AI is easy compared to this. The only time I had this much of a headache was when I was dealing with the influence ring maths in Unity and that ended up involving several people including an ex-industry dev lol. One thing I will point out, it is a fine balance for sure between making code easier to read and keeping it as efficient as possible and that's something I'm learning a lot about these days beyond just bashing stuff out and getting it functional. You'll probably see me re-write a lot of that By the gods code by the time I'm done with all of this once I figure out the intricacies of everything without breaking stuff so don't worry about that code it's early days yet, I've been fleshing everything out and getting it functional. I was concerning myself quite a bit with things like execution timing more because that was indeed breaking stuff like my day/night cycle code.
Thank you for all this detail of course @xyz because it has really helped even if going at things bit by bit is admittedly frustrating and I'm sure it can be frustrating having to write things out, I hope this gets added to the documentation in some way because it is definitely needed for Godot 4, looking at you Megalomaniak
Edit: In fact, what might be a good idea for me would be to make another topic on using enums and dictionaries and we can generally discuss good options for coding practices with specific gaming code because to be honest that's another aspect of coding that Godot is either lacking or has a bit of out of date information on. I will make sure to double check though because I might have made an old thread on it awhile back, could all be worth a revisit.
There's some very good reasons that I've been exploring 3D selection boxes like this and being absolutely mad about not giving up, RTS' are a great way to stress test engines because you can have lots of agents doing stuff all at once and if you really want you can up the graphical detail to be ridiculous too. I've been working on an RTS idea for a bit of a wave defence game to satisfy a long standing craving I've had because the big studio devs especially don't bother but also this is giving me all sorts of ideas for doing things like formation based RTS' with lots and lots of agents. I'll keep it relatively simple first though and go with a classic RTS experience then work my way up. The rest of the stuff I can do myself.
I recommend the moderators have a look at @xyz 's post and put it in as a tutorial for the documentation because selection boxes seem to be a long neglected game feature that Godot could take over on quite a bit just by having up to date how to's on it.
Was double checking I wasn't just being daft, there's a bug that points towards your array, code works perfectly when dragging top left to bottom right but then a warning pops up when dragging bottom right to top left, the code is working great otherwise and yes once I get through the comments it is in fact easy to edit up.
make_frustum_collision_mesh(): Too few vertices per face.
Ah! Misinterpreted this warning I think I'm getting it now thanks to the comments, the bottom right to top left dragging is a separate issue and seems to be borked somehow, the too few vertices per faces I think has a possible fix because I've seen this problem before with the 2D selection boxes and the answer seemed to be to do an if statement and check how many pixels there are and run a standard mouse click if the size is too small.
Lethn It's not an error, it's a warning. It can be eliminated by doing boundary check on rect size so it's never (0,0):
$ReferenceRect.size.x = max(1, $ReferenceRect.size.x)
$ReferenceRect.size.y = max(1, $ReferenceRect.size.y)
I left it out for brevity as it's irrelevant for the main concept demonstrated in the code. You can tune rect behavior to your liking.
Did I mix up something in the other issue? I don't know if that's due to the camera angle or what, will triple check things to make sure.
Edit: Nope, doesn't seem to be anything to do with the setup I don't think.
Dragging from bottom right to top left doesn't seem to work for some reason, works super accurate the other way around.
Lethn That's because it's not implemented . It's not relevant for the main concept of frustum check. Rect handling is a separate thing. I just included minimal rect functionality that's enough to show that frustum check is working, simply not to bloat the example code.
Here's that programmer speak again: it's trivial to add. If you want me to include that too - you'll have to buy me a beer
There you go. I edited it in. The example code above now handles reverse rects too.
LOL it's annoying you did that because for once I was halfway through fixing it because I had realised that this is the same sort of code as using a 2D selection box because you've done all the maths previously and like you pointed out everything back end works with the 2D rect, just those extra two lines I didn't know about, thanks. You may find this interesting what I've done because I've already tweaked things to my liking for what I want to do with my RTS and it will give more of a 'feature complete' example for people to work from.
But I'm sure as hell going to quadruple check everything now.
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
func _ready():
# initial reference rect setup
RefRect.editor_only = false
RefRect.visible = false
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:
selected.DisableSelectionRing()
RefRect.visible = false
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:
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:
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
Okay, this is my offering, very minor tweaking of the code, this is just to show how to use it with bodies and put them into an array so there will be no confusion. I might be inclined to tweak the behaviour some more to increase accuracy and I've got some ideas already on how to do it but I'd call this finished. Will need to experiment and get things working how I like, maybe add some extra single click functionality, will update this code when I work out a fix for myself, it just looks like a basic execution order situation.
I think we can finally call this thread finished hurray! Now to satisfy my RTS craving Hopefully as well this will encourage many more devs to try and take up these kinds of games.
xyz You need to wait for 2 physics frames (when triggered from the event callback) for newly replaced frustum collider to become active. If anyone knows a way to force it to be immediate - let us know.
I was wondering about that. There is force_raycast_update() but apparently nothing for Areas yet; it's been requested.
https://github.com/godotengine/godot-proposals/issues/3111
We'll just have to try it and see if the 2-frame delay is acceptable for UI. Probably as long as the game is running at 60+ FPS.
synthnostate We'll just have to try it and see if the 2-frame delay is acceptable for UI. Probably as long as the game is running at 60+ FPS.
It's perfectly acceptable, just doesn't look very elegant
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.