Делаем порталы как в игре Portal

Можно найти различные варианты реализации порталов, на а мы попробуем, да собственно чего пробовать, мы сделаем сегодня собственный вариант порталов, прямо как в игре Portal. И кроме всего прочего, этот урок поможет понять, как сделать отражение в зеркале, например. Стоит отметить, что это будет не точная копия, а просто похоже на то, что есть в оригинальной игре. Финальный результат не идеален, есть над чем поработать и любой желающий уже сам займется всем этим. Тем не менее, главное понять принцип работы и собственно, чтобы вся конструкция работала соответствующе. С этим и будем разбираться.


Создаем в Unity новый 3D проект.

Во-первых, нам понадобиться персонаж, за основу возьмем то, что делали в предыдущих уроках. То есть, скрипт управления от первого лица и персонаж, но в скрипт PlayerControl, нужно внести некоторые изменения в соответствующих строках:

void OnCollisionStay(Collision coll) 
{
	// Возврат вращения тела по Х к нулю, после выхода из портала
	if(!PortalMenedjer.isPortal) transform.localRotation = Quaternion.Lerp(transform.localRotation, Quaternion.Euler(0, transform.localRotation.eulerAngles.y, 0), 3 * Time.deltaTime);
	jump = true;
}

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

Настроим сцену. Удалите камеру со сцены, так как у нашего персонажа уже есть камера. Также создайте какую-нибудь поверхность по которой будем ходить, сделайте стены и прочее на свое усмотрение, где будем тестировать порталы. Добавляем Canvas, и переименуем в GameUI, а в него добавляем Image, это будет прицел, размещаем по центру экрана, в качестве источника картинки, возьмите какое-нибудь изображение прицела. Теперь на эту картинку вешаем скрипт PortalMenedjer:

using UnityEngine;
using System.Collections;

public class PortalMenedjer : MonoBehaviour {

	public static bool isPortal;
	public GameObject[] partal;
	private Vector3 rot;

	void Start () 
	{
		// Сброс активности порталов
		isPortal = false;
		foreach(GameObject p in partal)
		{
			p.SetActive(false);
			p.GetComponent<Portal>().activePortal = false;
		}
	}

	// Назначение переменной activePortal, если оба портала активны
	void SetPortalActive()
	{
		int i = 0;
		foreach(GameObject p in partal)
		{
			if(p.activeSelf) i++;
		}
		if(i == 2)
		{
			foreach(GameObject p in partal)
			{
				p.GetComponent<Portal>().activePortal = true;
			}
		}
	}
	
	void Update () 
	{
		RaycastHit hit;
		Ray ray = Camera.main.ScreenPointToRay(transform.position);
		if (Physics.Raycast(ray, out hit))
		{
			if(Input.GetMouseButtonDown(0))
			{
				// Установка позиции портала и поворот 
				partal[0].GetComponent<Portal>().hitNormal = hit.normal;
				partal[0].GetComponent<RectTransform>().localRotation = Quaternion.FromToRotation(Vector3.forward, hit.normal);
				partal[0].GetComponent<RectTransform>().position = hit.point;
				rot = partal[0].GetComponent<RectTransform>().localRotation.eulerAngles;
				rot = new Vector3(rot.x, rot.y, 0);
				partal[0].GetComponent<RectTransform>().localRotation = Quaternion.Euler(rot);
				partal[0].SetActive(true);
				SetPortalActive();

			}
			else if(Input.GetMouseButtonDown(1))
			{
				partal[1].GetComponent<Portal>().hitNormal = hit.normal;
				partal[1].GetComponent<RectTransform>().localRotation = Quaternion.FromToRotation(Vector3.forward, hit.normal);
				partal[1].GetComponent<RectTransform>().position = hit.point;
				rot = partal[1].GetComponent<RectTransform>().localRotation.eulerAngles;
				rot = new Vector3(rot.x, rot.y, 0);
				partal[1].GetComponent<RectTransform>().localRotation = Quaternion.Euler(rot);
				partal[1].SetActive(true);
				SetPortalActive();
			}
		}
	}
}

Этот скрипт отвечает за установку порталов. Из прицела пускает луч и при нажатии ПКМ или ЛКМ, скрипт устанавливает портал в точку, куда смотрит прицел. Не забудьте добавить потом порталы в массив partal.



Теперь порталы.
Добавляем Canvas, назовем его Portal_0, настроим как показано ниже:

Делаем порталы как в игре Portal

Внимание! У портала и всех вложенных в него объектов, должен быть установлен слой Ignore Raycast, чтобы луч игнорировал эти объекты.

В Portal_0 добавим Image, это будет маска, т.е. на это изображение надо повесить компонент Mask, это нужно чтобы сделать эллипс нашего портала. Настраиваем:


Обратите внимание на небольшой отступ по Z, это нужно чтобы изображение было чуть выше поверхности на которую накладывается, иначе порта будет не виден.

В качестве маски используем обычную PNG картинку, импортированную как спрайт:


Далее, в маску добавляем Raw Image под именем Portal_0_RenderTexture и настроим:


Создадим две текстуры RenderTexture_0 и RenderTexture_1 для другого портала. Чтобы это сделать, просто кликнете ПКМ по папке, куда хотите добавить текстуру и пункте меню Create выберите Render Texture.

В портал нужно добавить камеру, удалите на ней Audio Listener. Позиция камеры по нулям, а в качестве Target Texture камеры, указываем нашу текстуру RenderTexture_1. Внимание! Для портала под номером 0 указываем текстуру под номером 1, соответственно для другого портала наоборот.

Суть в том, что камера будет показывать то что она "видит" на этой текстуре, получается какбы монитор. По такому принципу можно сделать отражение в зеркале. Но в нашем случаи, первый портал - передает картинку второму, а второй - первому. На саму камеру, прицепим небольшой скрипт GameRenderTexture, который будет устанавливать разрешение текстуры, вдвое меньшее текущего разрешения по ширине:

using UnityEngine;
using System.Collections;

public class GameRenderTexture : MonoBehaviour {
	
	private Camera _camera;

	void Start () 
	{
		_camera = GetComponent<Camera>();
		_camera.targetTexture.width = Screen.width/2;
		_camera.targetTexture.height = Screen.width/2;
	}
}

Цепляем скрипт Portal на сам портал:

using UnityEngine;
using System.Collections;

public class Portal : MonoBehaviour {
	
	public int ID; // номер портала
	public Vector3 hitNormal;
	public bool activePortal;
	public PortalMenedjer _portalMenedjer;
	private Transform _object;

	void OnTriggerEnter(Collider coll)
	{
		if(coll.transform.CompareTag("Player"))
		{
			_object = coll.transform;
			PortalMenedjer.isPortal = true;
			if(activePortal)
			{
				if(ID == 0)
				{
					foreach(GameObject p in _portalMenedjer.partal)
					{
						if(p.GetComponent<Portal>().ID == 1)
						{
							// Перемещение игрока в другой портал
							_object.position = p.GetComponent<Transform>().position;
							_object.GetComponent<PlayerControl>().head.localEulerAngles = Vector3.zero; // Сброс вращения головы/камеры, чтобы выронить с телом
							// Разворот тела, также как и портал в который отправлен игрок
							_object.localRotation = Quaternion.FromToRotation(Vector3.forward, p.GetComponent<Portal>().hitNormal);
							_object.localRotation = Quaternion.Euler(_object.localRotation.eulerAngles.x, _object.localRotation.eulerAngles.y, 0);
							p.GetComponent<Portal>().activePortal = false;
						}
					}
				}
				else
				{
					foreach(GameObject p in _portalMenedjer.partal)
					{
						if(p.GetComponent<Portal>().ID == 0)
						{
							_object.position = p.GetComponent<Transform>().position;
							_object.GetComponent<PlayerControl>().head.localEulerAngles = Vector3.zero;
							_object.localRotation = Quaternion.FromToRotation(Vector3.forward, p.GetComponent<Portal>().hitNormal);
							_object.localRotation = Quaternion.Euler(_object.localRotation.eulerAngles.x, _object.localRotation.eulerAngles.y, 0);
							p.GetComponent<Portal>().activePortal = false;
						}
					}
				}
			}
		}
	}
	
	void OnTriggerExit(Collider coll)
	{
		if(coll.transform.CompareTag("Player"))
		{
			if(!activePortal)
			{
				foreach(GameObject p in _portalMenedjer.partal)
				{
					if(!p.GetComponent<Portal>().activePortal)
					{
						p.GetComponent<Portal>().activePortal = true;
					}
				}
			}
			PortalMenedjer.isPortal = false;
		}
	}
}

В принципе, с этим всё. Осталось сделать дубликат портала и указать соответствующий ID и текстуру с нужным номером. Должно получиться, что-то вроде этого:



Дополнительно, скачать готовый проект:

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

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

Офлайн
G-Warp 16 апреля 2017
один косяк: не поворачиваете камеру на нужный угол =)
Офлайн
Mike12 22 февраля 2019
Классно, но не совсем как в портал всё таки :)
Вот тут в видосе вроде более похоже получилось: https://youtu.be/bciyTmBPiFg
Информация
Посетители, находящиеся в группе Гости, не могут оставлять комментарии к данной публикации.
  • Яндекс.Метрика