Okay, since this seems to be one of the only comprehensive tutorials on the internet about RTS selection and it is sadly very out of date, like some of you guys I suspect, I came across this tutorial and couldn't get anywhere with it so I ended up working on the code to update it get it running for Godot 4. This is 100% confirmed working and I also threw in some deselect for you because honestly there is not enough documentation out there in general for selection boxes beyond 2D. You still need to setup groups and so on but this should all work. extends Node3D @onready var camera = $PlayerCamera @onready var selectionBox2D = $PlayerCamera/SelectionBox2D var startSelectionPosition = Vector2() var mouseClickCollider3DResult var raycastMouseClick3DResult var mousePosition var selection = [] var newSelection = [] var focusFireTargetCollider var sideBarMouseEntered = false @onready var mouseRaycastGroup = get_tree().get_nodes_in_group("MouseRaycast") @onready var mouseRaycast = mouseRaycastGroup[0] func _physics_process(delta): mousePosition = get_viewport().get_mouse_position()
if Input.is_action_just_pressed("LeftClick"): selectionBox2D.startSelectionPosition = mousePosition startSelectionPosition = mousePosition
for selected in selection: selected.DisableSelectionRing()
if Input.is_action_pressed("LeftClick"): selectionBox2D.mousePosition = mousePosition selectionBox2D.isVisible = true else: selectionBox2D.isVisible = false if Input.is_action_just_released("LeftClick"): SelectUnits()
if Input.is_action_just_pressed("Rightclick") && selection.size() != 0: for selected in selection: if selected.SR.visible == false: return else: RaycastMouseClick() selected.beanSoldier.speed = 600 selected.beanSoldier.beanNA.set_target_position(raycastMouseClick3DResult) selected.beanSoldier.hasStopped = false selected.beanSoldier.combatRange.combatTimer.stop() selected.beanSoldier.combatRange.hasCombatTimerStarted = false
RaycastFromMouse() focusFireTargetCollider = mouseClickCollider3DResult if focusFireTargetCollider.is_in_group("Enemy"): selected.beanSoldier.combatRange.focusFireTarget = mouseClickCollider3DResult selected.beanSoldier.beanNA.set_target_position(focusFireTargetCollider.global_transform.origin) print(selected.beanSoldier.combatRange.focusFireTarget.name) else: selected.beanSoldier.combatRange.focusFireTarget = null func SelectUnits(): newSelection = [] if mousePosition.distance_to(startSelectionPosition) < 16: var u = GetUnitUnderMouse() if u != null: newSelection.append(u) else: newSelection = GetUnitsInBox(startSelectionPosition, mousePosition) if newSelection.size() != 0: for selected in newSelection: selected.EnableSelectionRing() selection = newSelection
func GetUnitUnderMouse(): var result = mouseClickCollider3DResult if result != null && result.is_in_group("Selectable"): return result.collider func GetUnitsInBox(topLeft, bottomRight): if topLeft.x > bottomRight.x: var temp = topLeft.x topLeft.x = bottomRight.x bottomRight.x = temp if topLeft.y > bottomRight.y: var temp = topLeft.y topLeft.y = bottomRight.y bottomRight.y = temp var box = Rect2(topLeft, bottomRight - topLeft) selection = [] for selected in get_tree().get_nodes_in_group("Selectable"): if box.has_point(camera.unproject_position(selected.global_transform.origin)): selection.append(selected) return selection func RaycastMouseClick(): var spaceState = get_world_3d().direct_space_state 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(): return else: raycastMouseClick3DResult = raycastResult["position"]
func RaycastFromMouse(): var spaceState = get_world_3d().direct_space_state 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(): return else: mouseClickCollider3DResult = raycastResult["collider"]
func RaycastNodePosition(): 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 mouseRaycast.target_position = Vector3(raycastTarget) func _on_side_bar_control_mouse_entered(): sideBarMouseEntered = true func _on_side_bar_control_mouse_exited(): sideBarMouseEntered = false
Holy crap THANK YOU. I've spent so much time trying to figure out janky ways to do this, but I had no clue that unprojecting a 3d position into a 2d position on the screen was even possible. This is a 10/10 tutorial - very easy to understand and straight to the point.
Wow this is great, with you there is so much to learn, being honest with you, I have many videos pending on your channel, but I'm happy to see so much interesting content. Thanks for continuing to advance in this genre, you are doing amazing things
Yeah, even though it server a great example off rts basics, mainly camera and peojecting 2d mouse coordinates onto 3d world, there's so much more to cover. Minimap for example.
And then also here's the 2D Selection Box for you because a bit of the code has changed for that in Godot 4 too, well one function really but it's barely mentioned in documentation. extends Control var isVisible = false var mousePosition = Vector2() var startSelectionPosition = Vector2() var selectionBoxColor = Color(0, 1, 0) var selectionBoxLineWidth = 1 func _draw(): if isVisible and startSelectionPosition != mousePosition: draw_line(startSelectionPosition, Vector2(mousePosition.x, startSelectionPosition.y,), selectionBoxColor, selectionBoxLineWidth) draw_line(startSelectionPosition, Vector2(startSelectionPosition.x, mousePosition.y), selectionBoxColor, selectionBoxLineWidth) draw_line(mousePosition, Vector2(mousePosition.x, startSelectionPosition.y), selectionBoxColor, selectionBoxLineWidth) draw_line(mousePosition, Vector2(startSelectionPosition.x, mousePosition.y), selectionBoxColor, selectionBoxLineWidth)
This is pretty clean, literally easier to follow than some other tutorials despite the pacing being fast and the end result not insignificant by any means
Great Tut man . it answered a hand full of things I was wondering if Godot could do. I'll have to figure out flocking, avoidance, and steering as I will try to apply this to ships. a very clear example, nice and easy to follow along.
I found that this method only makes them look at the target position initially but not always a full rotation. This worked for me in the func move_to(target_pos) in the unit script: look_at(target_pos, Vector3(0,1,0)). If your unit is looking the opposite way just make it -target_pos. Thanks for pointing me in the right direction!
my selection rings kinda get clipped by the floor.... does this have to do with the 'agent height' in the nav mesh settings? Edit: The fix was to move higher all child nodes of Unit's kinematicBody root
Thats where 3d rts game ends for everyone I guess cause navigation in godot is awful and the only existing addon for dynamic navmesh changing is broken
Hey there Miz, I have a request. I would like to learn how to make a shitty generic 3d horror game using Godot. My math is trash, and I still can't make a simple game in Godot because I can't wrap my mind around the nodes blah blah system. Prior to this, I could make shitty 2d games in Game Maker without much trouble. Can you give me some advice? Maybe give me some things to work on? Thanks.
You could start with the 3d platformer tutorial I have and just move the camera in close to make it first person. Then I guess just check out unity tutorials on making horror games for ideas. Mechanically horror games are pretty simple, it's mostly about making atmosphere
Okay, since this seems to be one of the only comprehensive tutorials on the internet about RTS selection and it is sadly very out of date, like some of you guys I suspect, I came across this tutorial and couldn't get anywhere with it so I ended up working on the code to update it get it running for Godot 4. This is 100% confirmed working and I also threw in some deselect for you because honestly there is not enough documentation out there in general for selection boxes beyond 2D. You still need to setup groups and so on but this should all work.
extends Node3D
@onready var camera = $PlayerCamera
@onready var selectionBox2D = $PlayerCamera/SelectionBox2D
var startSelectionPosition = Vector2()
var mouseClickCollider3DResult
var raycastMouseClick3DResult
var mousePosition
var selection = []
var newSelection = []
var focusFireTargetCollider
var sideBarMouseEntered = false
@onready var mouseRaycastGroup = get_tree().get_nodes_in_group("MouseRaycast")
@onready var mouseRaycast = mouseRaycastGroup[0]
func _physics_process(delta):
mousePosition = get_viewport().get_mouse_position()
if Input.is_action_just_pressed("LeftClick"):
selectionBox2D.startSelectionPosition = mousePosition
startSelectionPosition = mousePosition
for selected in selection:
selected.DisableSelectionRing()
if Input.is_action_pressed("LeftClick"):
selectionBox2D.mousePosition = mousePosition
selectionBox2D.isVisible = true
else:
selectionBox2D.isVisible = false
if Input.is_action_just_released("LeftClick"):
SelectUnits()
if Input.is_action_just_pressed("Rightclick") && selection.size() != 0:
for selected in selection:
if selected.SR.visible == false:
return
else:
RaycastMouseClick()
selected.beanSoldier.speed = 600
selected.beanSoldier.beanNA.set_target_position(raycastMouseClick3DResult)
selected.beanSoldier.hasStopped = false
selected.beanSoldier.combatRange.combatTimer.stop()
selected.beanSoldier.combatRange.hasCombatTimerStarted = false
RaycastFromMouse()
focusFireTargetCollider = mouseClickCollider3DResult
if focusFireTargetCollider.is_in_group("Enemy"):
selected.beanSoldier.combatRange.focusFireTarget = mouseClickCollider3DResult
selected.beanSoldier.beanNA.set_target_position(focusFireTargetCollider.global_transform.origin)
print(selected.beanSoldier.combatRange.focusFireTarget.name)
else:
selected.beanSoldier.combatRange.focusFireTarget = null
func SelectUnits():
newSelection = []
if mousePosition.distance_to(startSelectionPosition) < 16:
var u = GetUnitUnderMouse()
if u != null:
newSelection.append(u)
else:
newSelection = GetUnitsInBox(startSelectionPosition, mousePosition)
if newSelection.size() != 0:
for selected in newSelection:
selected.EnableSelectionRing()
selection = newSelection
func GetUnitUnderMouse():
var result = mouseClickCollider3DResult
if result != null && result.is_in_group("Selectable"):
return result.collider
func GetUnitsInBox(topLeft, bottomRight):
if topLeft.x > bottomRight.x:
var temp = topLeft.x
topLeft.x = bottomRight.x
bottomRight.x = temp
if topLeft.y > bottomRight.y:
var temp = topLeft.y
topLeft.y = bottomRight.y
bottomRight.y = temp
var box = Rect2(topLeft, bottomRight - topLeft)
selection = []
for selected in get_tree().get_nodes_in_group("Selectable"):
if box.has_point(camera.unproject_position(selected.global_transform.origin)):
selection.append(selected)
return selection
func RaycastMouseClick():
var spaceState = get_world_3d().direct_space_state
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():
return
else:
raycastMouseClick3DResult = raycastResult["position"]
func RaycastFromMouse():
var spaceState = get_world_3d().direct_space_state
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():
return
else:
mouseClickCollider3DResult = raycastResult["collider"]
func RaycastNodePosition():
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
mouseRaycast.target_position = Vector3(raycastTarget)
func _on_side_bar_control_mouse_entered():
sideBarMouseEntered = true
func _on_side_bar_control_mouse_exited():
sideBarMouseEntered = false
Holy crap THANK YOU. I've spent so much time trying to figure out janky ways to do this, but I had no clue that unprojecting a 3d position into a 2d position on the screen was even possible. This is a 10/10 tutorial - very easy to understand and straight to the point.
Wow this is great, with you there is so much to learn, being honest with you, I have many videos pending on your channel, but I'm happy to see so much interesting content. Thanks for continuing to advance in this genre, you are doing amazing things
Wish you continued this series :[
Yeah, even though it server a great example off rts basics, mainly camera and peojecting 2d mouse coordinates onto 3d world, there's so much more to cover. Minimap for example.
Thanks! Hope you will continue this series! =)
And then also here's the 2D Selection Box for you because a bit of the code has changed for that in Godot 4 too, well one function really but it's barely mentioned in documentation.
extends Control
var isVisible = false
var mousePosition = Vector2()
var startSelectionPosition = Vector2()
var selectionBoxColor = Color(0, 1, 0)
var selectionBoxLineWidth = 1
func _draw():
if isVisible and startSelectionPosition != mousePosition:
draw_line(startSelectionPosition, Vector2(mousePosition.x, startSelectionPosition.y,), selectionBoxColor, selectionBoxLineWidth)
draw_line(startSelectionPosition, Vector2(startSelectionPosition.x, mousePosition.y), selectionBoxColor, selectionBoxLineWidth)
draw_line(mousePosition, Vector2(mousePosition.x, startSelectionPosition.y), selectionBoxColor, selectionBoxLineWidth)
draw_line(mousePosition, Vector2(startSelectionPosition.x, mousePosition.y), selectionBoxColor, selectionBoxLineWidth)
func _process(_delta):
queue_redraw()
This is pretty clean, literally easier to follow than some other tutorials despite the pacing being fast and the end result not insignificant by any means
Great Tut man . it answered a hand full of things I was wondering if Godot could do. I'll have to figure out flocking, avoidance, and steering as I will try to apply this to ships.
a very clear example, nice and easy to follow along.
Thanks for video! :) Would be great to fix the issue when they fight for the spot (bump in to each other), which you mentioned in previous video!
this is so cool, I love it! keep it up man!
Hey Miziziziz
How would you do to make your units look at the direction they are moving?
I found that this method only makes them look at the target position initially but not always a full rotation. This worked for me in the func move_to(target_pos) in the unit script:
look_at(target_pos, Vector3(0,1,0)). If your unit is looking the opposite way just make it -target_pos. Thanks for pointing me in the right direction!
pure genius!
my selection rings kinda get clipped by the floor.... does this have to do with the 'agent height' in the nav mesh settings?
Edit: The fix was to move higher all child nodes of Unit's kinematicBody root
As of Godot 4 you now have 3D decals which mean you can project selection rings onto the ground without any code.
Amazing video. Great content. Clear and concise.
Hope the part 3 will come someday :3
Part 3 probably contains dynamic obstacles and thats where you drop godot and go to unity
Thanks, Hello from Kazakstan
Please, continue tutor, it helps me more then school(
Why are they not looking towards direction
Can you please add avoidance and steering behavior tutorial next ? Thanks
hmm.. is there full rts game develop tutorial with godot? this was very interesting.
Good tutorials.
Why is my selecrion box not visible?
Is there coming part 3? :)
Thats where 3d rts game ends for everyone I guess cause navigation in godot is awful and the only existing addon for dynamic navmesh changing is broken
very good
Hey there Miz, I have a request.
I would like to learn how to make a shitty generic 3d horror game using Godot. My math is trash, and I still can't make a simple game in Godot because I can't wrap my mind around the nodes blah blah system. Prior to this, I could make shitty 2d games in Game Maker without much trouble.
Can you give me some advice? Maybe give me some things to work on?
Thanks.
You could start with the 3d platformer tutorial I have and just move the camera in close to make it first person. Then I guess just check out unity tutorials on making horror games for ideas. Mechanically horror games are pretty simple, it's mostly about making atmosphere
3rd part please!
sus
can you do same in c#?
I did it in C# if you want the code.