Решил реализовать функционал по выделению сразу нескольких юнитов (как в Generals). Писал в ChatGPT, гуглил, смотрел YouTube, анализировал сторонние ассеты. <br> ChatGPT-4 и 3.5 себя показали очень плохо. Они не дали мне решения под ключ. Они писали код, но код не работал «из коробки» и любые мои уточнения делали их код только хуже. Я выделил все доступные варианты. ### Вариант 1 ##### В чем суть варианта? Реализации могут различаться, но подход один и тот же: 1. Берется область, которую выделили на экране. 2. Берутся все юниты, доступные на карте и конвертируются в экранные координаты. 3. Экранные координаты каждого юнита проверяются на нахождение внутри выделенной экранной области. ##### Какие есть нюансы у данного варианта? Требуется перебрать всех юнитов, доступных на карте, что неоптимально для производительности. ##### Мой комментарий Это самый распространенный и доступный вариант. В поисковой выдаче все статьи предлагали только этот вариант. Я успел просмотреть два ассета, и у них применен текущий вариант. - [ISOMETRIUM 2023 - RTS Pro Kit](https://assetstore.unity.com/packages/tools/game-toolkits/isometrium-2023-rts-pro-kit-34996) - [RTS Wheeled Vehicle Controller](https://assetstore.unity.com/packages/templates/systems/rts-wheeled-vehicle-controller-115945) В итоге я воспользовался решением из второго ассета. По началу ChatGPT-3.5 (четвертая версия жестко тормозила) предлагал данный вариант. Некоторые из предложенных однотипных решений ботом GPT-4: ```csharp using System.Collections.Generic; using UnityEngine; public class UnitSelection : MonoBehaviour { public LayerMask unitLayerMask; public RectTransform selectionBox; private Vector3 _startClickPosition; private List<Unit> _selectedUnits = new List<Unit>(); private void Update() { SelectUnits(); } private void SelectUnits() { // Одиночное выделение if (Input.GetMouseButtonDown(0)) { RaycastHit hit; Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); if (Physics.Raycast(ray, out hit, Mathf.Infinity, unitLayerMask)) { Unit unit = hit.collider.GetComponent<Unit>(); if (unit != null) { ClearSelectedUnits(); _selectedUnits.Add(unit); unit.ToggleSelection(true); } } _startClickPosition = Input.mousePosition; } // Множественное выделение if (Input.GetMouseButton(0)) { UpdateSelectionBox(Input.mousePosition); } if (Input.GetMouseButtonUp(0)) { if (selectionBox.gameObject.activeInHierarchy) { ClearSelectedUnits(); Collider[] hitColliders = Physics.OverlapBox(selectionBox.position, selectionBox.size / 2, Quaternion.identity, unitLayerMask); foreach (Collider col in hitColliders) { Unit unit = col.GetComponent<Unit>(); if (unit != null) { _selectedUnits.Add(unit); unit.ToggleSelection(true); } } selectionBox.gameObject.SetActive(false); } } } private void UpdateSelectionBox(Vector3 curMousePos) { if (!selectionBox.gameObject.activeInHierarchy) { selectionBox.gameObject.SetActive(true); } float width = curMousePos.x - _startClickPosition.x; float height = curMousePos.y - _startClickPosition.y; selectionBox.sizeDelta = new Vector2(Mathf.Abs(width), Mathf.Abs(height)); selectionBox.anchoredPosition = _startClickPosition + new Vector3(width / 2, height / 2); } private void ClearSelectedUnits() { foreach (Unit unit in _selectedUnits) { unit.ToggleSelection(false); } _selectedUnits.Clear(); } } ``` ```csharp private Vector3 mouseDownPos; // позиция мыши при зажатии ЛКМ private bool isSelecting; // выбор в процессе void Update() { if (Input.GetMouseButtonDown(0)) // ЛКМ нажата { mouseDownPos = Input.mousePosition; isSelecting = true; } else if (Input.GetMouseButtonUp(0)) // ЛКМ отпущена { isSelecting = false; List<GameObject> selectedUnits = GetUnitsInSelection(mouseDownPos, Input.mousePosition); foreach (GameObject unit in selectedUnits) { SelectUnit(unit); } } // отображение выделенной зоны if (isSelecting) { DrawSelectionBox(mouseDownPos, Input.mousePosition); } } List<GameObject> GetUnitsInSelection(Vector3 startPos, Vector3 endPos) { List<GameObject> selectedUnits = new List<GameObject>(); Rect selectionBox = GetSelectionBox(startPos, endPos); foreach (GameObject unit in allUnits) { if (selectionBox.Contains(Camera.main.WorldToScreenPoint(unit.transform.position))) { selectedUnits.Add(unit); } } return selectedUnits; } Rect GetSelectionBox(Vector3 startPos, Vector3 endPos) { Rect selectionBox = new Rect(); selectionBox.xMin = Mathf.Min(startPos.x, endPos.x); selectionBox.yMin = Mathf.Min(startPos.y, endPos.y); selectionBox.xMax = Mathf.Max(startPos.x, endPos.x); selectionBox.yMax = Mathf.Max(startPos.y, endPos.y); return selectionBox; } void DrawSelectionBox(Vector3 startPos, Vector3 endPos) { Rect selectionBox = GetSelectionBox(startPos, endPos); Texture2D selectionTexture = new Texture2D(1, 1); selectionTexture.SetPixel(0, 0, new Color(0.5f, 0.5f, 1f, 0.2f)); // прозрачный синий цвет selectionTexture.Apply(); GUI.DrawTexture(selectionBox, selectionTexture); } ``` ### Вариант 2 ##### В чем суть варианта? Выделенная область проецируется на поверхность земли. ##### Какие есть нюансы у данного варианта? Подход применим только, если в RTS-игре можно выделять только наземных юнитов. Летающих юнитов нельзя выделять при таком подходе ##### Мой комментарий В данном видео показывается, как реализовать данный вариант. [Unity RTS - Box Selection Tutorial](https://www.youtube.com/watch?v=OL1QgwaDsqo&t=757s) Видео от того же автора, который утверждает, что учел нюанс про летающих юнитов. Однако, я все равно считаю, что проблема актуальна и не решаема. [Unity RTS - Box Selection Tutorial (addendum)](https://www.youtube.com/watch?v=33RQEzFoFIM) ### Вариант 3 ##### В чем суть варианта? Использовать коллайдеры (colliders) - [Physics.OverlapBox](https://docs.unity3d.com/ScriptReference/Physics.OverlapBox.html) - [Physics.BoxCastAll](https://docs.unity3d.com/ScriptReference/Physics.BoxCastAll.html) ##### Нюансы? Предложенные варианты GPT-ботами фундаментально ошибочны.. Коллайдеры из Unity Engine можно было бы применить, если бы они умели работать с формой многоугольника, который образуется при проекции камерой на поверхность игры (или при выделении области, когда тоже образуется такой многоугольник) Некоторые из предложенных однотипных решений ботом GPT-3.5: ```csharp using UnityEngine; public class BoxSelection : MonoBehaviour { [SerializeField] private LayerMask selectableLayerMask; [SerializeField] private Transform selectionBoxTransform; [SerializeField] private RectTransform canvasRectTransform; private Vector3 startPosition; private bool isSelecting; private void Update() { if (Input.GetMouseButtonDown(0)) { startPosition = Input.mousePosition; isSelecting = true; } if (Input.GetMouseButtonUp(0)) { isSelecting = false; SelectUnits(); selectionBoxTransform.gameObject.SetActive(false); } if (isSelecting) { UpdateSelectionBox(Input.mousePosition); } } private void UpdateSelectionBox(Vector3 currentMousePosition) { if (!selectionBoxTransform.gameObject.activeInHierarchy) { selectionBoxTransform.gameObject.SetActive(true); } Vector3 screenPosition = Camera.main.WorldToScreenPoint(transform.position); float width = currentMousePosition.x - startPosition.x; float height = currentMousePosition.y - startPosition.y; Vector2 min = new Vector2(Mathf.Min(startPosition.x, currentMousePosition.x), Mathf.Min(startPosition.y, currentMousePosition.y)) - screenPosition; Vector2 max = new Vector2(Mathf.Max(startPosition.x, currentMousePosition.x), Mathf.Max(startPosition.y, currentMousePosition.y)) - screenPosition; selectionBoxTransform.position = min + (max - min) / 2f; selectionBoxTransform.sizeDelta = new Vector2(Mathf.Abs(width), Mathf.Abs(height)); } private void SelectUnits() { Vector2 minScreenPos = selectionBoxTransform.position - (selectionBoxTransform.sizeDelta / 2f); Vector2 maxScreenPos = selectionBoxTransform.position + (selectionBoxTransform.sizeDelta / 2f); Rect screenRect = new Rect(minScreenPos, maxScreenPos - minScreenPos); Collider[] hitColliders = Physics.OverlapBox(GetSelectionBoxWorldCenter(), GetSelectionBoxWorldSize() / 2f, Quaternion.identity, selectableLayerMask); foreach (Collider hitCollider in hitColliders) { GameObject unitObject = hitCollider.gameObject; if (IsWithinSelectionBounds(unitObject.transform.position)) { // Юнит находится внутри области выделения // Добавляем его в список выбранных юнитов или выполняем другие действия } } } private bool IsWithinSelectionBounds(Vector3 worldPosition) { Vector3 screenPosition = Camera.main.WorldToScreenPoint(worldPosition); return screenRect.Contains(screenPosition) && Vector3.Dot(transform.forward, (worldPosition - transform.position).normalized) > 0f; } private Vector3 GetSelectionBoxWorldCenter() { return Camera.main.ScreenToWorldPoint(selectionBoxTransform.position) + transform.forward * 10f; } private Vector3 GetSelectionBoxWorldSize() { return Camera.main.ScreenToWorldPoint(selectionBoxTransform.position + selectionBoxTransform.sizeDelta) - GetSelectionBoxWorldCenter(); } } ``` <br> <br> Как альтернатива, я спросил у бота "как найти объекты, которые видит камера?». Однако такое решение также фундаментально никак не может работать. <br> **Вопрос:** Как найти объекты, которые видит камеры? **Ответ:** Чтобы найти объекты, которые видит камера, вы можете использовать метод`Physics.Raycast` или `Physics.BoxCast`. В данном случае рассмотрим, как использовать `Physics.BoxCast` для определения видимых объектов: которые видит камеры: 1. Создайте прямоугольник, соответствующий видимому полю камеры. 2. Используйте метод `Physics.BoxCast` для определения видимых объектов. Вот пример кода, который определяет видимые объекты с помощью `Physics.BoxCast`: ```csharp private void FindVisibleObjects() { float cameraHeight = 2f * Camera.main.nearClipPlane * Mathf.Tan(Camera.main.fieldOfView * 0.5f * Mathf.Deg2Rad); float cameraWidth = cameraHeight * Camera.main.aspect; Vector3 origin = Camera.main.transform.position; Vector3 direction = Camera.main.transform.forward; Vector3 halfExtents = new Vector3(cameraWidth * 0.5f, cameraHeight * 0.5f, 0f); Quaternion orientation = Camera.main.transform.rotation; float maxDistance = Camera.main.farClipPlane - Camera.main.nearClipPlane; int layerMask = 1 << LayerMask.NameToLayer("YourLayerName"); // Замените "YourLayerName" на имя слоя, содержащего объекты, которые вы хотите найти RaycastHit[] hitInfos = Physics.BoxCastAll(origin, halfExtents, direction, orientation, maxDistance, layerMask); foreach (RaycastHit hitInfo in hitInfos) { Debug.Log("Visible Object: " + hitInfo.collider.gameObject.name); } } ``` Вызывайте метод `FindVisibleObjects` каждый раз, когда вам нужно определить видимые объекты. Обратите внимание, что вы должны заменить "YourLayerName" на имя слоя, содержащего объекты, которые вы хотите найти. <br> <br> [Google](https://www.google.com/search?q=rts+selection+box+unity) <br> <br> ##### Как я сформулировал запрос для GPT я разрабатываю RTS-игру на Unity. Мне нужно сделать выделение юнитов с помощью мыши мне нужно выделять юнитов так: зажимается ЛКМ, проводится по карте, в процессе проведения, появляется четырехугольник. ЛКМ отпускается. Все юниты, которые попали под четырехугольник - выделяются напиши код, который позволяет выбрать одного юнита, кликнув по нему, а также позволяет выбирать группу юнитов, с помощью выделения области мышью. Выделенная зона должна визуально отображаться.