Как создать 2D-шутер в Unity
Создание шутеров позволяет научиться многим вещам, которые пригодятся в разработке игр других жанров.


vlada_maestro / shutterstock
На примере шутеров можно научиться нескольким очень полезным приемам работы с Unity: инстанцированию префабов, созданию логики для NPC, изменению здоровья персонажей и так далее.
Перед чтением статьи рекомендуем ознакомиться с другими нашими материалами о базовых навыках работы с Unity, которые пригодятся для создания шутера:
Что делает шутер шутером
Шутер (от англ. Shoot — стрелять) — это игра, в которой игрок должен стрелять из какого-либо оружия (пистолет, лук, лазерная винтовка), чтобы побеждать врагов. Добавления этой механики в игру достаточно, чтобы ее можно было назвать шутером.
Кроме стрельбы, также можно реализовать и другие возможности:
- управление транспортом;
- прокачку персонажа;
- торговлю;
- исследование мира;
- крафтинг (создание игровых предметов).
При этом не важно, в каком сеттинге находится игра (фэнтези, фантастика, Средневековье) и сколько в ней измерений (два или три), — она все равно будет считаться шутером, если есть возможность стрелять. Поэтому в статье основное внимание уделено именно этой механике.
Стрельба
Есть два основных способа реализовать стрельбу:
- Префабы. Добавляет на карту снаряд, которому можно прописать поведение — направление полета, действия при попадании и так далее.
- Лучи (Raycast). Движок рисует невидимую линию от какой-нибудь точки в заданном направлении и возвращает данные о том, есть ли что-нибудь на пути.
Каждый стоит разобрать более подробно.
Начало выстрела
В первую очередь нужно подготовить всё, чтобы персонаж мог стрелять. Начать стоит с создания точки, откуда будет лететь снаряд или направляться луч. Для этого добавьте пустой объект с именем FirePoint и поместите его внутрь персонажа, расположив возле дула его оружия:

Затем нужно написать код, который позволит персонажу вращаться вместе с этой точкой. Создайте скрипт Controller.cs и прикрепите его к персонажу:
public class Controller : MonoBehaviour
{
private Rigidbody2D rb;
private float speed = 5f;
private bool isRightSide = true;
void Start()
{
rb = GetComponent<Rigidbody2D>(); //Получение компонентов
}
void Update()
{
//Движение
float moveX = Input.GetAxis("Horizontal");
rb.MovePosition(rb.position + Vector2.right * moveX * speed * Time.deltaTime);
if ((moveX > 0f && !isRightSide) || (moveX < 0f && isRightSide)) //Если персонаж начал двигаться в противоположную сторону
{
Spin();
}
}
void Spin()
{
isRightSide = !isRightSide;
transform.Rotate(0f, 180f, 0f); //Вращение персонажа по оси X на 180 градусов
}
}
Теперь персонаж сможет двигаться и вращаться вместе с объектом FirePoint:

После этого можно приступить к скрипту, который позволит стрелять. Назовите его Shooting.cs, добавьте к персонажу и используйте следующий код:
public class Shooting : MonoBehaviour
{
public GameObject bullet; //Снаряд
public Transform firePoint; //Точка, с которой будут отправляться снаряды и лучи
public LineRenderer lineRenderer; //Луч
void Update()
{
if (Input.GetKeyDown(KeyCode.RightControl)) //Если игрок нажал на правый Ctrl
{
//Вызов метода стрельбы снарядами
ShootBullet();
//Вызов метода стрельбы лучами
StartCoroutine(Shoot());
//Выберите один из них
}
}
}
Теперь можно разобрать оба способа стрельбы.
Стрельба снарядами
Для начала нужно создать снаряд. Для этого перетащите на карту спрайт и назовите его Bullet:

Добавьте коллайдер с триггером и создайте скрипт Bullet.cs, в котором будут обрабатываться попадания (он будет рассмотрен чуть позже). Сохраните объект в качестве префаба, а потом перетащите его в компонент Shooting.cs. Туда же перетащите FirePoint:

Теперь нужно написать метод, который будет создавать (инстанцировать) новые снаряды на карте:
void ShootBullet()
{
Instantiate(bullet, firePoint.position, firePoint.rotation);
}
Вот как это выглядит:

Пока снаряд остается на месте. Чтобы это исправить, нужно прописать в Bullet.CS этот код:
public class Bullet : MonoBehaviour
{
private Rigidbody2D rb;
private float speed = 15f;
private int damage = 20;
private int life = 0;
private int lifeMax = 500;
void Start()
{
rb = GetComponent<Rigidbody2D>();
rb.velocity = transform.right * speed; //Изменение скорости
}
void Update()
{
life++;
if (life >= lifeMax)
{
Explode(); //Если снаряд пролетел определенное расстояние и ни с чем не столкнулся, его нужно удалить, чтобы он не расходовал ресурсы
}
}
void OnTriggerEnter2D(Collider2D hitInfo) //Метод, который срабатывает при попадании
{
Explode();
}
void Explode()
{
Destroy(gameObject); //Уничтожение объекта
}
}
Теперь снаряд будет лететь и уничтожаться при попадании во что-то:

Стрельба лучами
В первую очередь нужно добавить персонажу объект Effect -> Line.

Укажите в X — 1, а в Z — 0, а затем поставьте галочку Use World Space. После этого можно изменить толщину линии, поменять цвет, закруглить края и так далее.
Прикрепите получившуюся линию к скрипту Shooting.cs и добавьте следующий метод:
IEnumerator Shoot() //Выполнение методов этого типа можно остановить на определенный срок
{
RaycastHit2D hitInfo = Physics2D.Raycast(firePoint.position, firePoint.right); //Создание луча
if (hitInfo) //Если луч во что-то попал
{
lineRenderer.SetPosition(0, firePoint.position); //Задание позиции начала линии
lineRenderer.SetPosition(1, hitInfo.point); //Конец линии
}
else //Если попадания не было
{
lineRenderer.SetPosition(0, firePoint.position);
lineRenderer.SetPosition(1, firePoint.position + firePoint.right * 50);
}
lineRenderer.enabled = true; //Включение отображения линии
yield return 0; //Ждать один кадр
lineRenderer.enabled = false; //Отключить отображение линии
}
Вот как это выглядит:

Теперь, выбрав подходящий способ стрельбы, можно реализовать работу с очками здоровья.
Получение повреждений
За очки жизни будет отвечать скрипт Health.cs — его нужно добавить всем объектам, которые должны получать повреждения при попадании.
public class Health : MonoBehaviour
{
public int hp = 100;
public int hpMax = 100;
void Update()
{
if (hp > hpMax)
{
hp = hpMax;
}
if (hp <= 0)
{
Die();
}
}
public void Hit(int damage)
{
hp -= damage;
}
void Die()
{
Destroy(gameObject);
}
}
Теперь нужно изменить код попадания внутри класса Bullet.cs:
void OnTriggerEnter2D(Collider2D hitInfo)
{
Health health = hitInfo.GetComponent<Health>();
if (health)
{
health.Hit(damage);
}
Explode();
}
Обновленный метод проверяет, есть ли у объекта, в который попал снаряд, компонент Health. Если он существует, то вызывается метод Hit(), который отнимает здоровье.

Примерно такой же код можно добавить в метод запуска луча.
NPC для 2D-шутера
Чтобы игра не превратилась в стрельбу по неподвижным мишеням, нужно написать скрипт поведения объектов — NPC.cs:
public class NPC : MonoBehaviour
{
public GameObject bullet;
public Transform firePoint;
private Rigidbody2D rb;
private Animator animator;
private float speed = 5f;
private bool isRightSide = true;
private int timer = 0;
private int timerMax = 50;
private int action = 0;
private int reload = 0;
private int reloadMax = 5;
void Start()
{
rb = GetComponent<Rigidbody2D>();
animator = GetComponent<Animator>();
}
void Update()
{
timer++;
if (timer >= timerMax)
{
timer = 0;
action++;
}
float moveX = 0f;
switch (action)
{
case 0:
moveX = 1f;
break;
case 1:
moveX = 0f;
break;
case 2:
moveX = -1f;
break;
case 3:
moveX = 0f;
break;
default:
action = 0;
break;
}
rb.MovePosition(rb.position + Vector2.right * moveX * speed * Time.deltaTime);
if ((moveX > 0f && !isRightSide) || (moveX < 0f && isRightSide))
{
Spin(moveX);
}
//Shooting
if (reload >= 1)
{
reload--;
}
RaycastHit2D hitInfo = Physics2D.Raycast(firePoint.position, firePoint.right);
if (hitInfo)
{
if (hitInfo.transform.tag == "Player") //Если объекту присвоен тег Player
{
Shoot();
}
}
}
void Spin(float moveX)
{
isRightSide = !isRightSide;
transform.Rotate(0f, 180f, 0f);
}
void Shoot()
{
if (reload <= 0)
{
Instantiate(bullet, firePoint.position, firePoint.rotation);
reload = reloadMax;
}
}
}
Так создается примитивная логика поведения персонажа. Он может двигаться или стоять на месте, а если заметит героя (это проверяется с помощью Raycast), то начнет стрелять.

Заключение
Это довольно простой шутер, но полученных знаний должно хватить, чтобы разработать что-то более сложное и интересное. Если вы хотите глубже погрузиться в тему разработки игр на Unity, читайте статьи в нашем блоге и записывайтесь на курс «Профессия разработчик игр на Unity».