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

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

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

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

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

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