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

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

Больше видимости.



Я тебя вижу


Как можно увидеть объект? Вариантов можно перечислить множество. Встречаются варианты с видимостью по коллайдерами, разбором картинки через рендер текстуру, лучи по точкам и т.д. В данном примере рассмотрим вариант лучей по точкам.
Следовательно нам понадобится базовый класс с точками для системы видимости.
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

Комментарии (5)


  1. xGromMx
    21.04.2015 15:58
    +2

    Есть прекрасная штука rivaltheory.com/rain. И что интересно она бесплатна.


    1. derek_streyt Автор
      22.04.2015 08:24

      Подозреваю вы не пользовались этой системой. Просто фрагмент кода из Rain. Доставал рефлектором

      Rain vision
      protected virtual bool TestVisibility(RAINAspect aAspect, float aSqrRange, Matrix4x4 aSensorSpace)
      {
          Vector3 position = aAspect.Position;
          Vector3 vector4 = position - this.Position;
          if (vector4.sqrMagnitude > aSqrRange)
          {
              return false;
          }
          Transform transform = aAspect.Entity.Form.transform;
          if (aAspect.MountPoint != null)
          {
              transform = aAspect.MountPoint.transform;
          }
          if ((!this.CanDetectSelf && (transform != null)) && ((transform == this.AI.Body.transform) || transform.IsChildOf(this.AI.Body.transform)))
          {
              return false;
          }
          bool flag = true;
          if ((this.HorizontalAngle == 360f) && (this.VerticalAngle == 360f))
          {
              flag = true;
          }
          else
          {
              position = aSensorSpace.MultiplyPoint(position);
              Vector3 vector5 = new Vector3(position.x, 0f, position.z);
              Vector3 normalized = vector5.normalized;
              float num = Mathf.Acos(Mathf.Clamp(normalized.z, -1f, 1f)) * 57.29578f;
              if (num > 180f)
              {
                  num = 360f - num;
              }
              if (!Mathf.Approximately(normalized.sqrMagnitude, 0f) && (num > (this.HorizontalAngle / 2f)))
              {
                  flag = false;
              }
              else
              {
                  Vector3 vector6 = new Vector3(0f, position.y, position.z);
                  normalized = vector6.normalized;
                  float num2 = Mathf.Acos(Mathf.Clamp(normalized.z, -1f, 1f)) * 57.29578f;
                  if (num2 > 180f)
                  {
                      num2 = 360f - num2;
                  }
                  if (num2 > 90f)
                  {
                      num2 = 180f - num2;
                  }
                  if (!Mathf.Approximately(normalized.sqrMagnitude, 0f) && (num2 > (this.VerticalAngle / 2f)))
                  {
                      flag = false;
                  }
              }
          }
          if (!flag)
          {
              return false;
          }
          if (this._lineOfSight)
          {
              Vector3 direction = aAspect.Position - this.Position;
              RaycastHit[] hitArray = Physics.RaycastAll(this.Position, direction, direction.magnitude, (int) this._lineOfSightMask);
              for (int i = 0; i < hitArray.Length; i++)
              {
                  if ((hitArray[i].collider.isTrigger || (hitArray[i].transform == this.AI.Body.transform)) || hitArray[i].transform.IsChildOf(this.AI.Body.transform))
                  {
                      continue;
                  }
                  if (aAspect.MountPoint != null)
                  {
                      if ((hitArray[i].transform != aAspect.MountPoint.transform) && !hitArray[i].transform.IsChildOf(aAspect.MountPoint.transform))
                      {
                          goto Label_0331;
                      }
                      continue;
                  }
                  if ((aAspect.Entity.Form != null) && ((hitArray[i].transform == aAspect.Entity.Form.transform) || hitArray[i].transform.IsChildOf(aAspect.Entity.Form.transform)))
                  {
                      continue;
                  }
              Label_0331:
                  return false;
              }
          }
          return true;
      }
      

      Стоит обратить внимание на raycastAll и то что он всегда стреляет в одну точку.


  1. Disen
    22.04.2015 07:01

    Ваши скриншоты сильно напомнили Jagged Alliance :)


    1. optimizer
      29.04.2015 13:47

      мне тоже, и честно говоря, я из-за этого и открыл статью, посмотреть что за игра


  1. Idot
    22.04.2015 18:14

    А почему не учитывается, что звук плохо проходит сквозь стены? (то есть сквозь толстую стену не слишком громкий звук не пройдёт) Кроме того, не учитывается уровень общей зашумлённости, то есть если имеются источники шума, то тихи звук можно будет не заметить.