Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

How to select several gameobjects with "drawing" a rectangle by mouse

Discussion in 'Scripting' started by oloesch, Oct 27, 2018.

  1. oloesch

    oloesch

    Joined:
    Oct 11, 2018
    Posts:
    16
    Hi,

    Is there a possibility to select several gameobjects with the mouse? I have many player gameobjects on a pitch and now, e.g., I want to select 3 player gameobjects by drag and drop a circle or rectangle around them with my mouse. (like if you're on your desktop and drag a rectangle around several files and the let the mouseclick go, then you have selected all files or folders which are in this rectangle).
    Is this possible in unity and if yes how? I couldn't find anything so far :-\

    For the moment I always have to click on each gameobject which I want to select and since I'm using a Bird Eye View the gameobjects are really small so they are hard to select. And also if you want to select more than one gameobject it's easier to just "draw" a rectangle around them and select them with that.

    Hopefully you can help me with this. Thanks in advance! :)
     
  2. StarManta

    StarManta

    Joined:
    Oct 23, 2006
    Posts:
    8,741
    Break this down into two problems:
    1) How to draw a rectangle on the screen by dragging
    This is pretty simple: when the mouse button is pressed down, record its screen position; when it's released (or, for realtime feedback, moved), record it; then create a new Rect(Mathf.Min(firstPos.x, secondPos.x), Mathf.Min(firstPos.y, secondPos.y), Mathf.Abs(secondPos.x-firstPos.x), Mathf.Abs(secondPos.y-firstPos.y) )

    Showing the selection rectangle on screen is a matter of assigning the .anchoredPosition and .sizeDelta of a UI rectangle object.

    2) How to find which objects are within that rectangle
    The magic method here is Camera.main.WorldToScreenPoint. You'll want to loop through all of your selectable objects, get their screen position using that method, and then use Rect.Contains to find out which are inside the rectangle.
     
    justtime, RavenOfCode and Ryiah like this.
  3. oloesch

    oloesch

    Joined:
    Oct 11, 2018
    Posts:
    16
    Hey, I finally got the rectangle working as I want it to be.
    So now I have to do your second step "How to find which objects are within that rectangle"
    I already tried it but it doesn't find any player in the rectangle. Could you please help me out here?

    First I get the list of all displayed player objects and then I did a foreach with each player and do a if clause with Rect.Contains
    Code (CSharp):
    1. int frame = objectHandle.getFrame();
    2. List<SportsPlayer> displayedPlayers = objectHandle.GetDisplayedPlayers();
    3. foreach (var player in displayedPlayers)
    4.                 {
    5.                     if (rectBox.Contains(new Vector2(player.PosXArray[frame] / 100, player.PosYArray[frame] / 100)))
    6.                     {
    7.                         Debug.Log("Player is in the Rectangle: " + player.JerseyNumber);
    8.                     }
    9.                 }
     
  4. StarManta

    StarManta

    Joined:
    Oct 23, 2006
    Posts:
    8,741
    You don't seem to have WorldToScreenPoint anywhere in there.

    I also won't really be able to help further without knowing what things like PosXArray are.
     
  5. oloesch

    oloesch

    Joined:
    Oct 11, 2018
    Posts:
    16
    Sorry my fault. I used the wrong if statement.
    my code looks like this:
    Code (CSharp):
    1.  
    2.     RaycastHit hit;
    3.  
    4.     public GUIStyle MouseDragSkin;
    5.     private static Vector3 mouseDownPoint;
    6.     private static Vector3 currentMousePoint;
    7.     public static bool UserIsDragging;
    8.     private static Vector2 MouseDragStart;
    9.  
    10.     public Camera camera;
    11.     private static Rect rectBox;
    12.  
    13.     void Awake()
    14.     {
    15.         mouseDownPoint = Vector3.zero;
    16.     }
    17.  
    18. void Update()
    19.     {
    20.         Ray ray = camera.ScreenPointToRay(Input.mousePosition);
    21.  
    22.         if (Physics.Raycast(ray, out hit, Mathf.Infinity))
    23.         {
    24.  
    25.             currentMousePoint = hit.point;
    26.  
    27.             if (Input.GetMouseButtonDown(0))
    28.             {
    29.                 mouseDownPoint = hit.point;
    30.                 MouseDragStart = Input.mousePosition;
    31.             }
    32.             else if (Input.GetMouseButton(0))
    33.             {
    34.                 UserIsDragging = true;
    35.             }
    36.             else if (Input.GetMouseButtonUp(0))
    37.             {
    38.                 UserIsDragging = false;
    39.                 GameObject[] allObjects = GameObject.FindGameObjectsWithTag("Player");
    40.                 foreach (var player in allObjects)
    41.                 {
    42.                     if (rectBox.Contains(new Vector2(player.transform.position.x, player.transform.position.z)))
    43.                     {
    44.                         Debug.Log("Player in Rectangle");
    45.                     }
    46.                 }
    The WorldToScreenPoint() is in the OnGUI() function where I draw the Box. Looks like this:
    Code (CSharp):
    1. void OnGUI()
    2.     {
    3.         if (UserIsDragging)
    4.         {
    5.             float BoxWidth = camera.WorldToScreenPoint(mouseDownPoint).x - camera.WorldToScreenPoint(currentMousePoint).x;
    6.             float BoxHeight = camera.WorldToScreenPoint(mouseDownPoint).y - camera.WorldToScreenPoint(currentMousePoint).y;
    7.             float BoxLeft, BoxTop;
    8.             BoxLeft = Input.mousePosition.x;
    9.             BoxTop = (Screen.height - Input.mousePosition.y) - BoxHeight;
    10.  
    11.             rectBox = new Rect(BoxLeft,
    12.                               BoxTop,
    13.                               BoxWidth,
    14.                               BoxHeight);
    15.             GUI.Box(rectBox, "", MouseDragSkin);
    16.         }
    17.     }
     
    Last edited: Oct 31, 2018
  6. SparrowGS

    SparrowGS

    Joined:
    Apr 6, 2017
    Posts:
    2,536
    1) i'd advise against using screen position, that makes it so your bound to the screen(no ability to scroll during the selection, or dragging the selection box with you), unless this is intentional i'd the world position.
     
  7. StarManta

    StarManta

    Joined:
    Oct 23, 2006
    Posts:
    8,741
    You use it there on the mouse position (even though the mouse should already be in screen space...?), but you need to use it on the players' positions. But again, the posted code doesn't provide enough context for me to help you much more (e.g. I have no idea how "mouseDownPoint" got its value), but it seems like you're using it on the wrong thing.

    That's true, but also way harder. Once OP has figured out screen space method it may be worth it. But tbh, I've found it to be very rare in games to support scrolling while click-drag-selecting, so it won't be particularly missed.
     
  8. oloesch

    oloesch

    Joined:
    Oct 11, 2018
    Posts:
    16
    I have edited the missing parts of the script. Hopefully you can now help me :)
     
  9. SparrowGS

    SparrowGS

    Joined:
    Apr 6, 2017
    Posts:
    2,536
    i think C&C generals do it like that, i'm not here to argue but i don't think it's that rare.

    it's not hard, just raycast from mousePos and use the worldPos instead
     
  10. StarManta

    StarManta

    Joined:
    Oct 23, 2006
    Posts:
    8,741
    OK I just saw that you're using OnGUI. Don't do that. OnGUI is part of a deprecated (and bad) UI system. Use the canvas-based UI system instead. In this case, you could be setting the selection box's position directly within Update instead of having it in a different function.

    As for the code itself, you're doing some unnecessary conversions which can only add confusion.* You're taking Input.mousePosition (which is in screen space), turning into world space via a raycast, then turning it back into screen space. Just store Input.mousePosition directly instead.

    And then, you're comparing the screen-space mouse coordinates to the world-space player coordinates. This is where the real issue is - you need to get them both into screen space. Line 42 as pasted is where you need WorldToScreenPoint:
    Code (csharp):
    1. if (rectBox.Contains(Camera.main.WorldToScreenPoint(player.transform.position) ) ) {
    Once they're both converted into screen space, it should work like a charm.


    * Unless you want to try SparrowsNest's method, in which case, you'll want to have everything in world space instead. For this, you'll want to build your rect out of the X and Z coordinates of your raycast hit, and instead of the above code snippet for the players, use the line you have. At that point your big challenge will be drawing the selection rectangle on screen, which is left as an exercise for the reader.

    The important thing is to keep track of world space vs. screen space and make sure you're comparing like to like.
     
  11. oloesch

    oloesch

    Joined:
    Oct 11, 2018
    Posts:
    16
    But if I don't do it with OnGUI it says that I can't call GUI functions like GUI.Box
    How can I draw the rectangle without OnGUI?
    The script is already added to a canvas so I think my programm is already a canvas-based UI system.

    I also don't know how I should work without the convertion of the ScreenPointToRay and then doing the raycast. Because I did a Debug.Log on "Input.mousePosition" on my "ray" and on "currentMousePoint" and all 3 variables have different values but if I understand you right "Input.mousePosition" should be the same number as my "currentMousePoint" which is the Screen Point "Input.mousePosition" converted to Ray and then doing the Raycast (converting it back to ScreenPoint. I'm quite confused right now :-\
     
    Last edited: Nov 1, 2018
  12. StarManta

    StarManta

    Joined:
    Oct 23, 2006
    Posts:
    8,741
    Check the link I provided. The Canvas system is an entirely different system with a different way of functioning compared to the OnGUI system. Basically, you will create an object in the scene within the UI (this is your "box") and modify that object's position using rectTransform.anchoredPosition.

    Not how that works. In reality you just have both types of UI in your game, and the Canvas one is just going unused while you work entirely in the OnGUI system.
     
  13. oloesch

    oloesch

    Joined:
    Oct 11, 2018
    Posts:
    16
    So I tried it without the OnGUI system and it looks like that at the moment:
    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. public class RtsSelect : MonoBehaviour
    6. {
    7.  
    8.     public LayerMask SelectUnitsLayerMask;
    9.     public LayerMask GroundLayerMask;            
    10.     public GameObject UiPanel;
    11.     public bool ShowDebug;
    12.     public Camera _camera;
    13.  
    14.     private RectTransform _boxRect;
    15.     private Vector2 _selectStart;
    16.     private bool _dragSelect;
    17.  
    18.     private void Awake()
    19.     {
    20.         UiPanel.gameObject.SetActive(false);
    21.         _boxRect = UiPanel.GetComponent<RectTransform>();
    22.     }
    23.  
    24.     void Update()
    25.     {
    26.         if (Input.GetMouseButtonDown(0))
    27.         {
    28.             _selectStart = Input.mousePosition;
    29.             _dragSelect = true;
    30.         }
    31.  
    32.         if (!_dragSelect) return;
    33.  
    34.         DrawBoundingBox();
    35.         SelectOverlapUnits();
    36.  
    37.         if (Input.GetMouseButtonUp(0))
    38.         {
    39.             _dragSelect = false;
    40.             UiPanel.SetActive(false);
    41.         }
    42.     }
    43.  
    44.     void DrawBoundingBox()
    45.     {
    46.         if (!UiPanel.activeSelf) UiPanel.SetActive(true);
    47.  
    48.         float minX = _selectStart.x < Input.mousePosition.x ? _selectStart.x : Input.mousePosition.x;
    49.         float maxX = _selectStart.x < Input.mousePosition.x ? Input.mousePosition.x : _selectStart.x;
    50.         float minY = _selectStart.y < Input.mousePosition.y ? _selectStart.y : Input.mousePosition.y;
    51.         float maxY = _selectStart.y < Input.mousePosition.y ? Input.mousePosition.y : _selectStart.y;
    52.  
    53.         _boxRect.offsetMin = new Vector2(minX, minY);
    54.         _boxRect.offsetMax = new Vector2(-Screen.width + maxX, -Screen.height + maxY);
    55.     }
    56.  
    57.     void SelectOverlapUnits()
    58.     {
    59.         Vector3 min = Vector3.zero;
    60.         Vector3 center = Vector3.zero;
    61.         Vector3 max = Vector3.zero;
    62.  
    63.         RaycastHit hit;
    64.         if (Physics.Raycast(_camera.ScreenPointToRay(new Vector3(_boxRect.position.x - _boxRect.rect.width / 2f, _boxRect.position.y - _boxRect.rect.height / 2f) + _camera.transform.position), out hit, GroundLayerMask)) min = hit.point;
    65.         if (Physics.Raycast(_camera.ScreenPointToRay(new Vector3(_boxRect.position.x + _boxRect.rect.width / 2f, _boxRect.position.y + _boxRect.rect.height / 2f) + _camera.transform.position), out hit, GroundLayerMask)) max = hit.point;
    66.         if (Physics.Raycast(_camera.ScreenPointToRay(new Vector3(_boxRect.position.x, _boxRect.position.y) + _camera.transform.position), out hit, GroundLayerMask)) center = hit.point;
    67.  
    68.         Collider[] units = Physics.OverlapBox(center, (max - min) / 2f, Quaternion.identity, SelectUnitsLayerMask);
    69.         Debug.Log(units.Length);
    70.     }
    71. }
    My problem now is that the length of units is always 0. I thought it's might because y and z are inverted in the program (so each object on the pitch is y = 0.0) but even if I change the Y's to Z's it's still 0.
    Anybody has an idea where my mistake is? :-\
     
  14. SparrowGS

    SparrowGS

    Joined:
    Apr 6, 2017
    Posts:
    2,536
    what does min center and max say?
     
  15. oloesch

    oloesch

    Joined:
    Oct 11, 2018
    Posts:
    16
    Do you mean the values?
    Example values are: min: (97.6, 0.0, 20.6) ;max: (73.3, 0.0, 4.9) ;center: (85.5, 0.0, 12.7)
    If I change y with z it's: min: (95.8, 0.0, 31.5) ;max: (75.2, 0.0, 16.6) ;center: (85.5, 0.0, 24.1)

    If you mean what they are in general:
    The documentation of the OverlapBox says:
    Code (CSharp):
    1. public static Collider[] OverlapBox(Vector3 center, Vector3 halfExtents, Quaternion orientation = Quaternion.identity, int layerMask = AllLayers, QueryTriggerInteraction queryTriggerInteraction = QueryTriggerInteraction.UseGlobal);
    so center is the center of the box. and "max - min" divided by 2f is the halfExtents (half of the box in each dimensions)
     
  16. SparrowGS

    SparrowGS

    Joined:
    Apr 6, 2017
    Posts:
    2,536
    I mean what are the actual values during runtime, put a log there, this may be the ussue
     
  17. oloesch

    oloesch

    Joined:
    Oct 11, 2018
    Posts:
    16
    I attached a screenshot of the rectangle.



    There you can see the rectangle and two players.
    The values of center, max and min are the following:
    min: (93.3, 0.0, 38.3)
    max: (77.7, 0.0, 9.9)
    center: (85.5, 0.0, 24.1)

    and the players have the following values:
    top player: X: -15.54 Y: 0 Z: -18.97
    bottom player: X: -17.07 Y: 0 Z: -2.84
     
  18. blu3drag0n

    blu3drag0n

    Joined:
    Nov 9, 2018
    Posts:
    94
    This is an old post, but when I was looking for a solution to get a selection rectangle drawing in game mode, also in a build version, then there were not really satisfying answers. Most tried via some assets packages or either Line Renderer or Shaders, which I found to be overloaded for what I was trying to achieve.

    I finally managed to build it myself with very few code and want to share for everyone else googling in the future, enjoy:
    Code (CSharp):
    1.  
    2. using UnityEngine;
    3. /// <summary>
    4. /// Draws a selection rectangle on the left mouse button down & dragging
    5. ///
    6. /// You only need an InputReceiverManager that basically tracks
    7. /// - if the left mouse button is currently down and saves it in "LeftMouseButtonDown"
    8. /// - saves the initial click position when mouse button was clicked  and saves it in "InitialMousePositionOnLeftClick"
    9. /// - updates the current mouse position and saves it in "CurrentMousePosition"
    10. ///
    11. /// </summary>
    12. public class SelectionRectangleDrawer : MonoBehaviour
    13. {
    14.     //set color via inspector for the selection rectangles filler color
    15.     public Color SelectionRectangleFillerColor;
    16.     //set color via inspector for the selection rectangles border color
    17.     public Color SelectionRectangleBorderColor;
    18.     //set selection rectangles  border thickness
    19.     public int SelectionRectangleBorderThickness = 2;
    20.     private Texture2D _selectionRectangleFiller;
    21.     private Texture2D _selectionRectangleBorder;
    22.     private bool _drawSelectionRectangle;
    23.     private float _x1, _x2, _y1, _y2;
    24.     private Vector2 pos1, pos2;
    25.     void Start()
    26.     {
    27.         _selectionRectangleFiller = new Texture2D(1, 1);
    28.         _selectionRectangleFiller.SetPixel(0, 0, SelectionRectangleFillerColor);
    29.         _selectionRectangleFiller.Apply();
    30.         _selectionRectangleFiller.wrapMode = TextureWrapMode.Clamp;
    31.         _selectionRectangleFiller.filterMode = FilterMode.Point;
    32.         _selectionRectangleBorder = new Texture2D(1, 1);
    33.         _selectionRectangleBorder.SetPixel(0, 0, SelectionRectangleBorderColor);
    34.         _selectionRectangleBorder.Apply();
    35.         _selectionRectangleBorder.wrapMode = TextureWrapMode.Clamp;
    36.         _selectionRectangleBorder.filterMode = FilterMode.Point;
    37.     }
    38.     void Update()
    39.     {
    40.         if (InputReceiverManager.Instance.LeftMouseButtonDown && !Mathf.Approximately(Vector2.Distance(InputReceiverManager.Instance.CurrentMousePosition,
    41.                                                                                                        InputReceiverManager.Instance.InitialMousePositionOnLeftClick), 0f))
    42.             _drawSelectionRectangle = true;
    43.         else if (!InputReceiverManager.Instance.LeftMouseButtonDown && _drawSelectionRectangle)
    44.             _drawSelectionRectangle = false;
    45.     }
    46.     private void OnGUI()
    47.     {
    48.         if (_drawSelectionRectangle)
    49.             drawSelectionRectangle();
    50.     }
    51.     private void drawSelectionRectangle()
    52.     {
    53.         pos1 = InputReceiverManager.Instance.InitialMousePositionOnLeftClick;
    54.         pos2 = InputReceiverManager.Instance.CurrentMousePosition;
    55.         //check initial mouse position on X axis versus dragging mouse position
    56.         if (pos1.x < pos2.x)
    57.         {
    58.             _x1 = pos1.x;
    59.             _x2 = pos2.x;
    60.         }
    61.         else
    62.         {
    63.             _x1 = pos2.x;
    64.             _x2 = pos1.x;
    65.         }
    66.         //check initial mouse position on Y axis versus dragging mouse position
    67.         if (pos1.y < pos2.y)
    68.         {
    69.             _y1 = pos1.y;
    70.             _y2 = pos2.y;
    71.         }
    72.         else
    73.         {
    74.             _y1 = pos2.y;
    75.             _y2 = pos1.y;
    76.         }
    77.         //filler
    78.         GUI.DrawTexture(new Rect(_x1, Screen.height - _y1, _x2 - _x1, _y1 - _y2), _selectionRectangleFiller, ScaleMode.StretchToFill);
    79.         //top line
    80.         GUI.DrawTexture(new Rect(_x1, Screen.height - _y1, _x2 - _x1, -SelectionRectangleBorderThickness), _selectionRectangleBorder, ScaleMode.StretchToFill);
    81.         //bottom line
    82.         GUI.DrawTexture(new Rect(_x1, Screen.height - _y2, _x2 - _x1, SelectionRectangleBorderThickness), _selectionRectangleBorder, ScaleMode.StretchToFill);
    83.         //left line
    84.         GUI.DrawTexture(new Rect(_x1, Screen.height - _y1, SelectionRectangleBorderThickness, _y1 - _y2), _selectionRectangleBorder, ScaleMode.StretchToFill);
    85.         //right line
    86.         GUI.DrawTexture(new Rect(_x2, Screen.height - _y1, -SelectionRectangleBorderThickness, _y1 - _y2), _selectionRectangleBorder, ScaleMode.StretchToFill);
    87.     }
    88. }
    89.  
    90.  
    91.