Простая автоматическая турель

Турель, которую мы сегодня будем делать, предназначена для трехмерного проекта, ее можно будет использовать как в стратегии, так и, например, в шутере. Работает турель следующим образом. На старте сцены, переходит в режим ожидания. Если в область триггера турели попадает цель, то включается режим поиска и уничтожения. Турель ловит первый объект, который попал в область поражения, когда он будет уничтожен, то происходит новый поиск и среди доступных объектов турель выберет ближайший из них. Есть два режима стрельбы турели, «префабом» или «лучом».

Итак, турель состоит из нескольких частей. Башня, которая вращается на некой опоре, и собственно пушки. Модели могут быть разными, мы рассмотрим примитивный образец, вот такого вида:


Очень простая модель, с двумя пушками.

Для каждой пушки нам нужно создать дочерний пустой объект:


Это точки, откуда будут вылетать снаряды.

В центре башни также понадобится пустой дочерний объект:


Это вспомогательный объект, из которого будет вылетать поисковый луч, обратите внимание, что данный объект необходимо установить по центру между пушками, используя оси Х и Y, а по Z ставим на ноль, чтобы длина луча соответствовала настройкам скрипта.

Теперь, для варианта с снарядом, как он должен выглядеть:

Простая автоматическая турель

Если по форме это нечто похожее на пулю, то модель нужно выровнять по оси Z. И на модельку не забываем прицепить коллайдер в режиме триггера. Затем, вешаем скрипт:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[RequireComponent(typeof(Rigidbody))]

public class TurretBullet : MonoBehaviour {

	[SerializeField] private float damage = 10;
	[SerializeField] private float bulletSpeed = 5;
	private LayerMask layer;

	public void SetBullet(LayerMask layerMask, Vector3 direction)
	{
		layer = layerMask;
		Rigidbody body = GetComponent<Rigidbody>();
		body.useGravity = false;
		body.velocity = direction * bulletSpeed;
		transform.forward = direction;
	}
	
	void OnTriggerEnter(Collider other)
	{
		if(!other.isTrigger)
		{
			if(((1 << other.gameObject.layer) & layer) != 0)
			{
				other.GetComponent<UnitHP>().Adjust(-damage);
			}

			Destroy(gameObject);
		}
	}
}

Очень простой класс, где мы можем регулировать скорость снаряда и наносимый урон.

Класс которому мы передаем урон, может быть, например, таким:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class UnitHP : MonoBehaviour {

	[SerializeField] private float health = 100;

	public float currentHealth
	{
		get{ return health; }
	}

	public void Adjust(float value)
	{
		health += value;

		if(health <= 0)
		{
			Destroy(gameObject);
		}
	}
}

Тут вообще сложностей нет никаких.

Далее, основной скрипт, который цепляем на турель:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[RequireComponent(typeof(SphereCollider))]

public class Turret : MonoBehaviour {

	[SerializeField] private TurretBullet bulletPrefab; // префаб снаряда, если его нет, то будет выбран режим стрельбы "лучом"
	[SerializeField] private float fireRate = 1; // скорострельность
	[SerializeField] private float smooth = 1; // сглаживание движения башни
	[SerializeField] private float rayOffset = 1; // делаем поисковый луч, немного больше области поиска
	[SerializeField] private float damage = 10; // повреждение (при стрельбе "лучом")
	[SerializeField] private Transform[] bulletPoint; // точки, откуда ведется стрельба
	[SerializeField] private Transform turretRotation; // объект вращения, башня турели
	[SerializeField] private Transform center; // центр между пушками, для поискового луча
	[SerializeField] private LayerMask layerMask; // фильтр коллайдеров по маске слоя
	[Header("Лимиты по осям башни:")]
	[SerializeField] private bool useLimits;
	[SerializeField] [Range(0, 180)] private float limitY = 50;
	[SerializeField] [Range(0, 180)] private float limitX = 30;
	private SphereCollider turretTrigger;
	private Transform target;
	private Vector3 offset;
	private int index;
	private float curFireRate;
	private Quaternion defaultRot = Quaternion.identity;

	void Awake()
	{
		turretTrigger = GetComponent<SphereCollider>();
		turretTrigger.isTrigger = true;
		offset = turretTrigger.center;
		curFireRate = fireRate;
		turretTrigger.enabled = true;
		enabled = false;
	}

	void OnTriggerEnter(Collider other)
	{
		if(CheckLayerMask(other.gameObject, layerMask))
		{
			target = other.transform;
			turretTrigger.enabled = false;
			enabled = true;
		}
	}

	Transform FindTarget() // возвращает ближайшую цель
	{
		Collider[] colliders = Physics.OverlapSphere(transform.position + offset, turretTrigger.radius, layerMask);

		Collider currentCollider = null;
		float dist = Mathf.Infinity;

		foreach(Collider coll in colliders)
		{
			float currentDist = Vector3.Distance(transform.position + offset, coll.transform.position);

			if(currentDist < dist)
			{
				currentCollider = coll;
				dist = currentDist;
			}
		}

		return (currentCollider != null) ? currentCollider.transform : null;
	}

	Vector3 CalculateNegativeValues(Vector3 eulerAngles)
	{
		eulerAngles.y = (eulerAngles.y > 180) ? eulerAngles.y - 360 : eulerAngles.y;
		eulerAngles.x = (eulerAngles.x > 180) ? eulerAngles.x - 360 : eulerAngles.x;
		eulerAngles.z = (eulerAngles.z > 180) ? eulerAngles.z - 360 : eulerAngles.z;
		return eulerAngles;
	}

	bool Search() // разворот башни на цель
	{
		if(rayOffset < 0) rayOffset = 0;
		float dist = Vector3.Distance(transform.position + offset, target.position);
		Vector3 lookPos = target.position - turretRotation.position;
		Debug.DrawRay(turretRotation.position, center.forward * (turretTrigger.radius + rayOffset));
		Vector3 rotation = Quaternion.Lerp(turretRotation.rotation, Quaternion.LookRotation(lookPos), smooth * Time.deltaTime).eulerAngles;

		if(useLimits)
		{
			rotation = CalculateNegativeValues(rotation);
			rotation.y = Mathf.Clamp(rotation.y, -limitY, limitY);
			rotation.x = Mathf.Clamp(rotation.x, -limitX, limitX);
		}

		rotation.z = 0;
		turretRotation.eulerAngles = rotation;

		if(dist > turretTrigger.radius + rayOffset)
		{
			target = null;
			return false;
		}

		if(IsRaycastHit(center)) return true;

		return false;
	}

	bool CheckLayerMask(GameObject obj, LayerMask layers)
	{
		if(((1 << obj.layer) & layers) != 0)
		{
			return true;
		}

		return false;
	}

	bool IsRaycastHit(Transform point)
	{
		RaycastHit hit;
		Ray ray = new Ray(point.position, point.forward);
		if(Physics.Raycast(ray, out hit, turretTrigger.radius + rayOffset))
		{
			if(CheckLayerMask(hit.transform.gameObject, layerMask))
			{
				return true;
			}
		}

		return false;
	}

	void Shot()
	{
		if(!Search()) return;

		curFireRate += Time.deltaTime;
		if(curFireRate > fireRate)
		{
			Transform point = GetPoint();
			curFireRate = 0;

			if(bulletPrefab != null)
			{
				TurretBullet bullet = Instantiate(bulletPrefab, point.position, Quaternion.identity) as TurretBullet;
				bullet.SetBullet(layerMask, point.forward);
			}
			else if(IsRaycastHit(point))
			{
				target.GetComponent<UnitHP>().Adjust(-damage);
			}
		}
	}

	void Choice()
	{
		curFireRate = fireRate;

		target = FindTarget();

		turretRotation.rotation = Quaternion.Lerp(turretRotation.rotation, defaultRot, smooth * Time.deltaTime);

		if(Quaternion.Angle(turretRotation.rotation, defaultRot) == 0)
		{
			turretRotation.rotation = defaultRot;
			turretTrigger.enabled = true;
			enabled = false;
		}
	}

	Transform GetPoint()
	{
		if(index == bulletPoint.Length-1) index = 0; else index++;
		return bulletPoint[index];
	}

	void LateUpdate()
	{
		if(target != null) Shot(); else Choice();
	}
}

Скрипт автоматом добавит Sphere Collider, настраиваем радиус сферы, это будет область поиска целей. Если в игре используются объекты/юниты без компонента Rigidbody, то его нужно добавить на саму турель и поставить галочку Is Kinematic, чтобы срабатывал триггер турели.

Скачать демо проекта:

Внимание! Посетители, находящиеся в группе Гости, не могут скачивать файлы.
Тестировалось на: Unity 5.5.0

Комментариев 12

Офлайн
datodavita1212 16 октября 2018
a kak delat chtobi pulia sledil za vraga? chtobi on nemog ukloniatsia ot puli? sp zaranie
Офлайн
Light 5 ноября 2017
AlexMcKing, просто вешать на нее определенный тег или скрипт, что и будет определять конкретную принадлежность.
Офлайн
AlexMcKing 1 ноября 2017
Как сделать например в RTS, чтобы она принадлежала к определенной стороне?

Той что ее построит!
Офлайн
Light 18 мая 2017
Stason4ikRU, переписать под 2д физику.
Офлайн
Stason4ikRU 17 мая 2017
Как сделать турель для 2D Игры ?
Офлайн
Light 1 февраля 2017
Atanashi, будет.
Офлайн
Atanashi 31 января 2017
Light,
Можно и так. Будет ли у вас в планах писать подобный скрипт?
Офлайн
Light 31 января 2017
Atanashi, можно не по кадрам, а по времени клипа. Т.е. если за основу взять длину клипа анимации как сто процентов, и регулировать анимацию через переменную HP по формуле:

HP_текущий / HP_максимальный
Офлайн
Atanashi 30 января 2017
Light,
Индикатор здоровья hp. В стандартных hp обычно создают полосу, например из картинки которая растягивается на весь индикатор. От одного выстрела в игрока уменьшается какой-то определенный процент здоровья, но сами проценты не должны отображатся на индикаторе, вместо этого полоса здоровья, растянутая картинка сжимается тем самым показывая количество оставшегося здоровья у игрока. Но, мне нужно вместо растягивающейся картинки использовать готовую зарендеренную анимацию из другого 3d пакета состоящую из отдельных картинок. У меня например, 20 картинок покадровой анимации. Вставив все картинки в скрипт в инспекторе unity, в самой игре где-то с низу должен отображатся первый кадр заготовленной анимации с альфа каналом. Первый кадр это 100% здоровья. При попадании выстрела в игрока должно запускатся поочередно 5 кадров тем самым уменьшение здоровья к примеру до 80%. При попадании второго выстрела в игрока проигрывание следующих 5 кадров указанных в инспекторе и уменьшение еще на 20% от 80% . После снижения здоровья до 0% проигрываются последние 5 кадров и игра заканчивается.
Офлайн
Light 30 января 2017
Atanashi, по конкретней, какой результат планируется достичь, для чего это?
Офлайн
Atanashi 29 января 2017
Здравствуйте. Можете написать скрипт HP здоровье персонажа с использованием секвенции, покадровой анимации, при уменьшении здоровья воспроизведение определеного количества кадров?
Офлайн
Andrejus 5 января 2017
Как всегда на высшем уровне.
Информация
Посетители, находящиеся в группе Гости, не могут оставлять комментарии к данной публикации.
  • Яндекс.Метрика