I’m curious as to how to go about properly handling unit selection in a RTS styled game, a few months ago i looked into this and I came up with my own option (Which had it’s own issues) which basically had a script on each unit that could be selected and a boolean that toggled rather or not that unit was selected. This obviously isn’t the right way to do it.
I know you can ray-cast a GUI.Box or whatever and use the BoxContains stuff to get everything inside of the box, but that’s not exactly what I’m talking about; what is the proper way to handle this?
Well, I’m not sure if you’re talking about multiple unit selection or single unit selection? I don’t know the “best” way to do it, but for single unit selection, I personally Physics.raycast from the Input.mouseposition against the layer I want to check and see if the ray hits a units collider or not. If it does, I set a Boolean on a script on the unit as “selected” (as I think you mentioned).
For multiple unit selection, that is a little more tricky, and I still don’t have a solution I’m 100% satisfied with, but to keep it short, I basically get the world locations of the four corners of a drag-select box, and then use a math formula to determine if the unit’s transform position point lies within the polygon. It works “okay”, but I still really dislike selecting by just single points. I’d much rather have a system where you can select by area, but havn’t found a way to do it yet.
This solution works “okay”, but it still has problems. As do most solutions for this. The biggest problem being that selecting units on the corners of the screen doesn’t work so well with this method, and this is due to the angled perspective camera (assuming you’re using an angled perspective camera as a lot of rts games do). The more your camera is angled towards zero, or 90 degrees, the better this solution works. But, like I said, this method will start missing units you think should be selected due to the camera angle.
The method I’m using (with a math function using world coordinates) accounts a little more for units towards the edges of the screen, so I preferred using it more than djfunkey’s (but that’s not to say that djfunkeys is bad or couldn’t be made to work). But even my current method still has camera angle problems, just not as severe.
I think the best method I’ve seen thus far, short of messing with the camera matrix and whatnot, is a guy who figured out how to use the method djfunkey just mentioned, except instead of filling a square box (or rectangle), this guy figured out how to fill the actual camera frustrum with a mesh collider. Which, to be honest, is pretty complicated, but doable. This solves almost perfectly for the camera angle problem you’d encounter otherwise. I also don’t know how well performance-wise it works – but it looked good. I think I saw it on YouTube somewhere so it’s out there, I just can’t remember the video name or anything like that.
There is also another way of going about this. Sorry for the book but I’ve been looking at solutions for this for over a year now. You can also use the WorldToScreenPoint() function to transform your units world transform point onto a screen point. Then you can test to see whether or not that point is within your gui box the player would draw on screen – and select the unit that way.
Which I would do right now if it weren’t for one thing, the WorldToScreenPoint() function does NOT account for camera angle with perspective!!! Darn them. lol. I really wish someone at Unity would create a function that would account for camera angle in perspective mode. As soon as you start angling your camera, the more “off” the converted world point becomes. It would be oh so easy if WorldToScreenPoint() would account for this.
WorldToScreenPoint() works just fine with perspective cameras AFAIK (I use it all the time). The downside to that approach is that the unit needs to be visible so if you allow camera movement while drag-selecting then that method will fail.
You can maintain a global list of player-owned units and register the “up” and “down” points of the mouse clicks and then compare the X and Z values of each unit’s position vector to see if it falls inside the “bounds” of the selection in 2D space. Doing it this way accounts for all units and discounts possible issues with map depth etc.
However - if a unit is at a rather low or high Y value (hill or valley) and is close to the selection boundary you might end up selecting more or less units than you would expect. This is compounded with very austere camera angles.
Really Kelso? My experience has been to the contrary. In my testing of it, when I moved a unit across the screen from the center towards the edges, the WorldToScreenPoint() got further and further off. For instance, by the time my test unit reached the right edge of the screen, the WorldToScreenPoint() location would actually be about 3 or 4 “units” to the left of the character.
Actually, only in the very center of the screen (both x and y) is the point actually where it should be. I would love to be wrong about this though. Just my experience.
Instantiate a Cube with a Box Collider (set the Mesh Collider to trigger), with one of its corners equaling the stored Vector 3. make sure it has a material on it, so you can see the dimensions of the Box in-game
On MouseButton, update each frame the size of the Box (This will include the Box Collider). This can be done by getting the current Camera.ScreenPointToRay and subtracting it from the stored Vector 3. Using OnTriggerStay get all the Gameobjects within the Box Collider that have the Tag “FriendlyUnit”, using a custom shader (try this one) set the shader material, for each of the “FriendlyUnits”, say to a green or blue colour. This is so you can see which units are temporarily selected. Store all the “FirendlyUnits” in a GameObejct Array.
Stop adding new GameObjects the the Array and Destroy the Selection Box (along with the Box Collider)
Using the GameObject Array we stored before, get information from each of the “FreindlyUnits” selected. and set all the “FriendlyUnits” Boolean’s to true saying that they are selected.
add fancy shader or change colour of model to show it is selected.
He would have got an Array of planes using GeometryUtility.CalculateFrustumPlanes then combined the meshes together (probably using Mesh.CombineMeshes) and added a Mesh Collider to it. Well that what I understood from what you mentioned.
If you do draw a GUI.Box and use that to find the units, Redo parts of the instructions I gave:
Get the Vector 2 of a corner of the GUI.Box and store it, make the GUI.Box a semi-transperant colour (e.g. green). Use Camera.ScreenPointToRay Racasting on the Terrain layer, Get the Vector 3 Position and store it. Continue to Step 2, of previous instructions.
For step 2, of previous instructions, turn off Mesh Render. So you dont see the 3D Box, you already have the GUI.Box
While Updating each frame get the Vector 2 Position of the GUI.Box and use Camera.ScreenPointToRay and continue with step 3.
I still would like someone (other than me) to use WorldToScreenPoint() on their character’s origin (perhaps their pivot point lets say to keep the reference point uniform) and test how precise it stays with your character on different parts of the screen during gameplay using a perspective camera (at say a 45 degree angle).
As I stated above, my tests show it’s not accurate, and drifts by an amount relative to the distance your character is from the center of the screen. At the center, it’s pretty accurate – but gets less and less accurate towards the edges of the screen.
Maybe you guys just don’t notice it, or don’t really need it to be that accurate? There is also the possibility that something in my code was causing the offset as well, but I won’t know unless someone else can test this?
How are you testing it? I’ve generated cube prefabs overtop of my units in screen space just to debug things from time to time and it’s always been fine. Quick googling doesn’t reveal others having similar issues aside from instances where the object wasn’t within the camera’s frustum. If this was an issue I would imagine it to be a pretty serious defect and therefore have been reported by others.
WorldToScreenPoint() approach has problems on scrolling screen. Since you are creating the rectangle on screen space and also objects disappearing on screen during scroll this is not a very healthy solution. I tried scaling a box with colliders but since my camera can rotate around in Y-axis and my map is centered at 0,0,0 i had so many problems too. Soon i will try creating mesh collider approach but i wish we had a simpler solution with some command like Physics.OverlapRectangle or something
Generally RTS games are on 2d planes (ground, water, air) etc. even if it’s presented in 3D it’s 2D under the hood. The only exceptions are games like Homeworld. So the question is how much can you simplify the problem to get the results you want.
I asked it because i know that Physics2d.OverlapAreaAll already provides a very easy solution for 2D games, and in my game graphics are 3d so the 3d collision detection didnt have a proper (as easy) substitutefor solving this problem.
However today i gave another shot to the Physics.OverlapBox with fresh mind and i think i solved the problem already. It only required me to do few little tweaks as below:
Vector3 startPos = Vector3.zero;
Vector3 endPos = Vector3.zero;
Rect selectRect = new Rect();
public Transform camRig;
if (Input.GetMouseButtonDown(0))
startPos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
if (Input.GetMouseButtonUp(0))
{
endPos= Camera.main.ScreenToWorldPoint(Input.mousePosition);
selectRect.xMin = Mathf.Min(startPos.x, endPos.x);
selectRect.xMax = Mathf.Max(startPos.x, endPos.x);
selectRect.yMin = Mathf.Min(startPos.y, endPos.y);
selectRect.yMax = Mathf.Max(startPos.y, endPos.y);
//Had to use rotation of my camera rig transform here to make it work properly
Collider[] units = Physics.OverlapBox(selectRect.center, new Vector3(selectRect.width, selectRect.height, 10), camRig.rotation);
Note: I also changed the ScreenToWorldPoint to a ScreenPointToRay function in my version. Becuase after using ScreenToWorldPoint with no problems, after sometime it decided to only bring me the near clip plane values. And even passing in Input.MousePosition with additional Z value didnt help. Very annoying.
If you’re navigation is powered by a grid graph you can track what graph nodes are inside your selection and then select the units that are standing on those nodes.
But then how do you make your selection of the grid nodes ? If you are using WorldToScreenPoint() it simply doesn’t work during screen scrolling. I have tested it.
You don’t. On mouse button down get the closest node to the mouse cursor. Do the same on mouse up. That gives you the 2 corners of your rectangle. Get all the nodes that are contained in that rectangle. Get all the units that are on (or near enough to) one of those nodes