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

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

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


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

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


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

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


Это вспомогательный объект, из которого будет вылетать поисковый луч, обратите внимание, что данный объект необходимо установить по центру между пушками, используя оси Х и 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

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

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

HP_текущий / HP_максимальный
Офлайн
Atanashi 31 января 2017
Light,
Можно и так. Будет ли у вас в планах писать подобный скрипт?
Офлайн
Light 1 февраля 2017
Atanashi, будет.
Офлайн
Stason4ikRU 17 мая 2017
Как сделать турель для 2D Игры ?
Офлайн
Light 18 мая 2017
Stason4ikRU, переписать под 2д физику.
Информация
Посетители, находящиеся в группе Гости, не могут оставлять комментарии к данной публикации.
  • Дешевый хостинг
  • Яндекс.Метрика