- Home /
RTS selection with frustum
So, pretty standard task for RTS game: select multiple units via mouse.
I use well-known way: call ScreenPointToRay
for starting and ending points, calculate rectangle with World coordinates and call Physics.OverlapBox
with given size and camera rotation.
The problem begins with selection for units that are closer to left/right edges, because Box is not the right figure. It must be some kind of frustum with not-parallel top and bottom planes (terrain is horizontal, camera rotated 45deg to terrain).
In other words, I need to make kind of 3D collider from camera selection rect to projected rect on terrain and than find object that collided with it.
I know there is a CalculateFrustumPlanes
for getting 6 planes, corresponds to camera viewport, but I need narrower figure (only selected part of camera viewport) and I haven’t found any method to find Overlaps within planes intersection.
Answer by Eno-Khaon · Dec 13, 2020 at 11:42 PM
This suggestion doesn't factor in Physics.OverlapBox(), and uses just the center point of each unit, but it should definitely be adaptable as needed.
Let's start by plotting out efficiency at the beginning. It may seem like a silly thing to do, but for a Real Time Strategy game, where you can expect most units to not be visible on screen at a time, this should be able to help significantly overall.
First, let's combine MonoBehaviour/Renderer.OnBecameVisible() with a handy custom script extension, Renderer.IsVisibleFrom(Camera).
void OnBecameVisible()
{
// Simplifying a few variables that are presumed to be kept track of
// if the player's main camera sees the unit
// and the player owns that unit...
if(renderer.IsVisibleFrom(playerCamera) && unit.owner == player)
{
// Add the unit to a List<Transform> of friendly/owned units currently in view
player.visibleUnits.Add(transform);
}
}
void OnBecameInvisible()
{
if(player.visibleUnits.Contains(transform))
{
player.visibleUnits.Remove(transform);
}
}
With this out of the way, there are now far fewer units to work with at a time when determining your selection bounds. From here, after you've created your selection box in your viewport, you can run through the on-screen unit list and convert the units' positions to viewport coordinates:
// When the mouse is released after creating the selection box...
if(selectionBoxFinalized)
{
for(int i = 0; i < player.visibleUnits.Count; i++)
{
// Screen point, viewport point, etc.
if(selectionBoxRect.Contains(playerCamera.WorldToViewportPoint(player.visibleUnits[i].position)))
{
AddToSelection(player.visibleUnits[i]);
}
}
}
Even if this doesn't suit your needs exactly, hopefully this can help get you pointed in a good direction.
Edit: Fixed typo
Thank you for your solution.
Thought of that way too. Will give it a try if my method won’t work.
What I’m trying to do - get two rects (similar to nearClipPlane and farClipPlane except the size is not whole viewport but selection rect) and then build a mesh with collider from their vertices.
I solved problem with back face of that mesh (it looks like trapezoid) and now I’m trying to get correct vertices from ScreenToWorld and mousePoint. It works when camera not rotated but when I apply orbit rotation (camera with offset in empty game object) - Vector3 becomes incorrect.
If you're defining your 3D selection box's corners using something akin to...
// msr = mouse selection rect, to save space on repetition
Vector3 topLeftNear = playerCamera.ScreenToWorldPoint(new Vector3(msr.x$$anonymous$$in, msr.x$$anonymous$$ax, selectionBoxNearDistance));
Vector3 topLeftFar = playerCamera.ScreenToWorldPoint(new Vector3(msr.x$$anonymous$$in, msr.x$$anonymous$$ax, selectionBoxFarDistance));
... then I'm honestly not sure why you would be seeing errant values from it.
What are you using as your logic to project the vertices from the camera's space?
I project rect to ground with ray casting and it works fine. I get errant values for top rect cause mousePosition only have x,y coords. So at any camera rotation you will get the same 2d coords and front rect always facing directly to 0 Y rotation.
So I need to rotate points from ScreenToWorld to the same angle as my camera in order to calculate right 3D position of that rect to face my camera at any current rotation.
Just adding multiplication for ScreenToWorld Vector3 with camera Quartenion doesn’t work. I’ll try to make code sample and show visually how it looks.
I know for sure that I don’t calculate correct coords for last two points of rect.
Answer by webstalk3r · Dec 16, 2020 at 02:04 AM
So, what I have now. Camera is inside CameraRig object
CameraRig - pos (0, 0, 0) rot (0, 0, 0)
===> Camera - pos (0, 20, -40) rot (45, 0, 0)
I visualized front face rect, as you can see if there is no rotation (or rotation by 180deg) - all ok
But when I rotate CameraRig (rotation Y) I get same orientation for that rect (even with correct position).
Here is some code
// selectionStartMouse == Input.mousePosition at start
// selectionEndMouse == Input.mousePosition now
var frontRect = GetScreenRect(selectionStartMouse, selectionEndMouse);
var frontBottomLeft = GetMouseToWorldPoint(new Vector3(frontRect.xMin, frontRect.yMin));
var frontTopRight = GetMouseToWorldPoint(new Vector3(frontRect.xMax, frontRect.yMax));
// I know that next two lines are incorrect, cause calculation of last 2 corners of rect will work that way only without rotation, but the main problem with first two from ScreenToWorldPoint
var frontTopLeft = frontTopRight.SetX(frontBottomLeft.x); // SetX == replace x coord
var frontBottomRight = frontBottomLeft.SetX(frontTopRight.x);
public static Rect GetScreenRect(Vector3 screenPosition1, Vector3 screenPosition2)
{
var topLeft = Vector3.Min(screenPosition1, screenPosition2);
var bottomRight = Vector3.Max(screenPosition1, screenPosition2);
return Rect.MinMaxRect(topLeft.x, topLeft.y, bottomRight.x, bottomRight.y);
}
public static Vector3 GetMouseToWorldPoint(Vector3 coords)
{
coords.z = cam.nearClipPlane + 0.5f; // move away a bit from viewer
return GetCamera().ScreenToWorldPoint(coord);
}
Based on the screenshots (and some personal testing), I can only suspect that your line:
public static Vector3 Get$$anonymous$$ouseToWorldPoint(Vector3 coords)
{
// vvv vvv
return GetCamera().ScreenToWorldPoint(coord);
// ^^^ ^^^
}
is the problem here. Are you certain you're referencing the correct camera? Your coordinates look fine in the first image, but it seems like it should only be possible to have the problem in the second if the wrong camera is referenced.
I used the following script for a quick test:
// Script moved to following reply, due to character limits
Using this script for testing in the editor, I can draw a simple box, then go to the scene view (i.e. another camera) and view the 3D box, oriented to match the rotation of the camera (when the box is changed).
using UnityEngine;
public class CameraBoxTest : $$anonymous$$onoBehaviour
{
Vector3 mouseDown = new Vector3(50f, 50f, 50f);
Vector3 mouseUp = new Vector3(250f, 250f, 250f);
Camera cam;
public float $$anonymous$$Depth = 1.0f;
public float maxDepth = 10.0f;
Vector3[] positions;
void Start()
{
cam = Camera.main;
positions = new Vector3[8];
}
void Update()
{
bool changed = false;
if(Input.Get$$anonymous$$ouseButtonDown(0))
{
mouseDown = Input.mousePosition;
changed = true;
}
else if(Input.Get$$anonymous$$ouseButton(0))
{
mouseUp = Input.mousePosition;
changed = true;
}
if(changed)
{
positions[0] = cam.ScreenToWorldPoint(new Vector3(mouseDown.x, mouseDown.y, $$anonymous$$Depth)); // front bottom left
positions[1] = cam.ScreenToWorldPoint(new Vector3(mouseUp.x, mouseDown.y, $$anonymous$$Depth)); // front bottom right
positions[2] = cam.ScreenToWorldPoint(new Vector3(mouseUp.x, mouseUp.y, $$anonymous$$Depth)); // front top right
positions[3] = cam.ScreenToWorldPoint(new Vector3(mouseDown.x, mouseUp.y, $$anonymous$$Depth)); // front top left
positions[4] = cam.ScreenToWorldPoint(new Vector3(mouseDown.x, mouseDown.y, maxDepth)); // back bottom left
positions[5] = cam.ScreenToWorldPoint(new Vector3(mouseUp.x, mouseDown.y, maxDepth)); // back bottom right
positions[6] = cam.ScreenToWorldPoint(new Vector3(mouseUp.x, mouseUp.y, maxDepth)); // back top right
positions[7] = cam.ScreenToWorldPoint(new Vector3(mouseDown.x, mouseUp.y, maxDepth)); // back top left
}
Debug.DrawLine(positions[0], positions[1], Color.red);
Debug.DrawLine(positions[1], positions[2], Color.red);
Debug.DrawLine(positions[2], positions[3], Color.red);
Debug.DrawLine(positions[3], positions[0], Color.red);
Debug.DrawLine(positions[4], positions[5], Color.blue);
Debug.DrawLine(positions[5], positions[6], Color.blue);
Debug.DrawLine(positions[6], positions[7], Color.blue);
Debug.DrawLine(positions[7], positions[4], Color.blue);
Debug.DrawLine(positions[0], positions[4], Color.green);
Debug.DrawLine(positions[1], positions[5], Color.green);
Debug.DrawLine(positions[2], positions[6], Color.green);
Debug.DrawLine(positions[3], positions[7], Color.green);
}
}
GetCamera() == Camera.main of course, didn't mention that.
I have only one camera in the scene.
Thank you very much for your time.
Will review my code, your solution works like a charm!
Your answer
![](https://koobas.hobune.stream/wayback/20220613021608im_/https://answers.unity.com/themes/thub/images/avi.jpg)
Follow this Question
Related Questions
How to make camera position relative to a specific target. 1 Answer
when I use shader grass is disappearing 0 Answers
GameObject and Camera 2 Answers
Terrain Distance 1 Answer
Rotate object with camera 0 Answers