///Original Credit Korindian ///Sourced from - http://forum.unity3d.com/threads/rts-style-drag-selection-box.265739/ ///Updated Credit BenZed ///Sourced from - http://forum.unity3d.com/threads/color-picker.267043/ /* * What the SelectionBox component does is allow the game player to select objects using an RTS style click and drag interface: * * We want to be able to select Game Objects of any type, * We want to be able to drag select a group of game objects to select them, * We want to be able to hold the shift key and drag select a group of game objects to add them to the current selection, * We want to be able to single click a game object to select it, * We want to be able to hold the shift key and single click a game object to add it to the current selection, * We want to be able to hold the shift key and single click an already selected game object to remove it from the current selection. * * Most importantly, we want this behaviour to work with UI, 2D or 3D gameObjects, so it has to be smart about considering their respective screen spaces. * * Add this component to a Gameobject with a Canvas with RenderMode.ScreenSpaceOverlay * And implement the IBoxSelectable interface on any MonoBehaviour to make it selectable. * * Improvements that could be made: * * Control clicking a game object to select all objects of that type or tag. * Compatibility with Canvas Scaling * Filtering single click selections of objects occupying the same space. (So that, for example, you're only click selecting the game object found closest to the camera) * */ using UnityEngine; using UnityEngine.Events; using UnityEngine.UI; using System.Collections.Generic; namespace UnityEngine.UI.Extensions { [RequireComponent(typeof(Canvas))] public class SelectionBox : MonoBehaviour { // The color of the selection box. public Color color; // An optional parameter, but you can add a sprite to the selection box to give it a border or a stylized look. // It's suggested you use a monochrome sprite so that the selection // Box color is still relevent. public Sprite art; // Will store the location of wherever we first click before dragging. private Vector2 origin; // A rectTransform set by the User that can limit which part of the screen is eligable for drag selection public RectTransform selectionMask; //Stores the rectTransform connected to the generated gameObject being used for the selection box visuals private RectTransform boxRect; // Stores all of the selectable game objects private IBoxSelectable[] selectables; // A secondary storage of objects that the user can manually set. private MonoBehaviour[] selectableGroup; //Stores the selectable that was touched when the mouse button was pressed down private IBoxSelectable clickedBeforeDrag; //Stores the selectable that was touched when the mouse button was released private IBoxSelectable clickedAfterDrag; //Custom UnityEvent so we can add Listeners to this instance when Selections are changed. public class SelectionEvent : UnityEvent {} public SelectionEvent onSelectionChange = new SelectionEvent(); //Ensures that the canvas that this component is attached to is set to the correct render mode. If not, it will not render the selection box properly. void ValidateCanvas(){ var canvas = gameObject.GetComponent(); if (canvas.renderMode != RenderMode.ScreenSpaceOverlay) { throw new System.Exception("SelectionBox component must be placed on a canvas in Screen Space Overlay mode."); } var canvasScaler = gameObject.GetComponent(); if (canvasScaler && canvasScaler.enabled && (!Mathf.Approximately(canvasScaler.scaleFactor, 1f) || canvasScaler.uiScaleMode != CanvasScaler.ScaleMode.ConstantPixelSize)) { Destroy(canvasScaler); Debug.LogWarning("SelectionBox component is on a gameObject with a Canvas Scaler component. As of now, Canvas Scalers without the default settings throw off the coordinates of the selection box. Canvas Scaler has been removed."); } } /* * The user can manually set a group of objects with monoBehaviours to be the pool of objects considered to be selectable. The benefits of this are two fold: * * 1) The default behaviour is to check every game object in the scene, which is much slower. * 2) The user can filter which objects should be selectable, for example units versus menu selections * */ void SetSelectableGroup(IEnumerable behaviourCollection) { // If null, the selectionbox reverts to it's default behaviour if (behaviourCollection == null) { selectableGroup = null; return; } //Runs a double check to ensure each of the objects in the collection can be selectable, and doesn't include them if not. var behaviourList = new List(); foreach(var behaviour in behaviourCollection) { if (behaviour as IBoxSelectable != null) { behaviourList.Add (behaviour); } } selectableGroup = behaviourList.ToArray(); } void CreateBoxRect(){ var selectionBoxGO = new GameObject(); selectionBoxGO.name = "Selection Box"; selectionBoxGO.transform.parent = transform; selectionBoxGO.AddComponent(); boxRect = selectionBoxGO.transform as RectTransform; } //Set all of the relevant rectTransform properties to zero, //finally deactivates the boxRect gameobject since it doesn't //need to be enabled when not in a selection action. void ResetBoxRect(){ //Update the art and color on the off chance they've changed Image image = boxRect.GetComponent(); image.color = color; image.sprite = art; origin = Vector2.zero; boxRect.anchoredPosition = Vector2.zero; boxRect.sizeDelta = Vector2.zero; boxRect.anchorMax = Vector2.zero; boxRect.anchorMin = Vector2.zero; boxRect.pivot = Vector2.zero; boxRect.gameObject.SetActive(false); } void BeginSelection(){ // Click somewhere in the Game View. if (!Input.GetMouseButtonDown(0)) return; //The boxRect will be inactive up until the point we start selecting boxRect.gameObject.SetActive(true); // Get the initial click position of the mouse. origin = new Vector2(Input.mousePosition.x, Input.mousePosition.y); //If the initial click point is not inside the selection mask, we abort the selection if (!PointIsValidAgainstSelectionMask(origin)) { ResetBoxRect(); return; } // The anchor is set to the same place. boxRect.anchoredPosition = origin; MonoBehaviour[] behavioursToGetSelectionsFrom; // If we do not have a group of selectables already set, we'll just loop through every object that's a monobehaviour, and look for selectable interfaces in them if (selectableGroup == null) { behavioursToGetSelectionsFrom = GameObject.FindObjectsOfType(); } else { behavioursToGetSelectionsFrom = selectableGroup; } //Temporary list to store the found selectables before converting to the main selectables array List selectableList = new List(); foreach (MonoBehaviour behaviour in behavioursToGetSelectionsFrom) { //If the behaviour implements the selectable interface, we add it to the selectable list IBoxSelectable selectable = behaviour as IBoxSelectable; if (selectable != null) { selectableList.Add (selectable); //We're using left shift to act as the "Add To Selection" command. So if left shift isn't pressed, we want everything to begin deselected if (!Input.GetKey (KeyCode.LeftShift)) { selectable.selected = false; } } } selectables = selectableList.ToArray(); //For single-click actions, we need to get the selectable that was clicked when selection began (if any) clickedBeforeDrag = GetSelectableAtMousePosition(); } bool PointIsValidAgainstSelectionMask(Vector2 screenPoint){ //If there is no seleciton mask, any point is valid if (!selectionMask) { return true; } Camera screenPointCamera = GetScreenPointCamera(selectionMask); return RectTransformUtility.RectangleContainsScreenPoint(selectionMask, screenPoint, screenPointCamera); } IBoxSelectable GetSelectableAtMousePosition() { //Firstly, we cannot click on something that is not inside the selection mask (if we have one) if (!PointIsValidAgainstSelectionMask(Input.mousePosition)) { return null; } //This gets a bit tricky, because we have to make considerations depending on the heirarchy of the selectable's gameObject foreach (var selectable in selectables) { //First we check to see if the selectable has a rectTransform var rectTransform = (selectable.transform as RectTransform); if (rectTransform) { //Because if it does, the camera we use to calulate it's screen point will vary var screenCamera = GetScreenPointCamera(rectTransform); //Once we've found the rendering camera, we check if the selectables rectTransform contains the click. That way we //Can click anywhere on a rectTransform to select it. if (RectTransformUtility.RectangleContainsScreenPoint(rectTransform, Input.mousePosition, screenCamera)) { //And if it does, we select it and send it back return selectable; } } else { //If it doesn't have a rectTransform, we need to get the radius so we can use it as an area around the center to detect a click. //This works because a 2D or 3D renderer will both return a radius var radius = selectable.transform.renderer.bounds.extents.magnitude; var selectableScreenPoint = GetScreenPointOfSelectable(selectable); //Check that the click fits within the screen-radius of the selectable if (Vector2.Distance(selectableScreenPoint, Input.mousePosition) <= radius) { //And if it does, we select it and send it back return selectable; } } } return null; } void DragSelection(){ //Return if we're not dragging or if the selection has been aborted (BoxRect disabled) if (!Input.GetMouseButton(0) || !boxRect.gameObject.activeSelf) return; // Store the current mouse position in screen space. Vector2 currentMousePosition = new Vector2(Input.mousePosition.x, Input.mousePosition.y); // How far have we moved the mouse? Vector2 difference = currentMousePosition - origin; // Copy the initial click position to a new variable. Using the original variable will cause // the anchor to move around to wherever the current mouse position is, // which isn't desirable. Vector2 startPoint = origin; // The following code accounts for dragging in various directions. if (difference.x < 0) { startPoint.x = currentMousePosition.x; difference.x = -difference.x; } if (difference.y < 0) { startPoint.y = currentMousePosition.y; difference.y = -difference.y; } // Set the anchor, width and height every frame. boxRect.anchoredPosition = startPoint; boxRect.sizeDelta = difference; //Then we check our list of Selectables to see if they're being preselected or not. foreach(var selectable in selectables) { Vector3 screenPoint = GetScreenPointOfSelectable(selectable); //If the box Rect contains the selectabels screen point and that point is inside a valid selection mask, it's being preselected, otherwise it is not. selectable.preSelected = RectTransformUtility.RectangleContainsScreenPoint(boxRect, screenPoint, null) && PointIsValidAgainstSelectionMask(screenPoint); } //Finally, since it's possible for our first clicked object to not be within the bounds of the selection box //If it exists, we always ensure that it is preselected. if (clickedBeforeDrag != null) { clickedBeforeDrag.preSelected = true; } } void ApplySingleClickDeselection(){ //If we didn't touch anything with the original mouse press, we don't need to continue checking if (clickedBeforeDrag == null) return; //If we clicked a selectable without dragging, and that selectable was previously selected, we must be trying to deselect it. if (clickedAfterDrag != null && clickedBeforeDrag.selected && clickedBeforeDrag.transform == clickedAfterDrag.transform ) { clickedBeforeDrag.selected = false; clickedBeforeDrag.preSelected = false; } } void ApplyPreSelections(){ foreach(var selectable in selectables) { //If the selectable was preSelected, we finalize it as selected. if (selectable.preSelected) { selectable.selected = true; selectable.preSelected = false; } } } Vector2 GetScreenPointOfSelectable(IBoxSelectable selectable) { //Getting the screen point requires it's own function, because we have to take into consideration the selectables heirarchy. //Cast the transform as a rectTransform var rectTransform = selectable.transform as RectTransform; //If it has a rectTransform component, it must be in the heirarchy of a canvas, somewhere. if (rectTransform) { //And the camera used to calculate it's screen point will vary. Camera renderingCamera = GetScreenPointCamera(rectTransform); return RectTransformUtility.WorldToScreenPoint(renderingCamera, selectable.transform.position); } //If it's no in the heirarchy of a canvas, the regular Camera.main.WorldToScreenPoint will do. return Camera.main.WorldToScreenPoint(selectable.transform.position); } /* * Finding the camera used to calculate the screenPoint of an object causes a couple of problems: * * If it has a rectTransform, the root Canvas that the rectTransform is a descendant of will give unusable * screen points depending on the Canvas.RenderMode, if we don't do any further calculation. * * This function solves that problem. */ Camera GetScreenPointCamera(RectTransform rectTransform) { Canvas rootCanvas = null; RectTransform rectCheck = rectTransform; //We're going to check all the canvases in the heirarchy of this rectTransform until we find the root. do { rootCanvas = rectCheck.GetComponent(); //If we found a canvas on this Object, and it's not the rootCanvas, then we don't want to keep it if (rootCanvas && !rootCanvas.isRootCanvas) { rootCanvas = null; } //Then we promote the rect we're checking to it's parent. rectCheck = (RectTransform)rectCheck.parent; } while (rootCanvas == null); //Once we've found the root Canvas, we return a camera depending on it's render mode. switch (rootCanvas.renderMode) { case RenderMode.ScreenSpaceOverlay: //If we send back a camera when set to screen space overlay, the coordinates will not be accurate. If we return null, they will be. return null; case RenderMode.ScreenSpaceCamera: //If it's set to screen space we use the world Camera that the Canvas is using. //If it doesn't have one set, however, we have to send back the current camera. otherwise the coordinates will not be accurate. return (rootCanvas.worldCamera) ? rootCanvas.worldCamera : Camera.main; default: case RenderMode.WorldSpace: //World space always uses the current camera. return Camera.main; } } public IBoxSelectable[] GetAllSelected(){ if (selectables == null) { return new IBoxSelectable[0]; } var selectedList = new List(); foreach(var selectable in selectables) { if (selectable.selected) { selectedList.Add (selectable); } } return selectedList.ToArray(); } void EndSelection(){ //Get out if we haven't finished selecting, or if the selection has been aborted (boxRect disabled) if (!Input.GetMouseButtonUp(0) || !boxRect.gameObject.activeSelf) return; clickedAfterDrag = GetSelectableAtMousePosition(); ApplySingleClickDeselection(); ApplyPreSelections(); ResetBoxRect(); onSelectionChange.Invoke(GetAllSelected()); } void Start(){ ValidateCanvas(); CreateBoxRect(); ResetBoxRect(); } void Update() { BeginSelection (); DragSelection (); EndSelection (); } } }