translatedTitles.Article
Ke
Kerioth
12/01/25

Unity. Урок 4

Привет!

Меня зовут Аристарх, я веду занятия по разработке на 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 вложенную камеру (нажмите +)


Прицеливание с приближением

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

Для этого будем перемещать оружие между двумя положениями:

  • Обычным положением оружия (DefaultPos)
  • Положением для прицеливания (AimPos)


Создайте внутри основной камеры два пустых объекта (можете сделать 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);
}

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


Что ещё можно добавить?

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

Как доработать, изменить или улучшить стрельбу в вашей игре – решать только вам.


Могу только предложить вам подумать над реализацией или даже сделать следующее:

  • Счетчик с количеством всех оставшихся патронов, а не только оставшихся в магазине (например, "20/80")
  • Выставлять для каждого оружия свой прицел, которой будет задаваться в контроллере
  • Изменение прицела не только по цвету, но и по размеру или форме
  • При прицеливании менять угол обзора для камеры оружия сильнее, чтобы оно казалось ближе к игроку
  • Случайный разброс при стрельбе, с настройкой в контроллере оружия
  • Мгновенна стрельба рэйкастом без создания пуль


Как всегда, все возникающие вопросы можете задать в чате или комментариях.

В следующей статье мы добавим врагов, чтобы стрелять было веселей)

Желаю успехов!

hub.comments