Unity3D → Стрельба по движущимся целям

В предыдущей статье была рассмотрена турель, которая автоматически наводится на цель, попавшую в радиус обзора этой турели. Это прекрасно работает, но что, если данная турель должна еще и стрелять по цели. Снаряд выпущенный в центр движущейся цели никогда её не достигнет. Для того, чтобы снаряд и цель «встретились», турели необходимо производить выстрелы на опережение, то есть точка, в которую выпущен снаряд, должна находится перед движущейся целью. В данной статье рассматриваются доработки, которые позволяют вести стрельбу по движущимся мишеням.

Чтобы определить точку перед целью, нужно знать скорость снаряда, скорость движения цели и расстояние от турели до цели. Со скоростью снаряда все просто - это может быть число, которое просто задается в скрипте, отвечающем за движение снаряда. В данном примере рассматривается снаряд, который летит по прямой с постоянной скоростью. Определить расстояние между турелью и целью тоже просто. Чтобы определить скорость движущейся цели, есть несколько вариантов:

  1. Если объект имеет компонент Rigidbody, то Rigidbody.velocity будет искомой скоростью;
  2. Если движение объекта определяется каким-то запрограммированным алгоритмом, то скорость в любом случае должна быть известна на любом этапе движения;
  3. Движение объекта анимировано. Для демонстрации работы турели мне показалось проще использовать этот вариант. Объект просто движется по цикличной траектории, меняя вектор скорости.

Посчитать скорость объекта, вектор движения которого может меняться, очень просто, для этого нужно знать текущее и предыдущее положение объекта и время, за которое объект сменил свое положение:

  1. public virtual void Update() {
  2. previousTargetPosition = target.position;
  3. }
  4.  
  5. public virtual void LateUpdate() {
  6. targetSpeed = (target.position - previousTargetPosition) / Time.deltaTime;
  7. }

В принципе, этот способ расчета вектора скорости универсальный и не зависит от того,  каким образом осуществляется движение объекта и есть или нет компонент Rigidbody.

Итак, начальные данные есть, теперь нужно найти точку впереди цели, по которой нужно произвести выстрел, чтобы снаряд и цель встретились. Единственное адекватное решение, которое я смог придумать, это высчитать время, за которое снаряд пролетит текущее расстояние до цели. Затем к текущей позиции цели добавить произведение скорости цели на это время (когда речь идет о скорости подразумевается вектор скорости). Но есть один нюанс - время, за которое снаряд достигнет текущее положение, не равно времени, за которое снаряд достигнет следующее положение цели. То есть, если цель не движется по кругу, в центре которого стоит турель, а отдаляется или приближается к турели, то высока вероятность того, что снаряд пролетит мимо цели (зависит, конечно, от размеров снаряда  и цели, расстояния между ними). Чтобы минимизировать эту вероятность, сведя её практически к нулю, нужно произвести данный расчет несколько раз, каждый раз используя в качестве текущего положения цели, положение полученное при предыдущем расчете:

  1. protected virtual Vector3 CalculateAim() {
  2. //По умолчанию турель стреляет прямо по цели, но, если цель движется, то нужно высчитать точку,
  3. //которая находится перед движущейся целью и по которой будет стрелять турель.
  4. //То есть турель должна стрелять на опережение
  5. targetingPosition = target.position;
  6.  
  7. //Высчитываем точку, перед мишенью, по которой нужно произвести выстрел, чтобы попасть по движущейся мишени
  8. //по идее, чем больше итераций, тем точнее будет положение точки для упреждающего выстрела
  9. for (int i = 0; i < 10; i++) {
  10. float dist = (turretGun.position - targetingPosition).magnitude;
  11. float timeToTarget = dist / projectileSpeed;
  12. targetingPosition = target.position + targetSpeed * timeToTarget;
  13. }
  14.  
  15. return targetingPosition;
  16. }

Вот и все, пример и весь код с комментариями можно найти на Github.

Комментарии

Замечательный и очень изящный код, можно я буду им пользоваться?
И как насчёт баллистической стрельбы? Заодно добавив для реалистичности массу снаряду или мине и импульс выстрела.

Конечно, можно. Пока с баллистической стрельбой готового решения предложить не могу, но, на первый взгляд, кажется не сложным, снаряд должен иметь rigidbody, а движение задаваться с помощью AddRelativeForce.

Ещё момент, как бы туда добавить ограничение вращения ствола вниз и вверх.
Вниз особенно актуально.

В методе Atack можно поставить условие. Высчитать угол между прямой от точки вращения оружия до цели и вертикальной осью, и задать нужный диапазон, например:

  1. float angel = Vector3.Angle(targetingPosition - turretGun.position, Vector3.up );
  2.  
  3. if ((angel <= 90 && angel >= 25)) {
  4. turretGun.rotation = Quaternion.Slerp (
  5. turretGun.rotation,
  6. rotateQuaternionGun,
  7. Mathf.Min (1f, Time.deltaTime * rotationSpeed / angleGun)
  8. );
  9. }

В данном случае дуло орудия будет вращаться от 25 до 90 градусов по отношению к Vector3.up (вертикальная ось). Так же нужно учесть, что выстрел должен производиться, когда орудие наведено на цель, как будет свободное время - доработаю скрипт, но сделать тоже не сложно, принцип такой же.

Вот турель сделанная с применением вашего кода. Есть недостатки но в целом практически замечательно :)
https://www.youtube.com/watch?v=4TlOp4bQH9U

Круто! :) Можно доработать, чтобы турель начинала стрелять только тогда, когда наведена на цель. Позже могу скинуть кусок кода, если интересно.

Да вот про это я и хотел сказать, а ещё чтобы она не моталась туда сюда а отстреливала всё подряд. А то её сильно мотает, и ещё проблема, вниз если вставить ограничитель, не опускается ствол, ну ниже горизонта.

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

    https://youtu.be/2VV5NxeJgCI

    Привет, выглядит круто, можешь расшарить код?

    Привет, ничего серьёзного
    тут где выбор ближайшей цели для атаки заменил
    на такой код

    1. angle1 = Vector3.Angle(turretGun.forward, Vector3.Normalize(col.transform.position - turretGun.position));
    2. if (angle1 < angle2) {
    3. angle2 = angle1;
    4. closest = col.transform;
    5. }

    а чтоб не стрелял пока мимо цели использую рэйкаст, но там сильно замуть на своё и лучше реализовывать для себя отдельно, главное принцип

    Привет.
    Пришлось доработать код турельки в месте наведения по вертикали, потому как не правильно работало при наклонах и поворотах основания на котором располагалась оная.

    1. /*********** Дополнительная переменная - Transform m_base это трансформ основания на котором закреплена башня ***************/
    2. Transform m_base = transform;
    3. // Компенсируем поворот основания
    4. directionTurretToTarget = Quaternion.Inverse(m_base.rotation) * directionTurretToTarget;
    5.  
    6. //Вращение идет вокруг оси Y, поэтому вектор направления между целью и башней турели
    7. //должен находится в горизонтальной плоскости
    8. directionTurretToTarget.y = 0;
    9. Quaternion rotateQuaternion = Quaternion.LookRotation(directionTurretToTarget);
    10. //Для вращения используется Quaternion.Slerp, 3-ий параметр, которой лежит в промежутке [0,1] включительно.
    11. //Чтобы вращение происходило с одинайковой скоростью, нужно расчитать значение,
    12. //на которое надо поворачивать турель каждый кадр.
    13. //Получаем угол, на который должна повернуться башня
    14. float angle = Quaternion.Angle(turretHead.localRotation, rotateQuaternion);
    15. //высчитываем на сколько должна провернуться башня в течение одного кадра
    16. turretHead.localRotation = Quaternion.Slerp(turretHead.localRotation, rotateQuaternion, Mathf.Min(1f, Time.deltaTime * rotationSpeed / angle));
    17.  
    18. //наведение пушки на цель
    19. float d = Vector3.Distance(targetingPosition, turretGun.position);
    20. //Находим направление от точки вращения пушки к точке, на высоте которой находится цель
    21. //минус высота, на которой находится turretGun, иначе турель будет стрелять выше цели
    22. Vector3 directionToTarget = new Vector3(turretGun.forward.x, 0, turretGun.forward.z) * d
    23. + new Vector3(0, targetingPosition.y, 0)
    24. - new Vector3(0, turretGun.position.y, 0);
    25.  
    26. // Компенсируем поворот башни
    27. directionToTarget = Quaternion.Inverse(turretHead.rotation) * directionToTarget;
    28. //Вращение идет вокруг оси X, поэтому вектор направления между целью и башней турели
    29. //должен находится в горизонтальной плоскости
    30. directionToTarget.x = 0;
    31.  
    32. Quaternion rotateQuaternionGun = Quaternion.LookRotation(directionToTarget);
    33.  
    34. float angleGun = Quaternion.Angle(turretGun.localRotation, rotateQuaternionGun);
    35. //Для вращения используется Quaternion.Slerp, 3-ий параметр, которой лежит в промежутке [0,1] включительно.
    36. //Чтобы вращение происходило с одинаковой скоростью, нужно расчитать значение,
    37. //на которое надо поворачивать турель каждый кадр.
    38. //Получаем угол, на который должна повернуться ствол
    39. turretGun.localRotation = Quaternion.Slerp(
    40. turretGun.localRotation,
    41. rotateQuaternionGun,
    42. Mathf.Min(1f, Time.deltaTime * rotationSpeed / angleGun)
    43. );
    44.  
    45. //Выстрел
    46. Shot();

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

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

    Добавить комментарий