Rts unit selection problem

Hi guys,

I found very little posts about selecting multiple units with a rectangle like in many rts.

I used a script (heavily ^^) based on this one :
http://forum.unity3d.com/threads/17634-C-amp-C-style-RTS-Community-Project-Generals

To be short, a 2D GUI rectangle is drawed, two ray are fired, one from the top left corner of this rectangle, another from the bottom right corner, a unit is selected if it is between the x and z of the two rays. And it work quite well :)

But there is a problem, if I rotate the camera on the Y axis (or any axis actually), the selection is not accurate anymore... (to be exact, if I rotate the camera to an angle other than 0, 90, 180, 270), I mean if a unit is in the 2D rectangle when the angle is not multiple of 90, sometime the unit is not selected, an sometime a unit outside of it is selected (more often on the fringes of the selection), If I pause the game and test the ray hit positions, it's normal, the units are not between them. But visually they are in the 2d rectangle.

Do you have an idea how to correct it? (I suck at math, I think that's the main problem. :p)

I was also thinking to put a cube with a collider and a triggered script on it, but I don't know how to make it match the 2D GUI rectangle shape.

Thanks in advance ;)

An idea:
Do a SphereCastAll or (just use a Physics.OverlapSphere ) that entirely covers the selected area
then test the returned hit objects screen coords using Camera.WorldToScreenPoint to make sure they are in the given rectangle.

If you have a small list of selectable, then you could probably skip the Sphere test,

I have to handle this case as well at some point in the future so am interested in what you find out.

Hi,

Thanks for your idea :)

it didn't work as I hoped, the WorldToScreenPoint return positions outside the rectangle as well...

But I found a solution, the four corners of my rectangle in 2D space create by raycasting the 4 courners a polygon (a trapezoid) in 3D world space, I just used a function (founded as is on the net) to know if a given point is in a polygon defined by an array of points.

So I iterate trought an array with all my units, test them with the function, and if they are in the polygon, they are selected.

And it work, if the unit is in the 2D gui rect, it's selected. I'm happy :smile:

Here is the complete code I use :

//the mouse position when the user move it ("bottom right" corner of the rect if you start from left to right and from top to bottom)
private var mouseButton1DownPoint : Vector2;
//the mouse position where the user first click ("top left" corner of the rect if you start from left to right and from top to bottom)
private var mouseButton1UpPoint : Vector2;

//the world position of the 4 corners
private var topLeft : Vector3;
private var topRight : Vector3;
private var bottomLeft : Vector3;
private var bottomRight : Vector3;

//the pointer moved where the user right-click (for moving unit(s))
var target : Transform;

//the list of selected units
private var listSelected = new Array ();

//the list of ALL units in the scene
private var listAllUnits = new Array ();

private var hit:RaycastHit;

//boolean to know if the left mouse button is down
private var leftButtonIsDown : boolean = false;

//cube placed at the 4 corner of the polygon, for visual debug
private var topLeftCube;
private var bottomRightCube;
private var topRightCube;
private var bottomLeftCube;

//the layer mask for walkable zones
private  var layerMask = 1 << 9;
//the layer mask for selectable objects
private var selectableLayerMask = 1 << 10;

//width and height of the 2D rectangle
var width : int;
var height : int;

var debugMode : boolean = false;

var selectionTexture : Texture;

// range in which a mouse down and mouse up event will be treated as "the same location" on the map.
private var mouseButtonReleaseBlurRange : int = 20;

function OnGUI() {
    if (leftButtonIsDown) {

        width = mouseButton1UpPoint.x - mouseButton1DownPoint.x;
        height = (Screen.height - mouseButton1UpPoint.y) - (Screen.height - mouseButton1DownPoint.y);
        var rect : Rect = Rect(mouseButton1DownPoint.x, Screen.height - mouseButton1DownPoint.y, width, height);
        GUI.DrawTexture (rect, selectionTexture, ScaleMode.StretchToFill, true);        

    }
}


function Start()
{

    if(debugMode)
    {
        topRightCube = GameObject.Find("topRight").transform;   
        bottomLeftCube = GameObject.Find("bottomLeft").transform;   
        topLeftCube = GameObject.Find("topLeft").transform; 
        bottomRightCube = GameObject.Find("bottomRight").transform;
    }

}

function Update () {

    if (Input.GetButtonDown ("Fire1"))
    {
        //if left button is down, save the mouse position, set the leftButtonIsDown to true and save the world position of the rectangle's "top left" corner
        mouseButton1UpPoint=Input.mousePosition;
        leftButtonIsDown = true;
        Physics.Raycast(Camera.main.ScreenPointToRay(Input.mousePosition),  hit, 1000);
        topLeft = hit.point;        
    }

    if (Input.GetButtonUp ("Fire1"))
    {
        //if left button is up set the leftButtonIsDown to false
        leftButtonIsDown = false;

        //if the range is not big enough, it's a simple clic, not a dragg-select operation
        if (IsInRange(mouseButton1DownPoint, mouseButton1UpPoint)) 
        {       
            // user just did a click, no dragging. mouse 1 down and up pos are equal.
            // if units are selected, move them. If not, select that unit.
            if (GetSelectedUnitsCount() == 0) 
            {
                // no units selected, select the one we clicked - if any.

                if ( Physics.Raycast (Camera.main.ScreenPointToRay (mouseButton1DownPoint), hit, 1000, selectableLayerMask) )
                { 
                    // Ray hit something. Try to select that hit target. 
                    //print ("Hit something: " + hit.collider.name);
                    AddSelectedUnit(hit.collider.gameObject);
                }

            }
        }

    }

    if (Input.GetButtonUp ("Fire2"))
    {
        //right click, move the pointer to the position
        if(Physics.Raycast(Camera.main.ScreenPointToRay(Input.mousePosition),  hit, 1000,layerMask))
        {       
            target.position=hit.point;
            target.position.y=0;
            //todo : move the selected units to the position of the pointer.
        }


    }

    //if the left button is down and the mouse is moving, start dragging

    if(leftButtonIsDown)
    {
        //actual position of the mouse
        mouseButton1DownPoint=Input.mousePosition;

        //set the 3 other corner of the polygon in the world space
        Physics.Raycast(Camera.main.ScreenPointToRay(Input.mousePosition),  hit, 1000);
        bottomRight = hit.point;

        Physics.Raycast(Camera.main.ScreenPointToRay(Vector2(Input.mousePosition.x+width,Input.mousePosition.y)),  hit, 1000);
        bottomLeft= hit.point;

        Physics.Raycast(Camera.main.ScreenPointToRay(Vector2(Input.mousePosition.x,Input.mousePosition.y-height)),  hit, 1000);
        topRight= hit.point;

        ClearSelectedUnitsList();       
        SelectUnitsInArea();

        if(debugMode)
        {
            bottomRightCube.position = bottomRight;
            topRightCube.position = topRight;
            bottomLeftCube.position = bottomLeft;
            topLeftCube.position = topLeft;     
        }

    }




}



function AddSelectedUnit(unitToAdd : GameObject) {
    listSelected.Push(unitToAdd);
    unitToAdd.GetComponent(pathTest).setSelectCircleVisible(true);  
}

function ClearSelectedUnitsList() { 

    for (var unitToClear : GameObject in listSelected) {
        unitToClear.GetComponent(pathTest).setSelectCircleVisible(false);   
    }
    listSelected.Clear();
}

function fillAllUnits(unitToAdd : GameObject)
{
    listAllUnits.Add(unitToAdd);
}

function SelectUnitsInArea() {
    var poly = new Array();

    //set the array with the 4 points of the polygon
    poly[0] =  topLeft;
    poly[1] = topRight;
    poly[2] = bottomRight;
    poly[3] = bottomLeft;

    //iterate trough the all unit's array
    for (var go : GameObject in listAllUnits) {
        var goPos : Vector3 = go.transform.position;
        //if the unit is in the polygon, select it. 
        if (isPointInPoly(poly, goPos)) 
        {
            AddSelectedUnit(go);
        }
    }
}   


//math formula to know if a given point is inside a polygon
function isPointInPoly(poly, pt){
    var c = false;
     l = poly.length;
     j = l - 1;

    for(i = -1 ; ++i < l; j = i){       
        if(((poly[i].z <= pt.z  pt.z < poly[j].z) || (poly[j].z <= pt.z  pt.z < poly[i].z))
         (pt.x < (poly[j].x - poly[i].x) * (pt.z - poly[i].z) / (poly[j].z - poly[i].z) + poly[i].x))
        c = !c;
        }
    return c;
}

function GetSelectedUnitsCount() {
    return listSelected.length;
}

function IsInRange(v1 : Vector2, v2 : Vector2) : boolean {
    var dist = Vector2.Distance(v1, v2);

    if (Vector2.Distance(v1, v2) < mouseButtonReleaseBlurRange) {
        return true;
    }
    return false;
}

So this script needs to be putted on a gameobject in the scene (most probably an empty one ;) ), I named mine "FrameListener"

And on each selectable unit you have to put in a start function somewhere this part of code :

var frameListener = GameObject.Find("FrameListener");

        frameListener.GetComponent (FrameListenerScript).fillAllUnits(this.gameObject);

Hope it can help someone

Thanks for posting that.

[quote]
it didn't work as I hoped, the WorldToScreenPoint return positions outside the rectangle as well...
[/quote]
My think was that yes, it would return some objects outside the selection rectangle, but you would iignore those by testing all points to see if they are inside the rectangle. Did this not work?

[quote=“pakfront”, post:4, topic: 429767]
Thanks for posting that.
[/quote]

No prob, this script is very basic, it can be optimized :wink:

[quote=“pakfront”, post:4, topic: 429767]
My think was that yes, it would return some objects outside the selection rectangle, but you would iignore those by testing all points to see if they are inside the rectangle. Did this not work?
[/quote]

Not like I wanted, some objects visibly within the rectangle return a point outside the rectangle, so that was the same problem as before :slight_smile:

I know this thread is dead, but this script works really well. I'm using it for one of my own projects, so I brought the script over to C#, and added some features. Hope someone will find this useful

//C# conversion from tet2bricks original code, by Jonah Peele


//All selectable units need need to have the script below attached to them or they won't be checked
/*    ---Selectable.CS---
using UnityEngine;
using System.Collections;
public class Selectable : MonoBehaviour 
{
    public void Start() 
    {
        StartCoroutine("braodcstSelectablility");
    }
    public IEnumerator braodcstSelectablility( )
    {
        yield return new WaitForSeconds(0.1F);

        foreach ( Object o in GameObject.FindObjectsOfType(typeof(GameObject)) ){
            GameObject go = (GameObject) o;
            go.SendMessage("fillAllUnits", this.gameObject, SendMessageOptions.DontRequireReceiver);
        }
    }
}
*///end--Selectable.CS---
//note: if we dont make selectable yield, there is a chance selectable gameObjects will try to register themselves,
//      before the _allUnits List has been intialized.

//TODO: make the debug code instatiate new cubes on runtime rather than trying to find them in scene
//TODO: clean up the sendMeassage calls with faster methods once we have locked down our unit manager code

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public enum MouseButton { Left, Right } //there is a more elgant way to handle enums attached thier members
public enum MouseFunction { MarqueeSelection, Click, ClickDrag }
public enum MarqueeType { Texture2d, EmptyBox, None } 

public class MouseButtonObserver : MonoBehaviour 
{

    public MouseButton WhichButton = MouseButton.Left;
    public string ButtonName { 
        get {
            switch ( WhichButton ) { 
            case MouseButton.Left:
                return "Fire1";
                break;
            case MouseButton.Right:
                return "Fire2";
                break;
            default: 
                return "Fire1";
            }
        }
    }
    //private bool IsUnitSelector = true;// anitquated
    public MouseFunction MouseFunctionType = MouseFunction.Click;
    public MarqueeType Marquee;
    private Vector2 downLocation;
    private Vector2 startDragLocation;
    private Vector2 upLocation;

    //these are for the position of the selection coreners
    private Vector3 _topLeft;
    private Vector3 _topRight;
    private Vector3 _bottomLeft;
    private Vector3 _bottomRight;

    private bool _isDragging = false;
    public bool isDragging { 
        get { return _isDragging; }
        set { 
            if ( _isDragging != value ){
                _isDragging = value;
                //DebugStreamer.message = ButtonName + " isDragging = " + _isDragging.ToString() ;
            }
        }
    }
    private bool _isDown = false;
    //public GameObject debugStreamer;
    public bool isDown { 
        get { return _isDown; } 
        set { 
            if (_isDown != value) { 
                _isDown = value;
                //DebugStreamer.message = ButtonName + " isDown = " + _isDown.ToString() ;
            }
        }       
    }

    //for debug
    private Transform topLeftCube;
    private Transform bottomRightCube;
    private Transform topRightCube;
    private Transform bottomLeftCube;

    public int DragDither = 20;
    public Color OutlineColor;
    public Texture2D MarqueeTexture;
    public GameObject MouseButtonListener;

    private float width() { return startDragLocation.x - upLocation.x; }
    private float height() { return (Screen.height - startDragLocation.y) - (Screen.height - upLocation.y); }

    public List<GameObject> _selectedUnits;// = new List<GameObject>();
    public int GetSelectedUnitsCount() { return _selectedUnits.Count; }
    public List<GameObject> GetSelectedUnits() { return _selectedUnits; }

    private List<GameObject> _allUnits;//this bugs out when its set to private because it initializes too late
    public void fillAllUnits( GameObject unitToAdd ) { 
        _allUnits.Add(unitToAdd);
        NumberOfSelectableUnitsInScene = _allUnits.Count;
        //DebugStreamer.message = "unit added:" + unitToAdd.ToString();
    }
    public int NumberOfSelectableUnitsInScene;
    public bool DebugMode = false;

    private RaycastHit hit = new RaycastHit();
    private Texture2D whiteTex ;

    private Vector3 ClickDragStart;
    private Vector3 ClickDragEnd;
    private Ray _clickDragRay;
    public Ray ClickDragray { get { return _clickDragRay; } } 

    public void Awake()
    {
        _allUnits = new List<GameObject>();
    }
    public void Start()
    {
        _allUnits = new List<GameObject>();
        _selectedUnits = new List<GameObject>();
        if( !MouseButtonListener ){ Debug.LogWarning("No Listener assigned to " + this );}  

        whiteTex = new Texture2D(1,1);
        whiteTex.SetPixel(0,0, new Color(1,1,1,1));
        whiteTex.Apply();

        if (DebugMode)
        {
            topRightCube = GameObject.Find("topRight").transform;   
            bottomLeftCube = GameObject.Find("bottomLeft").transform;   
            topLeftCube = GameObject.Find("topLeft").transform; 
            bottomRightCube = GameObject.Find("bottomRight").transform;
        }
    }



    public MouseButtonObserver( MouseButton newButton, /*string buttonName,*/ Texture2D marquee )
    {
        WhichButton = newButton;
        //ButtonName = buttonName;
        MarqueeTexture = marquee;
    }

    public void Update()
    {
        switch (MouseFunctionType) 
        {
        case MouseFunction.Click:
            if ( MouseButtonListener ) {
                if ( Input.GetButtonUp(ButtonName) ){
                    MouseButtonListener.SendMessage( "Click", ButtonName );
                }
            }
            break;
        case MouseFunction.ClickDrag:
            if ( Input.GetButton( ButtonName ) ) {

                downLocation = Input.mousePosition;

                if ( !_isDown ) {
                    isDown = true;
                    startDragLocation = downLocation; 
                } 

                if (_isDragging == false) { 
                    _isDragging = hasStartedDragging( startDragLocation, downLocation );
                } else { 
                    ClickDragStart = getScreenRaycastPoint( startDragLocation );
                    ClickDragEnd = getScreenRaycastPoint( downLocation );
                    _clickDragRay = new Ray( ClickDragStart, (ClickDragEnd - ClickDragStart).normalized );
                    Debug.DrawRay( ClickDragStart, (ClickDragEnd - ClickDragStart).normalized*10, Color.yellow );
                }

            }

            if ( Input.GetButtonUp( ButtonName ) ) {

                //did the player drag the mous or just click?
                if ( isDragging == false ) { 
                    MouseButtonListener.SendMessage( "Click", ButtonName );
                    DebugStreamer.message = ButtonName + " was clicked";
                } else { 
                    upLocation = Input.mousePosition;

                    if ( MouseButtonListener ) { 
                        DebugStreamer.message = "New Direction" + _clickDragRay.origin.ToString() + _clickDragRay.direction.ToString();
                        MouseButtonListener.SendMessage( "ClickDragInput", _clickDragRay, SendMessageOptions.DontRequireReceiver);
                    }
                }

                isDragging = false;
                isDown = false;
            }
            break;
        case MouseFunction.MarqueeSelection:
            if ( Input.GetButton( ButtonName ) )
            {
                downLocation = Input.mousePosition;

                if ( !_isDown ) {
                    isDown = true;
                    startDragLocation = downLocation; 
                } 

                if (_isDragging == false) { 
                    isDragging = hasStartedDragging( startDragLocation, downLocation );
                } else { 
                    _topLeft = getScreenRaycastPoint( startDragLocation );
                    _selectedUnits.Clear();
                }

            }
            if ( Input.GetButtonUp( ButtonName ) ) {

                isDragging = false;
                isDown = false;
                upLocation = Input.mousePosition;


                getOtherCorners();
                if (DebugMode) { showCorners(); } 

                SelectUnitsInArea();

                if ( MouseButtonListener ) { 
                    MouseButtonListener.SendMessage( "SetSelection", _selectedUnits, SendMessageOptions.DontRequireReceiver);
                }

            }
            break;
        }
    }
    public void getOtherCorners()
    {
        _bottomRight = getScreenRaycastPoint( upLocation );
        _bottomLeft  = getScreenRaycastPoint( new Vector2( upLocation.x+width(), upLocation.y ) );
        _topRight    = getScreenRaycastPoint( new Vector2( upLocation.x, upLocation.y-height() ) );
    }
    public void showCorners()   
    {
        topLeftCube.position     = _topLeft;
        topRightCube.position    = _topRight;
        bottomLeftCube.position  = _bottomLeft;
        bottomRightCube.position = _bottomRight;
    }
    private void SelectUnitsInArea()
    {
        //DebugStreamer.message = "allUnit count:" + _allUnits.Count ;
        //DebugStreamer.message = "allUnit 1:" + _allUnits[0].ToString() ;
        //var poly = new Array();
        Vector3[] poly = new Vector3[4];

        //set the array with the 4 points of the polygon
        poly[0] = _topLeft;
        poly[1] = _topRight;
        poly[2] = _bottomRight;
        poly[3] = _bottomLeft;

        //iterate trough the all unit's array
        foreach ( GameObject go in _allUnits) { 
            //if the unit is in the polygon, select it. 
            if (isPointInPoly(poly, go.transform.position)) {
                _selectedUnits.Add(go);
                //print ( "unit added" + go.ToString() );
            }
        }
    }
    //this is a fancy fucking algorithm , I have no idea how it works
    private bool isPointInPoly( Vector3[] poly, Vector3 pt )
    {
        bool c = false;
         int  l = 4;//poly.length;
         int j = l - 1;

        for( int i = -1 ; ++i < l; j = i){      
            if(((poly[i].z <= pt.z  pt.z < poly[j].z) || (poly[j].z <= pt.z  pt.z < poly[i].z))
             (pt.x < (poly[j].x - poly[i].x) * (pt.z - poly[i].z) / (poly[j].z - poly[i].z) + poly[i].x))
            c = !c;
            }
        return c;
    }



    public void OnGUI() {

        if ( _isDragging  (MouseFunctionType==MouseFunction.MarqueeSelection) )
        {
            float width = startDragLocation.x - downLocation.x;
            float height = ( Screen.height - startDragLocation.y) - ( Screen.height - downLocation.y ) ;
            Rect rect = new Rect( downLocation.x, Screen.height - downLocation.y, width, height);

            if ( Marquee == MarqueeType.Texture2d ){
                GUI.DrawTexture (rect, MarqueeTexture, ScaleMode.StretchToFill, true);
            } else if ( Marquee == MarqueeType.EmptyBox ){ 
                drawHollowRect(rect, 1.0f, OutlineColor);
            }
        }
    }
    //converted to C# from dr. blitzkrieg's original method
    public void drawHollowRect ( Rect r, float thickness, Color col)
    {
        if(thickness<=0)return;
        Color origColor = GUI.color;
        GUI.color=col;//Set color
        GUI.DrawTexture( new Rect(r.x,r.y,thickness,r.height),whiteTex);//left side
        GUI.DrawTexture( new Rect(r.x,r.y,r.width,thickness),whiteTex);//top
        GUI.DrawTexture( new Rect(r.x+r.width-(thickness-0),r.y,thickness,r.height),whiteTex);//right
        GUI.DrawTexture( new Rect(r.x,r.y+r.height-(thickness-0),r.width,thickness),whiteTex);//bottom
        GUI.color=origColor;//Reset color to white
    }

    private bool hasStartedDragging ( Vector2 v1, Vector2 v2)
    {
        //var dist = Vector2.Distance(v1, v2);

        if (Vector2.Distance(v1, v2) > DragDither) {
            return true;
        } else {
            return false;
        }
    }

    private Vector3 getScreenRaycastPoint ( Vector2 screenPosition ) 
    {
        Physics.Raycast(Camera.main.ScreenPointToRay(screenPosition), out hit, 1000);   
        return hit.point;
    }

}

[quote=“anon_22876926”, post:6, topic: 429767]
I know this thread is dead, but this script works really well. I’m using it for one of my own projects, so I brought the script over to C#, and added some features. Hope someone will find this useful
[/quote]
Good job thnx!

Just wanted to say thanks especially to tet2brick, but to others also. The javascript code worked awesome. I was having the same exact problem trying to do a spherecastall etc and was having units on the edges of the screen being selected even though the GUI box was not over them at all. I knew it had to do with angles and converting a 2d box to a 3d environment, but I had no clue how to fix it.

I used this code, plugged in some of my own variables and game objects and after a little tweaking it works like a charm!

Thank you for this code :)

Happy. :p

Ok so after testing this algorithm/function with a lot of units to select (say 12+ units), the function actually doesn't work so well for me now :x

The problem is that for some reason it's too intensive on the processor I think, and so every time I do a multiple unit selection in my game that involves more than about 10 units, my game lags. For small numbers of selections it doesn't lag at all, but it's just too cpu intensive when there are a lot of points to calculate at once.

The function works great, but it causes a slight lag stutter when I do my multiple unit selections. Anyone have any ideas how to resolve this?

[quote=“Velo222”, post:10, topic: 429767]
Ok so after testing this algorithm/function with a lot of units to select (say 12+ units), the function actually doesn’t work so well for me now :x

The problem is that for some reason it’s too intensive on the processor I think, and so every time I do a multiple unit selection in my game that involves more than about 10 units, my game lags. For small numbers of selections it doesn’t lag at all, but it’s just too cpu intensive when there are a lot of points to calculate at once.

The function works great, but it causes a slight lag stutter when I do my multiple unit selections. Anyone have any ideas how to resolve this?
[/quote]

Well Im not sure what the whole point of having 5 GUI calls instead of 1 and checking the point with a vector3 array instead of using the Rect you already have and doing Rect.Contains()

Edit: Just tested my Selection script with 337 units and not even a stutter. Finally got a stutter with 1345 objects being selected.

Well I havn't looked at workarounds for my stutter problem yet, but I hope you're right. I havn't posted any of my code yet (as far as I know lol), so we'll see. I'll take a look at it sometime this week probably.

I really hope I am doing something wrong actually, because this point in poly function does work really well. :sunglasses:

In my script I feed the function the rectangle points (4 of them) and each transform position of every game object in my selected units array.

Yep it was my mistake! I had the function running twice basically, although it wasn't super obvious. :roll: This is one of those times I'm glad I'm wrong.

Thanks for letting me know it works well with selecting lots of units Bladesfist. And I retract my former statement. This is really a cool function.

I am not using this script, I wrote my own which is a bit more optimized than this.

tet2brick, this code even do not compile:
Assets/scripts/FrameListenerScript.js(323,28): BCE0005: Unknown identifier: 'pathTest'.
Assets/scripts/FrameListenerScript.js(335,34): BCE0005: Unknown identifier: 'pathTest'.