Область поиска предмета для 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

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

Офлайн
Light 16 марта 2019
Ice, пример, что делается и что происходит, скрины или лучше видео?
Офлайн
Ice
Ice 16 марта 2019
Light, что делать, если при масштабировании экрана иконка локатора куда-то улетает?
Офлайн
Light 17 октября 2018
siriusspark, для таких задач надо совсем другой скрипт писать.
Офлайн
siriusspark 17 октября 2018
Light, гм, не совсем то. У меня суть такая, что маркеры отмечают объекты с которыми можно взаимодействовать, например включить/выключить, открыть/закрыть, подобрать и так далее. Так вот, допустим у меня есть аптечка, которая лежит в шкафу. Пока шкаф закрыт - маркер отмечает только шкаф, как объект который можно открыть. Когда дверь открывается - маркер показывает и аптечку тоже. Но когда закрыта дверь - локатор аптечку лежащую за ней видеть естественно не должен. Вот, как то так. Наверно тут рейкаст может помочь, но я не соображу как его сюда прикрутить.
Офлайн
Light 12 октября 2018
siriusspark, надо сделать поиск в глубину внутри маркера.

Найти функцию FindTarget и заменить содержимое на:
	void FindTarget()
	{
		if(target.Count == 0) return;
			
		float distance = Mathf.Infinity;
		foreach(LocatorComponent comp in target)
		{
            Vector2 contains = Camera.main.WorldToScreenPoint(comp.getTransform.position);
            if (comp && comp.isActive && rect.Contains(contains))
			{
                Vector2 pos = Camera.main.WorldToScreenPoint(comp.getTransform.position);
                float curDistance = Vector3.Distance(Camera.main.transform.position, comp.getTransform.position);
                if (curDistance < distance) // поиск ближайшей цели
				{
					result = pos;
					distance = curDistance;
					_current = comp.gameObject;
				}	
			}
		}

		if(_current && !CheckDistance()) _current = null;
	}
Офлайн
siriusspark 12 октября 2018
Light, а как сделать чтобы если один объект перекрывается другим, то отмечался только тот, что ближе?
Офлайн
Light 2 августа 2018
SleepyAsh, тут надо прогон по массиву делать и отображать все объекты попавшие в Rect окошко + нужно создать пул иконок, чтобы помечать объекты. Если нужна модификация скрипта, можно заказать в ЛС.
Офлайн
SleepyAsh 2 августа 2018
Light,
А как сделать отображение нескольких целей сразу а не одной?
Офлайн
при импорте пакета не работает "управление головой" (камерой) мышкой в тестовой сцене.
мне хоть это и не нужно, просто как багрепорт отписал)
5.5.2
Офлайн
Light 29 июля 2016
evan, всё верно, исправлено.
Офлайн
evan 29 июля 2016
Вы немного не точно объяснили куда вешать LocatorComponent. Его нужно вешать на файл с renderer'ом, иначе он не считает что он может быть виден.

и так же к if (!CheckDistance()) _current = null; стоит добавить условие if (_current), иначе при поиске он может выдавать ошибку
Информация
Посетители, находящиеся в группе Гости, не могут оставлять комментарии к данной публикации.
  • Яндекс.Метрика