Область поиска предмета для FPS / TPS

В некоторых играх, от первого или третьего лица, особенно когда там нет перекрестия по центру экрана, можно заметить интересную деталь, когда указатель предмета подсвечивается даже в том случае, если этот предмет находится в некоторой близости от перекрестия. Иначе говоря, по центру экрана есть область и когда объект попадает в эту область, то помечается как активный и с ним можно взаимодействовать. Такой подход намного удобнее при поиске предмета (патроны, аптечка и прочее), нежели если бы игроку приходилось целиться перекрестием со снайперской точностью, что допустим в шутерах, крайне неудобно, при учете темпа игры.


Работает всё это дело следующим образом. Все предметы, которые в текущий момент находятся в видимости камеры, добавляются в массив. Затем создается область, указанного размера, по центру экрана. Делается проверка, если объект в этой области, то помечается иконкой. Если объектов несколько, то будет выбран тот, что находится ближе всего к центру. Когда в видимости камеры нет ни одного предмета, массив обновляется, чтобы его длинна не увеличивалась постоянно, так как объектов может быть много по ходу игры.

На предметы, с которыми можно взаимодействовать, вешаем:

using UnityEngine;
using System.Collections;

public class LocatorComponent : MonoBehaviour {

	private bool active;
	private Transform tr;

	public bool isActive
	{
		get{ return active; }
	}

	public Transform getTransform
	{
		get{ return tr; }
	}

	void Awake()
	{
		tr = transform;
	}

	void OnBecameVisible()
	{
		Locator.Internal.ToLocator(this);
		active = true;
	}

	void OnBecameInvisible()
	{
		active = false;
	}
}

На объекте должен присутствовать рендер компонент: Mesh Renderer, Skinned Mesh Renderer или Sprite Renderer.

А теперь, например, на камеру цепляем:

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

public class Locator : MonoBehaviour {

	[Header("UI иконка для цели")]
	[SerializeField] private RectTransform locatorIcon;
	[Header("Размер окна поиска в процентах")]
	[SerializeField][Range(10, 80)] private float percent = 50;
	[Header("Максимальная дистанция до цели")]
	[SerializeField] private float maxDistance = 30;

	private List<LocatorComponent> target;
	private List<int> id;
	private static Locator _internal;
	private GameObject _current;
	private Rect rect;
	private Vector2 result;

	#if UNITY_EDITOR
	void OnGUI() // для визуальной настройки размера окна во время тестирования
	{
		GUI.Box(rect, "");
	}
	#endif

	public static Locator Internal
	{
		get{ return _internal; }
	}

	public GameObject current // текущая цель
	{
		get{ return _current; }
	}

	void Awake()
	{
		_internal = this;
		target = new List<LocatorComponent>();
		id = new List<int>();
	}

	void FindTarget()
	{
		if(target.Count == 0) return;
			
		float distance = Mathf.Infinity;
		foreach(LocatorComponent comp in target)
		{
			if(comp && comp.isActive)
			{
				Vector2 pos = Camera.main.WorldToScreenPoint(comp.getTransform.position);
				float curDistance = Vector3.Distance(rect.center, pos);
				if(curDistance < distance) // поиск ближайшей цели
				{
					result = pos;
					distance = curDistance;
					_current = comp.gameObject;
				}	
			}
		}

		if(_current && !CheckDistance()) _current = null;
	}

	public void ToLocator(LocatorComponent comp)
	{
		int i = comp.GetInstanceID();
		if(CheckID(i))
		{
			target.Add(comp);
			id.Add(i);
		}
	}

	bool CheckID(int i) // проверка, чтобы не добавлялись одинаковые объекты
	{
		foreach(int j in id)
		{
			if(j == i) return false;
		}

		return true;
	}

	bool CheckActive()
	{
		foreach(LocatorComponent comp in target)
		{
			if(comp && comp.isActive)
			{
				return false;
			}
		}

		return true;
	}

	bool CheckDistance()
	{
		if(Vector3.Distance(Camera.main.transform.position, _current.transform.position) > maxDistance)
		{
			return false;
		}

		return true;
	}

	void LateUpdate()
	{
		float size = Screen.height * (percent / 100f);
		rect = new Rect(0, 0, size, size);
		rect.center = new Vector2(Screen.width/2, Screen.height/2);

		FindTarget();

		if(_current && rect.Contains(result))
		{
			locatorIcon.gameObject.SetActive(true);
			locatorIcon.anchoredPosition = result;
		}
		else 
		{
			if(target.Count > 0 && CheckActive()) // если ранее добавленные объекты вне видимости камеры
			{
				target = new List<LocatorComponent>();
				id = new List<int>();
			}
			_current = null;
			locatorIcon.gameObject.SetActive(false);
		}
	}
}

Обращаем ваше внимание, что у трансформа иконки, должен стоять пресет:

Область поиска предмета для FPS / TPS

Это необходимо для корректного позиционирования на экране.

Для того чтобы обратиться к текущему выделенному объекту, из других скриптов:

void Update()
{
	if(Input.GetMouseButtonDown(0))
	{
		Destroy(Locator.Internal.current);
	}
}

В этом примере, мы просто удаляем цель, правой кнопкой мыши.

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

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

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

Офлайн
evan 29 июля 2016
Вы немного не точно объяснили куда вешать LocatorComponent. Его нужно вешать на файл с renderer'ом, иначе он не считает что он может быть виден.

и так же к if (!CheckDistance()) _current = null; стоит добавить условие if (_current), иначе при поиске он может выдавать ошибку
Онлайн
Light 29 июля 2016
evan, всё верно, исправлено.
Офлайн
при импорте пакета не работает "управление головой" (камерой) мышкой в тестовой сцене.
мне хоть это и не нужно, просто как багрепорт отписал)
5.5.2
Офлайн
SleepyAsh 2 августа 2018
Light,
А как сделать отображение нескольких целей сразу а не одной?
Онлайн
Light 2 августа 2018
SleepyAsh, тут надо прогон по массиву делать и отображать все объекты попавшие в Rect окошко + нужно создать пул иконок, чтобы помечать объекты. Если нужна модификация скрипта, можно заказать в ЛС.
Информация
Посетители, находящиеся в группе Гости, не могут оставлять комментарии к данной публикации.
  • Дешевый хостинг
  • Яндекс.Метрика