Привет!
Меня зовут Аристарх, я веду занятия по разработке на Unity и буду вашим ментором.
Рад, что вы приступаете к разработке прототипа шутера на интенсиве Практик•ON.
При возникновении вопросов по Unity, пишите мне в чате в ветке Unity с тегом @Kerioth.
Вступайте в чат Практик•ON для прохождения интенсива
В предыдущем уроке мы разобрали основы стрельбы, теперь давайте улучшим опыт игрока добавив сопутствующие элементы стрельбы, такие как прицел, механика более точного прицеливания и реакция на наличие цели.
И в первую очередь добавим прицел.
В самом простом виде, прицел представляет собой небольшое изображение расположенное по центру экрана.
Для создания изображение нажмите ПКМ в окне иерархии:
Create > UI > Image
Как только вы создадите изображение (или любой другой элемент UI) он появится сразу внутри объекта холст (Canvas). Холст работает как контейнер для UI элементов, определяя их положение о размеры относительно настроек холста.
Изображение будет сразу привязано к центру холста, а сам холст растянут на весь экран (в окне Game). Но вы можете изучить как работают якоря привязки нажав на квадрат в компоненте Rect Transform.
Для того, чтобы поставить своё изображение прицела, поместите спрайт в поле Source Image компонента Image.
Добавим на экран отображение количества патронов. Для этого создадим текстовый элемент и привяжем его к углу экрана
Create > UI > Text - TextMeshPro
TextMeshPro -– это альтернатива обычному текстовому компоненту, которая лишена проблемы разной кодировки шрифтов на разных устройствах, поскольку хранит шрифт в виде меша. Чтобы использовать на этом компоненте свой шрифт, необходимо создать на его основе FontAsset (ПКМ на шрифт > Create > TextMeshPro> FontAsset > SDF), который затем указывается в текстовом компоненте.
Через этот элемент мы будем выводить количество патронов и менять значение при каждом изменении количества патронов. Первым делом создадим событие об изменении количества патронов и расставим вызов этого события в местах изменения количества патронов.
Добавим в скрипт оружия (WeaponController) новое поле
C#// событие об изменении количества снарядов
// с событием передаем тип int – количество патронов
public event Action<int> OnAmmoChanged;В конце метода стрельбы, сделайте вызов события с измененным количеством патронов
C#public void Shoot()
{
currentAmmo--;
// Создаём снаряд
Projectile bullet = Instantiate(projectilePrefab, firePoint.position, firePoint.rotation);
bullet.Initialize(projectileSpeed);
// вызываем событие изменения количества патронов с текущим значением патронов
OnAmmoChanged?.Invoke(currentAmmo);
}Не забудьте аналогично вызвать событие (OnAmmoChanged?.Invoke(currentAmmo)) в конце метода перезарядки (Reload)
Изменим скрипт WeaponManager, чтобы подписываться на событие изменения патронов выбранного оружия и менять содержание текстового компонента.
Добавим поле для компонента TMP_Text и переменную CurrentWeapon которая будет ссылаться на текущий контроллер оружия.
C#public TMP_Text ammoText;
private WeaponController CurrentWeapon => weapons[currentWeaponIndex];Добавим метод установки текстового значения
C#// выставляем текст с нужным значением
private void SetAmmoText(int newAmmo)
{
ammoText.text = newAmmo.ToString();
}Поменяем методы смены оружия и выбора оружия, чтобы текст менялся под выбранное оружие
C#// Метод смены оружия изменен так,
// чтобы currentWeaponIndex менялся только в методе SelectWeapon
private void SwitchWeapon(int direction)
{
//меняем индекс
int newWeaponIndex = currentWeaponIndex;
newWeaponIndex += direction;
// если индекс выходит за предел вниз
if (newWeaponIndex < 0)
newWeaponIndex = weapons.Count - 1;
// если индекс выходит за предел вверх
if (newWeaponIndex >= weapons.Count)
newWeaponIndex = 0;
// выбираем оружие
SelectWeapon(newWeaponIndex);
}
// Измененный метод выбора оружия
public void SelectWeapon(int index)
{
// отписка от события у предыдущего оружия
CurrentWeapon.OnAmmoChanged -= SetAmmoText;
// меняем индекс текущего оружия
currentWeaponIndex = index;
for (int i = 0; i < weapons.Count; i++)
{
weapons[i].gameObject.SetActive(i == index);
}
// подписка на событие у текущего оружия
CurrentWeapon.OnAmmoChanged += SetAmmoText;
// выставляем количество патронов текущего оружия
SetAmmoText(CurrentWeapon.currentAmmo);
}После всех изменений не забудьте заполнить новые поля в редакторе движка.
Проверьте как меняется текстовое значение при стрельбе, перезарядке и т.д.
Частая проблема, когда оружие не имеет своего коллайдера, чтобы лишний раз не просчитывать физику, а персонаж не зацеплялся им за случайные объекты, но из-за этого проходит сквозь стены, когда игрок вплотную к ним подходит.
Такая проблема решается отдельной камерой для отрисовки оружия. Мы добавим отдельную камеру, которая будет видеть только оружие и отрисовывать его поверх того, что видит основная камера.
Первым делом создайте новый физический слой (Layer) и назначьте его объекту оружия
Создайте камеру внутри основной камеры, ПКМ на объект камеры:
Create > Camera
Настройте камере для оружия тип Overlay и укажите в Culling Mask только слой для оружия.
В основной камере наоборот – исключите из слоев для отрисовки слой с оружием.
Так же добавьте в Stack вложенную камеру (нажмите +)
Добавим прицел с приближением, чтобы игрок имел возможность для более точной стрельбы
Для этого будем перемещать оружие между двумя положениями:
Создайте внутри основной камеры два пустых объекта (можете сделать 3D объекты без физики или добавить объектам иконку, чтобы видеть их расположение). Установите положение объекта DefaultPos в соответсвии с положением вашего оружия (в районе правой руки), а положение AimPos по середине перед камерой, чуть выше DefaultPos.
Перемещение будет происходить по зажатию правой кнопки мыши.
Создайте экшен ввода для прицела и добавьте привязку к правой кнопке мыши.
Перейдем к логике перемещения оружия между двумя точками по зажатой ПКМ
В контроллер оружия (WeaponController) добавим настройки для прицеливания
C#[Header("Aiming")]
public Vector3 aimOffset; // отступ расположения оружия при прицеливании
public float aimZoomRatio = 0.5f; // коэффициен увеличения при прицеливанииВ менеджере оружия (WeaponManager) добавим поля, необходимые для механики прицеливания
C#[Header("Aiming settings")]
public InputActionReference Aim; // экшен ввода для прицеливания
public Camera playerCamera; // основная камера
public Camera weaponCamera; // камера оружия
public Transform defaultWeaponPosition; // обычная позиция оружия
public Transform aimWeaponPosition; // позиция оружия при прицеливании
public float defaultFov; // обычный угол обзора
public float aimAnimationSpeed = 10f; // скорость изменения положенияОбычный угол обзора можно получить на старте с основной камеры, но вы можете обойтись и без этого, задав его вручную
C#private void Start()
{
defaultFov = playerCamera.fieldOfView;
}Так же добавим метод обработки прицеливания HandleAim, который будем вызывать в Update и метод установки нового угла обзора SetFieldOfView, который используется в HandleAim
C#// Обработка прицеливания
private void HandleAim(){
// Если происходит прицеливание
if (Aim.action.IsPressed())
{
// Меняем позицию оружия на aimWeaponPosition с отступом оружия, если он задан
CurrentWeapon.transform.localPosition = Vector3.Lerp(
CurrentWeapon.transform.localPosition,
aimWeaponPosition.localPosition + CurrentWeapon.aimOffset,
aimAnimationSpeed * Time.deltaTime);
// Меняем угол обзора в зависимости от настройки оружия
SetFieldOfView(Mathf.Lerp(playerCamera.fieldOfView,
CurrentWeapon.aimZoomRatio * defaultFov, aimAnimationSpeed * Time.deltaTime));
}
// Если нет прицеливания
else
{
// Меняем позицию оружия на defaultWeaponPosition
CurrentWeapon.transform.localPosition = Vector3.Lerp(
CurrentWeapon.transform.localPosition,
defaultWeaponPosition.localPosition,
aimAnimationSpeed * Time.deltaTime);
// Меняем угол обзора на обычный
SetFieldOfView(Mathf.Lerp(playerCamera.fieldOfView, defaultFov,
aimAnimationSpeed * Time.deltaTime));
}
}
// Установка угла обзора для обоих камер
void SetFieldOfView(float fov){
playerCamera.fieldOfView = fov;
weaponCamera.fieldOfView = fov;
}Не забудьте добавить HandleAim в Update, а так же заполнить все поля в редакторе, прежде чем проверить как работает прицеливание. При необходимости, можете поправить положение AimPos или DefaultPos, поменять коэффицент увеличения aimZoomRatio и другие настройки.
В шутерах часто используют метод для определения попадания снаряда и адаптивного направления стрельбы, в зависимости от того, как далеко находится объект, на который направлен прицел.
Давайте добавим проверку, целится ли сейчас игрок во врага, сделаем это через Raycast.
Raycast (рэйкаст) – это луч, который распространяется из точки в направлении и сообщает, в какой коллайдер он попал.
Реализуем следующую логику:
1. Игрок целится – каждый кадр выполняется рэйкаст вперёд из камеры.
2. Если луч попал во врага:
3. При выстреле:
Для этого мы изменим скрипты менеджера оружия, контроллера оружия и пули:
1. Контроллер оружия будет пускать луч из камеры и менять прицел, а так же вычислять точку попадания и передавать направление пуле
2. Пуля, помимо скорости, так же будет получать направление своего движения от контроллера
4. Менеджер оружия будет передавать контроллеру всё необходимое (камеру и прицел)
Но перед этим добавим скрипт для изменения прицела:
C#using UnityEngine;
using UnityEngine.UI;
public class CrosshairController : MonoBehaviour
{
public Image crosshairImage;
public Color normalColor = Color.white;
public Color enemyColor = Color.red;
public void SetHighlighted(bool highlighted)
{
crosshairImage.color = highlighted ? enemyColor : normalColor;
}
}Его можно повесить на сам прицел или холст, главное укажите в редакторе изображение, которое надо менять.
Перейдем к изменениям уже существующих скриптов.
Начнем с изменений в пуле (Projectile), там поменяется только метод инициализации:
C#public void Initialize(float speed, Vector3 direction)
{
velocity = direction * speed;
Destroy(gameObject, lifetime);
}Добавим поля для настроек рэйкаста в контроллер оружия (WeaponController)
C#[Header("Hit settings")]
public float maxDistance = 200f; // длина луча, можно считать за прицельную дальность
private Camera hitCamera; // камера из которой будет пускаться луч
private bool hasTarget; // флаг показывающий наличие цели
private Vector3 hitPoint; // позиция попадания луча в цель
private CrosshairController crosshair; // Добавим метод инициализации, чтобы позже прописать получение всего необходимого из менеджера
(вы можете сделать эти поля в контроллере публичными и проставить их вручную, если у вас не планируется добавление большего числа орудий)
C#public void Initialize(Camera cam, CrosshairController crosshairController)
{
hitCamera = cam;
crosshair = crosshairController;
}Добавим метод проверки попадания луча из камеры в цель, который будем вызывать каждый кадр
C#private void Update()
{
HitCheck();
}
private void HitCheck()
{
// Не проверяем во время перезарядки
if(isReloading) return;
// Создаем луч, направленный из камеры
Ray ray = new Ray(hitCamera.transform.position, hitCamera.transform.forward);
hasTarget = false;
// Проверяем попадание луча на основе дистанции
if (Physics.Raycast(ray, out RaycastHit hit, maxDistance))
{
// Точка попадания
hitPoint = hit.point;
// Проверка на наличие цели
if (hit.collider.TryGetComponent<Damagable>(out var dmg))
{
hasTarget = true;
}
}
else
{
// Если ни во что не попали – ставим точку далеко впереди
hitPoint = hitCamera.transform.position + hitCamera.transform.forward * maxDistance;
}
// Обновляем прицел
crosshair.SetHighlighted(hasTarget);
}А метод выстрела поменяем так, чтобы вычислять направление в сторону цели и передавать его в пулю
C#public void Shoot()
{
currentAmmo--;
// Создаём снаряд
Projectile bullet = Instantiate(projectilePrefab, firePoint.position, firePoint.rotation);
// Вычисляем направление выстрела
Vector3 direction = (hitPoint - firePoint.position).normalized;
// Инициализируем пулю передавая скорость и направление выстрела
bullet.Initialize(projectileSpeed, direction);
OnAmmoChanged?.Invoke(currentAmmo);
}Осталось только прописать инициализацию оружия в нужный момент в WeaponManager
Добавим в менеджер поле для хранения контроллера прицела, который будем передавать в оружие:
C#public CrosshairController crosshair;Пропишем метод для добавления нового оружия:
C#public void AddWeapon(WeaponController weapon)
{
// если оружия нет в списке
if(!weapons.Contains(weapon))
weapons.Add(weapon); // добавляем
// инициализируем оружие
weapon.Initialize(weaponCamera, crosshair);
}Изменим метод старта, чтобы добавить всё оружие, которое имеется внутри объекта игрока:
C#private void Start()
{
defaultFov = playerCamera.fieldOfView;
foreach (var weapon in GetComponentsInChildren<WeaponController>())
{
AddWeapon(weapon);
}
SelectWeapon(0);
}На этом проверка на цель с рэйкастом закончена. Не забудьте заполнить публичные поля в редакторе перед проверкой.
Улучшать игровой опыт, наполняя его дополнительными механиками и удобными фишками можно до бесконечности, я лишь рассказал о самых важных и не самых очевидных механиках.
Как доработать, изменить или улучшить стрельбу в вашей игре – решать только вам.
Могу только предложить вам подумать над реализацией или даже сделать следующее:
Как всегда, все возникающие вопросы можете задать в чате или комментариях.
В следующей статье мы добавим врагов, чтобы стрелять было веселей)
Желаю успехов!