Привет!
Меня зовут Аристарх, я веду занятия по разработке на Unity и буду вашим ментором.
Рад, что вы приступаете к разработке прототипа шутера на интенсиве Практик•ON.
При возникновении вопросов по Unity, пишите мне в чате в ветке Unity с тегом @Kerioth.
Вступайте в чат Практик•ON для прохождения интенсива
Для игр в жанре шутер, система оружия это ключевая механика. Стрельба в такой игре и способ её реализации определяет существенную часть геймплея.
В этой статье мы разберем несложную архитектуру, создадим все необходимы объекты и пропишем для них логику.
Если попробовать упростить механику стрельбы до базовых компонентов, их останется всего 4:
И этого нам будет более чем достаточно. При необходимости мы можем задать разные виды оружия или патронов, а мишенью может стать что угодно, враги или окружение.
Создадим четыре скрипта:
Чуть подробнее про логику их взаимодействия
WeaponManager
WeaponController
Projectile
Реализовывать будем в обратном порядке и начнем с Damagable
Напишем логику для объектов, которые будут получать урон.
C#using UnityEngine;
public class Damagable : MonoBehaviour
{
// здоровье
public float health = 100f;
// метод получения урона
public void TakeDamage(float amount)
{
// уменьшаем здоровье
health -= amount;
// если здоровье меньше или 0
if (health <= 0f)
{
Die();
}
}
// метод для обработки смерти
private void Die()
{
// удаляем объект
Destroy(gameObject);
}
}Этот скрипт можно повесить на любой объект, который должен реагировать на попадания пуль.
Главное добавьте компоненты коллайдера и твердого тела, чтобы пуля могла считать столкновение
Напишем логику для пули и других объектов, которыми будем стрелять
C# using UnityEngine;
public class Projectile : MonoBehaviour
{
public float lifetime = 3f; // время жизни
public int damage = 25; // урон
private Vector3 velocity; // вектор скорости пули
// инициализация, которая будет производиться через контроллер оружия
public void Initialize(float speed)
{
// вычисляем вектор скорости
velocity = transform.forward * speed;
// указываем удаление объекта через время жизни
Destroy(gameObject, lifetime);
}
void Update()
{
MoveForward();
}
// движение вперед
private void MoveForward()
{
transform.position += velocity * Time.deltaTime;
}
// при столкновении с чем-либо
private void OnCollisionEnter(Collision other)
{
// проверяем наличие Damagable
if (other.collider.TryGetComponent(out Damagable d))
d.TakeDamage(damage);
// удаляем объект
Destroy(gameObject);
}
}Пуля необходимо создать как отдельный префаб (игровой объект, который затем помещается в ассеты).
Можете сделать её сферой, вытянутой по оси Z или любой другой фигурой. Главное добавьте на неё коллайдер, по которому и будет проверяться наличие столкновений через метод OnCollisionEnter().
Если в дальнейшем будете проверять коллизии в других ситуациях, то помните, что хотя бы один из объектов столкновения должен обладать твердым телом.
WeaponController
Напишем логику работы оружия. В ней будут настройки стрельбы и метод для создания пули
C#using UnityEngine;
public class WeaponController : MonoBehaviour
{
[Header("Shooting")]
public float fireRate = 0.2f;
private float nextShotTime;
[Header("Projectile")]
public Projectile projectilePrefab;
public Transform firePoint;
public float projectileSpeed = 30f;
private float nextFireTime;
public void TryShoot()
{
if (Time.time < nextShotTime) return;
Shoot();
nextShotTime = Time.time + fireRate;
}
private void Shoot()
{
// Создаём снаряд
Projectile bullet = Instantiate(projectilePrefab, firePoint.position, firePoint.rotation);
// Задаетм ему скорость
bullet.Initialize(projectileSpeed);
}
}Сейчас выстрел происходит очень просто, по созданию пули.
Позже мы добавим в оружие больше механик, но для обычной стрельбы нам хватит и этого.
Создайте объект для оружие, можете взять готовую модель из магазина ассетов или соберите из примитивов.
Поместите объект как дочерний к камере, находящейся в объекте игрока, для того, чтобы он поворачивался в соответствии со взглядом игрока. Я так же добавил промежуточный объект WeaponHolder, чтобы несколько видов оружия не лежали прямо в камере, но логика текущей реализации будет работать и без этого.
Обязательно добавьте в объект оружия точку выстрела FirePoint .
Это может быть пустой объект, потому что нам нужен только его трансформ, чтобы знать откуда и в каком направлении производить выстрел. Но я рекомендую добавить 3D объект сферы, уменьшить его и убирать коллайдер, так можно наглядно видеть его положение и легче настроить.
После того, как зафиксируете его положение, можно отключить визуал (Mesh Renderer), чтобы он не мешал в игре.
На игроке будет отдельный скрипт, который будет отвечать за хранение оружия игрока, а так же вызов методов этого оружия
Перед тем, как перейти к написанию скрипта, создадим новые экшены ввода для стрельбы и перезарядки
Эти экшены будут использоваться в менеджере оружия.
C#using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;
public class WeaponManager : MonoBehaviour
{
public InputActionReference Fire;
public InputActionReference Reload;
public List<WeaponController> weapons; // список оружия
private int currentWeaponIndex = 0; // индекс текущего оружия
private void Start()
{
SelectWeapon(0);
}
void Update()
{
HandleFire();
}
void HandleFire()
{
// если был вызван экшен стрельбы
if(Fire.action.triggered)
// стреляем из текущего оружия
weapons[currentWeaponIndex].TryShoot();
}
public void SelectWeapon(int index)
{
// для всего оружия в списке оружия
for (int i = 0; i < weapons.Count; i++)
// активируем только если нужный индекс, остальные будут деактивированы
weapons[i].gameObject.SetActive(i == index);
}
}Этот скрипт помещается на объект игрока, в нем надо указать экшены ввода для стрельбы и перезарядки, которые мы задали до этого, а так же оружие, которое настраивали до этого. После этого попробуйте протестировать стрельбу.
Сейчас мы можем стрелять сколько пожелаем, но в шутерах часто используется механика перезарядки оружия, для того чтобы контролировать темп и баланс геймплея.
В контроллер оружия добавим поля для настройки количества патронов и времени перезарядки
C#[Header("Ammo")]
public int maxAmmo = 30; // максимальное количество патронов
public int currentAmmo; // текущее количество патронов
public float reloadTime = 1.5f; // время перезарядки
private bool isReloading; // флаг показывающий идёт ли сейчас перезарядкаНа старте сделаем текущее количество патронов максимальным
C#private void Start()
{
currentAmmo = maxAmmo;
}Добавим новый метод перезарядки.
Он будет асинхронным, для того, чтобы сделать ожидание времени перезарядки, некую задержку, которую можно прописать в том же методе. Асинхронность можно реализовать разными методами, например через корутины (Corutines, IEnumerator) или таски (Task, UniTask). Но для того, чтобы выбрать самый подходящий метод, надо знать их особенности, преимущества и недочеты. Поэтому мы будем использовать самый простой метод для понимания и применения - Awaitables. Это решение от Unity, которое решает проблему неудобства использование IEnumerator и проблему работы системном потоке Task. В этом смысле он чем-то похож на UniTask хоть и с гораздо меньшим функционалом, но в отличие от UniTask, Awaitables не нужно докачивать отдельной библиотекой и они работают прямо так.
C#// модификатор async показывает что метод будет асинхронным
// использование async void не рекомендуется, но сейчас выбран для простоты
public async void Reload()
{
// если перезарядка уже идёт или не нужна, выходим из метода
if (isReloading || currentAmmo == maxAmmo)
return;
// начинаем перезарядку
print("Reloading start");
isReloading = true;
// ожидание времени перезарядки, модификатор await показывает что метод нужно ожидать
await Awaitable.WaitForSecondsAsync(reloadTime);
// обновляем текущее количество патронов
currentAmmo = maxAmmo;
// заканчиваем перезарядку
print("Reloading end");
isReloading = false;
}Так же необходимо обновить методы проверки стрельбы и самой стрельбы
C#public void TryShoot()
{
// добавлена проверка на перезарядку
if (Time.time < nextShotTime || isReloading) return;
// если нет патронов, выполняем перезарядку
if (currentAmmo <= 0)
{
Reload();
return;
}
Shoot();
nextShotTime = Time.time + fireRate;
}
private void Shoot()
{
// уменьшаем число патронов
currentAmmo--;
// Создаём снаряд
Projectile bullet = Instantiate(projectilePrefab, firePoint.position, firePoint.rotation);
bullet.Initialize(projectileSpeed);
}Так же обновим менеджер оружия для того, чтобы обрабатывать перезарядку
C#void Update()
{
HandleReload();
HandleFire();
}
void HandleReload()
{
if (Reload.action.triggered)
weapons[currentWeaponIndex].Reload();
}На этом настройка перезарядки закончена, попробуйте протестировать её работу и поиграть с настройкой значений.
Ещё одна механика, которую можно добавить в менеджер это смена оружия
Для менеджера оружия можно добавить метод выбора следующего или предыдущего оружия
C#// если direction = 1 выбирается следующее, если -1, то предыдущее
void SwitchWeapon(int direction)
{
//меняем индекс
currentWeaponIndex += direction;
// если индекс выходит за предел вниз
if (currentWeaponIndex < 0)
currentWeaponIndex = weapons.Count - 1;
// если индекс выходит за предел вверх
if (currentWeaponIndex >= weapons.Count)
currentWeaponIndex = 0;
// выбираем оружие
SelectWeapon(currentWeaponIndex);
}Осталось лишь подвязать вызов этого метода к экшенам ввода на смену оружия. Эту задачу попробуйте решить самостоятельно. Для тех кто хочет решение попроще – в экшенах ввода уже есть Previous и Next. А для тех, кто хочет проверить себя – попробуйте подвязать смену оружия ещё и на колесико мыши.
На этом основа системы оружия сделана, но никто не остановит вас от модификаций и новых реализаций, которые сделают ваш проект уникальным.
Попробуйте самостоятельно сделать несколько интересных вещей или просто подумать над реализацией:
Ещё раз напомню, что у вас всегда есть возможность задать вопрос в чате или комментариях.
В следующей статье мы доработаем стрельбу, чтобы улучшить игровой опыт.
Желаю успехов!