Unity3d. Реализация зон видимости и слышимости

в 12:18, , рубрики: AI, C#, game development, Gamedev, unity3d

Добрый день, хабра.

Сегодня расскажу о том, как можно реализовать систему видимости и слышимости для ваших проектов. Получилось нечто схожее с игрой Commandos.
Немного скриншотов.
Unity3d. Реализация зон видимости и слышимости - 1
Больше видимости.
Unity3d. Реализация зон видимости и слышимости - 2

Я тебя вижу

Как можно увидеть объект? Вариантов можно перечислить множество. Встречаются варианты с видимостью по коллайдерами, разбором картинки через рендер текстуру, лучи по точкам и т.д. В данном примере рассмотрим вариант лучей по точкам.
Следовательно нам понадобится базовый класс с точками для системы видимости.

public abstract class UnitBase : GameObjectBase 
{
    public List<Transform> visiblePoints;
...
}

Теперь. Нам нужно сформулировать требования к системе видимости.
Очевидно у юнита(не важно что это, друг, враг или камера видео-наблюдения) должен быть угол обзора и дистанция на которой он различает объекты. Близорукие и дальнозоркие тоже могут быть. Да и персонаж может, например, одеть очки и видеть дальше. Может что-то принять и видеть чуть шире боковым зрением. Следовательно параметры нам нужны цель, глаза, дальность и угол обзора.

  public static bool IsVisibleUnit<T>(T unit, Transform from, float angle, float distance, LayerMask mask) where T : UnitBase
    {
        bool result = false;
        if (unit != null)
        {
            foreach (Transform visiblePoint in unit.visiblePoints)
            {
                if (IsVisibleObject(from, visiblePoint.position, unit.gameObject, angle, distance, mask))
                {
                    result = true;
                    break;
                }
            }
        }
        return result;
    }

LayerMask — нужен для лучей. Например на окно среагирует пуля но не зрение. Тоже самое с заборами или другими объектами. Следовательно, видимость игнорирует не нужные или прозрачные объекты. А спрятавшись в листве, можно обнулить список видимых точек. Также можно расширить или сузить список точек, в зависимости от логики самой игры. Например камуфляж или еще что-то.

  public static bool IsVisibleObject(Transform from, Vector3 point, GameObject target, float angle, float distance, LayerMask mask)
    {
        bool result = false;
        if (IsAvailablePoint(from, point, angle, distance))
        {
            Vector3 direction = (point - from.position);
            Ray ray = new Ray(from.position, direction);
            RaycastHit hit;
            if (Physics.Raycast(ray, out hit, distance, mask.value))
            {
                if (hit.collider.gameObject == target)
                {
                    result = true;
                }
            }
        }
        return result;
    }

Тут стоит обратить внимание на условие (hit.collider.gameObject == target). О том почему это так выглядит, будет понятно на примере использования. Системе отравляем желаемого юнита, и проверяем, реально ли мы можем его увидеть. Именно того которого хотим.

    public static bool IsAvailablePoint(Transform from, Vector3 point, float angle, float distance)
    {
        bool result = false;

        if (from != null && Vector3.Distance(from.position, point) <= distance)
        {
            Vector3 direction = (point - from.position);
            float dot = Vector3.Dot(from.forward, direction.normalized);
            if (dot < 1)
            {
                float angleRadians = Mathf.Acos(dot);
                float angleDeg = angleRadians * Mathf.Rad2Deg;
                result = (angleDeg <= angle);
            }
            else
            {
                result = true;
            }
        }
        return result;
    }

Тут не использовались хитрости с magnitude или преобразованием углов. Находим скалярное произведение. Если 1, значит точки направлены в одну сторону и угол можно опустить, иначе получаем угол и проверяем пределы.

А теперь я тебя еще и слышу

С системой слышимости все обстоит гораздо проще. На любой звук, мы перебираем всех допустимых юнитов (опустим как это реализовано. Главное что в текущей зоне или всей локации, есть какое-то количество юнитов) и передаем точку шума, ее радиус, и в данном случае еще и тип шума. Например на звук выстрела и звук падающего камня аи может реагировать по разному. Это зависит то того как вы это реализуете.

    public virtual void ApplyNoise(Vector3 target, float radius, NoiseType type)
    {
        List<AIHearlingBase> aiObjects = AIManager.Instance.GetAIObjects<AIHearlingBase>();
        foreach (AIHearlingBase ai in aiObjects)
        { 
            if(Vector3.Distance(ai.unit.Position, target) <= ai.hearingRadius + radius)
            {
                ai.ApplyHearling(target, type);
            }
        }
    }

Система слышимости готова. Идем дальше.

Пример использования системы видимости

Ну а как-же без примера.
Нам понадобится метод получения доступных юнитов. Который по сути и будет точкой входа.

    public virtual List<T> GetVisibleUnits<T>(Comparer<T> comparer) where T : UnitBase
    {
        List<T> result = new List<T>();
        foreach (T unit in UnitsManager.Instance.GetUnits<T>())
        {
            if (unit != null && unit != this.unit && unit.enabled && comparer(unit) && IsVisibleUnit(unit))
            {
                result.Add(unit);
            }
        }
        return result;
    }

    public virtual bool IsVisibleUnit<T>(T unit) where T : UnitBase
    {
        bool result = ViewUtility.IsVisibleUnit(unit, CurrentEyes, visibleAngle, visibleDistance, visibleMask);
// SENSORS        
if (!result)
        {

            foreach (AISensorBase sensor in sensors)
            {
                if (sensor != null)
                {
                    if (sensor.DetectTarget<T>(unit))
                    {
                        result = true;
                        break;
                    }
                }
            }
        }
// END SENSORS
        return result;
    }

GetVisibleUnits — метод который вернет вам всех видимых юнитов, а дальше уже ваше непосредственная логика, что с этим делать.
Стоит обратить внимание. В этом примере есть глаза и возможные сенсоры. Сенсоры могут работать как угодно, например получение данных со спутника или с камер наблюдения, по запаху или еще как-нибудь. Мы их не рассматриваем в данном примере.

Дополнительно

Для существующей системы, дополнительно генерирую меш. Чтобы как в коммандос можно было определить, куда же смотрит противник. Ниже представлен код генерации меша (Повторять топологию местности задача вынесена за рамки текущей).

public class FragmentMeshCreator : MeshCreatorBase 
{
    public virtual void Create(float angle, float distance, float step = 10f)
    {
        List<Vector3> vertices = new List<Vector3>();
        List<int> triangles = new List<int>();
        List<Vector2> uvs = new List<Vector2>();

        Vector3 right = ViewUtility.GetRotation(Vector3.forward, angle) * distance;
        Vector3 left = ViewUtility.GetRotation(Vector3.forward, angle) * distance;
        Vector3 from = left;

        vertices.Add(Vector3.zero);
        vertices.Add(from);
        uvs.Add(Vector2.one * 0.5f);
        uvs.Add(Vector2.one);
        int triangleIdx = 3;

        for (float angleStep = -angle; angleStep < angle; angleStep += step)
        {
            Vector3 to = ViewUtility.GetRotation(Vector3.forward, angleStep) * distance; // метод ниже
            from = to;
            vertices.Add(from);
            uvs.Add(Vector2.one);
            triangles.Add(triangleIdx - 1);
            triangles.Add(triangleIdx);
            triangles.Add(0);

            triangleIdx++;
        }
        vertices.Add(right);

        uvs.Add(Vector2.one);

        Mesh mesh = new Mesh();
        mesh.name = "FragmentArea";
        mesh.vertices = vertices.ToArray();
        mesh.triangles = triangles.ToArray();
        mesh.uv = uvs.ToArray();
        mesh.RecalculateNormals();
        myMeshFilter.mesh = mesh;
    }
}
// 
    public static Vector3 GetRotation(Vector3 forward, float angle)
    {
        float rad = angle * Mathf.Deg2Rad;
        Vector3 result = new Vector3(forward.x * Mathf.Cos(rad) + forward.z * Mathf.Sin(rad), 0,
                                        forward.z * Mathf.Cos(rad) - forward.x * Mathf.Sin(rad));
        return result;
    }

На деталях кода останавливаться не будем. Тут представлен обычный алгоритм генерации меша.
Подходим к концу.
Некоторые вещи не рассмотрены (Сенсоры или гизмо в редакторе) но вам не составит труда их реализовать отдельно. Надеюсь, данная статья вам помогла или подкинула пару идей.

Ссылки

Unity scripting

Автор: derek_streyt

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js