Hi,
I wanted to accomplish the following:
Rectangle Box Selection like in RTS games - In 3D world with perspective camera
And as I was struggling through the internet the past days, either understanding the basics of camera, perspective, RayCasts etc.etc. and all the good stuff I stumble through a lot of partial help, but no final solution ever came across to how to put it all together, but I finally managed to completely write my own solution to this which I found to be pretty handy.
I leave it to you how to take input into “InputReceiverManager” or substitute the related parts of the code according to your own Input receiving methods. If you are looking for such a solution below, at this point you will be already able to do the input stuff (I use the new input system e.g.).
Also this example doesn’t make use of the generated shape here in the showcase yet.
Just go from there with the generated shape, because I also assume at this point you are able to utilize a simple collision detection according to the generated shape from my solution.
Its really all about getting the selection out into the world!
Please find a visualization further below.
It also works with any camera angle and rotation and updates your drag while moving around
(btw, the solution below also includes a single click detection on any object of the given layer, just as a bonus, as it was in my script anyways :-D)
using System;
using UnityEngine;
/// <summary>
/// Draws a selection rectangle on left mouse button down & dragging.
/// Also generates a 3D space selection mesh according to your selection.
///
/// You only need an InputReceiverManager that basically tracks
/// - if the left mouse button is currently down and saves it in "LeftMouseButtonDown"
/// - saves the initial click position when mouse button was clicked and saves it in "InitialMousePositionOnLeftClick"
/// - updates the current mouse position and saves it in "CurrentMousePosition"
///
/// </summary>
public class InputHandlingManager : MonoBehaviour
{
public static InputHandlingManager Instance;
#region Variables Single Click
//the layer which the mouseclick should account to
public LayerMask MouseClickLayer;
//for single click
private Ray _mouseClickRay;
#endregion
#region Variables Selection Rectangle
public Color SelectionRectangleFillerColor;
public Color SelectionRectangleBorderColor;
public int SelectionRectangleBorderThickness = 4;
public bool DrawSelectionRays = false;
public bool DrawSelectionGOFaces = false;
public bool KeepDrawnSelectionGO = false;
private Texture2D _selectionRectangleFiller;
private Texture2D _selectionRectangleBorder;
private bool _drawSelectionRectangle;
private float _x1, _x2, _y1, _y2;
private Vector3 _mouseSelectionRectangleCenter;
private float _redrawTimer = 0f;
private float _selectionProjectionDistance = 200f;
private GameObject _selectionMeshGO;
#endregion
private void Awake()
{
if (Instance)
throw new Exception("InputHandlingManager already exists as: '" + Instance.gameObject.name + "' under parent: '" + Instance.transform.parent.name + "'");
Instance = this;
}
void Start()
{
InitManager();
}
private void InitManager()
{
generateTexturesForSelectionRectangle();
}
void Update()
{
//simple left click
if (InputReceiverManager.Instance.LeftMouseFirstInitialTap)
performWorldClick();
//calculates if we are dragging a selection rectangle
if (InputReceiverManager.Instance.LeftMouseButtonDown && !Mathf.Approximately(Vector2.Distance(InputReceiverManager.Instance.CurrentMousePosition,
InputReceiverManager.Instance.InitialMousePositionOnLeftClick), 0f))
{
calculateSelectionRectanglePositions();
_drawSelectionRectangle = true;
try
{ multiSelectBuildings(); }
catch (Exception e)
{ Debug.Log("Just Ingore this, niche issue! Generating the selection sesh sameObject failed due to this stupid CONVEX collider calculation issue ~,~ " + e.Message); }
}
else if (!InputReceiverManager.Instance.LeftMouseButtonDown && _drawSelectionRectangle)
{
_drawSelectionRectangle = false;
if (_selectionMeshGO != null && !KeepDrawnSelectionGO) DestroyImmediate(_selectionMeshGO);
}
}
private void OnGUI()
{
if (_drawSelectionRectangle)
{
drawSelectionRectangle();
}
}
#region Methods for Selection Rectangle
private void generateTexturesForSelectionRectangle()
{
//define inner color for the selection rectangle
_selectionRectangleFiller = new Texture2D(1, 1);
_selectionRectangleFiller.SetPixel(0, 0, SelectionRectangleFillerColor);
_selectionRectangleFiller.Apply();
//define border color for the selection rectangle
_selectionRectangleBorder = new Texture2D(1, 1);
_selectionRectangleBorder.SetPixel(0, 0, SelectionRectangleBorderColor);
_selectionRectangleBorder.Apply();
}
private void multiSelectBuildings()
{
//redraw selection only based on this interval to reduce overhead
_redrawTimer += Time.deltaTime;
if (_redrawTimer > 0.05f)
{
_redrawTimer = 0f;
//take the corners of the selection rectangle as calculated in calculateSelectionRectanglePositions()
Vector3[] corners = {
new Vector3(_x1, _y2), //top left
new Vector3(_x2, _y2), //top right
new Vector3(_x1, _y1), //bottom left
new Vector3(_x2, _y1) //bottom right
};
//fire 4 Rays from camera through each of the 4 corners of the selection
Ray topLeftRay = CameraManager.MainCamera.ScreenPointToRay(corners[0]);
Ray topRightRay = CameraManager.MainCamera.ScreenPointToRay(corners[1]);
Ray bottomLeftRay = CameraManager.MainCamera.ScreenPointToRay(corners[2]);
Ray bottomRightRay = CameraManager.MainCamera.ScreenPointToRay(corners[3]);
// set flag if you want to draw the rays for the selected area
if (DrawSelectionRays)
{
Debug.DrawLine(topLeftRay.origin, topLeftRay.GetPoint(200f), Color.red);
Debug.DrawLine(topRightRay.origin, topRightRay.GetPoint(200f), Color.red);
Debug.DrawLine(bottomLeftRay.origin, bottomLeftRay.GetPoint(200f), Color.red);
Debug.DrawLine(bottomRightRay.origin, bottomRightRay.GetPoint(200f), Color.red);
}
//save 4 projected points at distance along the fired Rays
Vector3[] projectionEnds = {
topLeftRay.GetPoint(_selectionProjectionDistance), //top left
topRightRay.GetPoint(_selectionProjectionDistance), //top right
bottomLeftRay.GetPoint(_selectionProjectionDistance), //bottom left
bottomRightRay.GetPoint(_selectionProjectionDistance) //bottom right
};
//generate the selection object mesh according to the projected points in the distance
Mesh generatedSelectionMesh = generateSelectionMesh(projectionEnds);
//if a selection object is already there, destroy it
if (_selectionMeshGO != null) DestroyImmediate(_selectionMeshGO);
//finally generate the 3D object in space from the previous generated mesh details
_selectionMeshGO = generateSelection3DObject(generatedSelectionMesh);
}
}
private GameObject generateSelection3DObject(Mesh selectionMesh)
{
//create new object for the mesh
GameObject meshGO = new GameObject("selectionMeshGO");
MeshRenderer meshRenderer = meshGO.AddComponent<MeshRenderer>();
//add the collider for later inbound collision detection
MeshCollider meshCollider = meshGO.AddComponent<MeshCollider>();
meshCollider.sharedMesh = selectionMesh;
meshCollider.convex = true;
meshCollider.isTrigger = true;
// make the faces visible of the selectionMeshGO
if (DrawSelectionGOFaces)
{
MeshFilter meshFilter = meshGO.AddComponent<MeshFilter>();
meshFilter.mesh = selectionMesh;
Material selectionMeshMaterial = new Material(Shader.Find("Universal Render Pipeline/Lit"));
selectionMeshMaterial.SetColor("_BaseColor", Color.blue);
meshGO.GetComponent<Renderer>().material = selectionMeshMaterial;
}
return meshGO;
}
//generates the selection object mesh according to the projected points in the distance
private Mesh generateSelectionMesh(Vector3[] projectionEndPoints)
{
Vector3[] projectionEndPointsAll = new Vector3[5];
projectionEndPointsAll[0] = CameraManager.MainCamera.transform.position;
Array.Copy(projectionEndPoints, 0, projectionEndPointsAll, 1, 4);
Mesh selectionMesh = new Mesh();
selectionMesh.Clear();
selectionMesh.vertices = projectionEndPointsAll;
selectionMesh.triangles = new int[] {
0, 3, 1,
0, 1, 2,
0, 2, 4,
0, 4, 3,
1, 3, 2,
2, 3, 4
};
return selectionMesh;
}
//calculates the positions according where the mouse started when start draggin and where the mouse is now live
private void calculateSelectionRectanglePositions()
{
Vector2 pos1 = InputReceiverManager.Instance.InitialMousePositionOnLeftClick;
Vector2 pos2 = InputReceiverManager.Instance.CurrentMousePosition;
if (pos1.x < pos2.x) //initial mouse position is left from the moving mouse position
{
_x1 = pos1.x;
_x2 = pos2.x;
}
else //initial mouse poistion right from moving mouse position
{
_x1 = pos2.x;
_x2 = pos1.x;
}
if (pos1.y < pos2.y) //initial mouse position is below the moving mouse position
{
_y1 = pos1.y;
_y2 = pos2.y;
}
else //initial mouse position is above the moving mouse position
{
_y1 = pos2.y;
_y2 = pos1.y;
}
_mouseSelectionRectangleCenter = new Vector3(Mathf.Floor((_x1 + _x2)/2), Mathf.Floor((_y1 + _y2) / 2),0f);
}
//draws a rectangle into screenspace according to the players inputs of dragging a selection box
private void drawSelectionRectangle()
{
//filler
GUI.DrawTexture(new Rect(_x1, Screen.height - _y1, _x2 - _x1, _y1 - _y2), _selectionRectangleFiller, ScaleMode.StretchToFill);
//top line
GUI.DrawTexture(new Rect(_x1, Screen.height - _y1, _x2 - _x1, -SelectionRectangleBorderThickness), _selectionRectangleBorder, ScaleMode.StretchToFill);
//bottom line
GUI.DrawTexture(new Rect(_x1, Screen.height - _y2, _x2 - _x1, SelectionRectangleBorderThickness), _selectionRectangleBorder, ScaleMode.StretchToFill);
//left line
GUI.DrawTexture(new Rect(_x1, Screen.height - _y1, SelectionRectangleBorderThickness, _y1 - _y2), _selectionRectangleBorder, ScaleMode.StretchToFill);
//right line
GUI.DrawTexture(new Rect(_x2, Screen.height - _y1, -SelectionRectangleBorderThickness, _y1 - _y2), _selectionRectangleBorder, ScaleMode.StretchToFill);
}
#endregion
#region Method for Single Click
//performs a single click through the world and pools it to another manager to do something
private void performWorldClick()
{
_mouseClickRay = CameraManager.MainCamera.ScreenPointToRay(InputReceiverManager.Instance.CurrentMousePosition);
if (Physics.Raycast(_mouseClickRay, out RaycastHit hit))
{
MousePointer.transform.position = hit.point; //3D cube mouse pointer for testing
switch (LayerMask.LayerToName(hit.transform.gameObject.layer))
{
case "Buildings":
BuildingsInteractionManager.BuildingClicked(hit.transform);
break;
}
}
InputReceiverManager.Instance.LeftMouseFirstInitialTap = false;
}
#endregion
}
FPS of the GIF is bad, but otherwise I couldnt show as much:
Showcase
Feel free to ask any questions.
In case people don’t know how to achive the prerequisites of “InputReceiverManager” and afterwards how to utilize the generated shape to detect for collision and therefor finally select the objects within, let me know. I will go the extra mile if needed, but for now I wanted to have it compressed to what was the biggest challenge for me.
Have fun with it