Unity C# Поиск объекта в радиусе

Всем привет. Есть необходимость при нажатии на кнопку удара, проверять, есть ли в радиусе поражения противник, и если есть - повернуть к нему персонажа и произвести удар, а если в радиусе поражения никого нет - не производить удар.

Собственно, из всего этого вопросы вызывает только поиск противника в радиусе поражения.

Сейчас реализовал перебор всех противников через цикл. Выбираю самого ближнего из них и проверяю расстояние до игрока.

Вот код:

private GameObject FindClosestEnemy(float maxDistance)
    {
        GameObject closest = null;
        List<GameObject> enemyes = new List<GameObject>(GameObject.FindGameObjectsWithTag("Enemy"));
        
        float distance = Mathf.Infinity;
        float curDistance;
        if (enemyes.Count > 0)
        {
            foreach (GameObject go in enemyes)
            {
                Vector3 diff = go.transform.position - transform.position;
                curDistance = diff.sqrMagnitude;
                if (curDistance < distance)
                {
                    closest = go;
                    distance = curDistance;
                }
            }
            if (distance <= maxDistance) return closest;
            else return null;
        }
        else return null;
    }

Я бы так и оставил, но я переживаю за производительность, если противников на сцене будет 100+. Причем игра "каждый сам за себя", противники тоже будут атаковать и каждый из них будет перебирать 100+ объектов…

Какой самый оптимальный способ решения этой задачи?


Ответы (2 шт):

Автор решения: Yaroslav

Самый экономичный способ скорее всего будет создать два Rect с зонами поражения атаки и перебором через Rect.Overlaps проверить с Rect телами врагов.

Rect.Overlaps это очень простые вычисления, буквально + - > <, в отличие от Vector.Distance(magnitude), который есть квадратный корень из разниц квадратов осей.

public class Unit : MonoBehaviour {

    public Rect Body {
        get {
            Rect BodyRect = new Rect(Vector2.zero, _bodyRectSize);
            BodyRect.center = (Vector2)_transform.localPosition;
            return BodyRect;
        }
    }
    public int Fraction {
        get { return _fraction; }
    }

    [SerializeField] private Vector2 _bodyRectSize = new Vector2(10, 20);
    [SerializeField] private Vector2 _attackRectSize = new Vector2(10, 10);
    [SerializeField] private int _fraction; // Фракция
    private Transform _transform;


    public bool Attack () {
        // Зоны атаки справо и слево
        Rect SelfBody = Body;
        Vector2 Offset = new Vector2((SelfBody.width+_attackRectSize.x)*0.5f, 0);
        Rect RightZone = new Rect(Vector2.zero, _attackRectSize);
        RightZone.center = (Vector2)_transform.localPosition+Offset;
        Rect LeftZone = new Rect(Vector2.zero, _attackRectSize);
        LeftZone.center = (Vector2)_transform.localPosition-Offset;
        // Перебор
        List<Unit> RightTargets = new List<Unit>();
        List<Unit> LeftTargets = new List<Unit>();
        foreach (Unit unit in UnitManager.Units) {
            if (unit != this && _fraction != unit.Fraction) {
                Rect TargetBody = unit.Body;
                if (RightZone.Overlaps(TargetBody))
                    RightTargets.Add(unit);
                if (RightZone.Overlaps(TargetBody))
                    LeftTargets.Add(unit);
            }
        }
        if (RightTargets.Count == 0 && LeftTargets.Count == 0)
            // нет целей
            return false;
        else {
            // есть цели
            // ... [раелизация атаки]
            return true;
        }
    }


    private void Awake () {
        _transform = transform;
        UnitManager.Units.Add(this); // некий статический класс или синглтон
    }

    private void OnDestroy () {
        UnitManager.Units.Remove(this);
    }
}
→ Ссылка
Автор решения: Delmas

Вот решение проблемы, этот скрипт помогает искать компоненты в радиусе, а также самый ближний и самый дальний из них.

ComponentSearcher.cs:

using System;
using System.Collections.Generic;
using UnityEngine;

public static class ComponentSearcher<T> where T : Component
{
    public static T Closest(Vector3 pos, List<T> components) => Search(pos, components, Desired.Closest);
    public static T Closest(Vector3 pos, float radius) => Search(pos, radius, Desired.Closest);

    public static T Furthest(Vector3 pos, List<T> components) => Search(pos, components, Desired.Furthest);
    public static T Furthest(Vector3 pos, float radius) => Search(pos, radius, Desired.Furthest);

    private static T Search(Vector3 pos, List<T> components, Desired type)
    {
        if (components == null)
            throw new NullReferenceException("Components is null, cant search");

        float maxDistanceToComponent;
        if (type == Desired.Closest)
            maxDistanceToComponent = Mathf.Infinity;
        else
            maxDistanceToComponent = Mathf.NegativeInfinity;

        T component = null;
        for (int i = 0; i < components.Count; i++)
        {
            float distanceToComponent = Vector3.Distance(components[i].transform.position, pos);

            if (type == Desired.Closest)
            {
                if (distanceToComponent < maxDistanceToComponent)
                {
                    maxDistanceToComponent = distanceToComponent;
                    component = components[i];
                }
            }
            else
            {
                if (distanceToComponent > maxDistanceToComponent)
                {
                    maxDistanceToComponent = distanceToComponent;
                    component = components[i];
                }
            }
        }

        return component;
    }
    private static T Search(Vector3 pos, float radius, Desired type)
    {
        InRadius(pos, radius, out List<T> components);
        return Search(pos, components, type);
    }

    public static int InRadius(Vector3 pos, float radius, out List<T> results, bool findOnlyEnabled = true)
    {
        Collider[] colliders = Physics.OverlapSphere(pos, radius);

        results = new List<T>();
        for (int i = 0; i < colliders.Length; i++)
        {
            if (colliders[i].TryGetComponent(out T component))
            {
                if (findOnlyEnabled)
                {
                    Behaviour behaviour = component as Behaviour;

                    if (behaviour)
                    {
                        if (behaviour.enabled)
                            results.Add(component);
                    }
                    else
                    {
                        findOnlyEnabled = false;
                        results.Add(component);
                    }
                }
                else
                {
                    results.Add(component);
                }
            }
        }

        return results.Count;
    }

    private enum Desired { Closest, Furthest }
}

пример использования:

using System.Collections.Generic;
using UnityEngine;

public class ExampleUsage : MonoBehaviour
{
    private List<MyComponentType> components;

    private void Start()
    {
        // Populate the 'components' list with your MyComponentType components
    }

    private void Update()
    {
        Vector3 currentPosition = transform.position;

        MyComponentType closestComponent = ComponentSearcher<MyComponentType>.Closest(currentPosition, components);
        MyComponentType furthestComponent = ComponentSearcher<MyComponentType>.Furthest(currentPosition, components);

        float searchRadius = 10f;
        List<MyComponentType> componentsInRange = new List<MyComponentType>();
        ComponentSearcher<MyComponentType>.InRadius(currentPosition, searchRadius, out componentsInRange);

        // Now you can interact with the found components
    }
}
→ Ссылка