Решил реализовать функционал по выделению сразу нескольких юнитов (как в 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. Мне нужно сделать выделение юнитов с помощью мыши
мне нужно выделять юнитов так: зажимается ЛКМ, проводится по карте, в процессе проведения, появляется четырехугольник. ЛКМ отпускается. Все юниты, которые попали под четырехугольник - выделяются
напиши код, который позволяет выбрать одного юнита, кликнув по нему, а также позволяет выбирать группу юнитов, с помощью выделения области мышью. Выделенная зона должна визуально отображаться.