Создаем большую карту уровня / местности [UI]

Мы уже рассматривали как создать миникарту для игры с помощью UI системы Unity, но на этот раз речь пойдет о карте другого типа. Делать будем карту, которая появляется по нажатию на клавишу «М», что очень часто встречается в играх различного жанра. Суть в том, чтобы на экране показывалась большая карта местности, текущего уровня или мира, как в играх песочницах. У игрока должна быть возможность увеличивать или уменьшать масштаб карты и передвигать ее, например, мышкой. Для упрощения процесса разработки, создадим специальный инструмент, с помощью которого можно будет делать скриншоты местности в высоком разрешении.


Для начала, создадим необходимый инструмент.
Добавляем на сцену обычный Cube и переименуем его в MainCube, чтобы не путаться в дальнейшем. Сделаем для этого куба отдельный материал и настраиваем прозрачность. Ставим его по центру игрой карты и растягиваем по осям Х и Z. Кроме того куб нужно поднять по оси Y выше, чем любые другие объекты на карте. Нам нужно добиться того, чтобы получилась плоскость, которая перекроет весь уровень. Например:




Смотрим сбоку и сверху, настраиваем масштаб по Х и Z.

Теперь, правый клик по MainCube и добавляем пустой дочерний объект, выставляем позиции объекта по нулям и вешаем на него следующий скрипт:

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

[RequireComponent(typeof(Camera))]

public class MapScreenshot : MonoBehaviour { // инструмент для создания скриншота карты

	private enum AA {none = 1, _2samples = 2, _4samples = 4, _8samples = 8}
	[SerializeField] private Transform mainCube;
	[SerializeField] private int resolution = 2000; // разрешение для наибольшей стороны
	[SerializeField] private AA antiAliasing = AA.none;
	private int width, height;

	public void TakeScreenshot()
	{
		float aspect = mainCube.localScale.x / mainCube.localScale.z;

		if(mainCube.localScale.x > mainCube.localScale.z)
		{
			width = resolution;
			height = Mathf.RoundToInt(resolution / aspect);
		}
		else
		{
			height = resolution;
			width = Mathf.RoundToInt(resolution * aspect);
		}

		Camera cam = GetComponent<Camera>();
		cam.orthographic = true;
		cam.clearFlags = CameraClearFlags.Depth;
		cam.renderingPath = RenderingPath.VertexLit;
		RenderTexture rt = new RenderTexture(width, height, 24);
		rt.antiAliasing = (int)antiAliasing;
		cam.targetTexture = rt;
		cam.orthographicSize = mainCube.localScale.z * 0.5f;
		Texture2D screenShot = new Texture2D(width, height, TextureFormat.ARGB32, false);
		cam.Render();
		RenderTexture.active = rt;
		screenShot.ReadPixels(new Rect(0, 0, width, height), 0, 0);
		cam.targetTexture = null;
		RenderTexture.active = null;
		DestroyImmediate(rt);
		byte[] bytes = screenShot.EncodeToPNG();
		string filename = Screenshot();
		System.IO.File.WriteAllBytes(filename, bytes);
		Debug.Log("Создан скриншот: " + filename);
		DestroyImmediate(screenShot);
	}

	string Screenshot()
	{
		return string.Format("{0}/shot_{1}.png", 
			Application.persistentDataPath, 
			System.DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss"));
	}
}

Он автоматом добавит камеру на данный объект.

Создаем большую карту уровня / местности [UI]

Внимание! Не забываем развернуть объект по оси Х на 90 градусов и указать переменную MainCube. Получится что камера у нас будет смотреть сверху вниз на уровень.

#if UNITY_EDITOR
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;

[CustomEditor(typeof(MapScreenshot))]

public class MapScreenshotEditor : Editor {

	public override void OnInspectorGUI()
	{
		DrawDefaultInspector();
		MapScreenshot t = (MapScreenshot)target;
		GUILayout.Label("Сделать снимок карты:", EditorStyles.boldLabel);
		if(GUILayout.Button("Take Screenshot")) t.TakeScreenshot();
	}
}
#endif

Чтобы добавить кнопку в инспекторе, кидаем этот скрипт в папку к другим.

Далее, все что осталось, это выключить динамические объекты (игрок, враги и т.п.) и нажать соответствующую кнопку, получим снимок уровня. После чего включаем игрока и врагов обратно, а MainCube наоборот выключаем. Внимание! Объект MainCube нельзя перемещать или изменять масштаб, после того как сделан снимок, в противном случае, нужно сделать снимок еще раз, так как MainCube является ключевым объектом.

Продолжаем. Добавляем на сцену Canvas и вешаем на него:

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

public class WorldMap : MonoBehaviour {

	[SerializeField] private Transform target; // объект на основе которого делался скрин карты
	[SerializeField] private float zoomMultiple = 5; // во сколько раз возможно увеличение
	[SerializeField] private float zoomOffset = 50; // отступ от края экрана (при минимальном зуме)
	[SerializeField] private KeyCode showMap = KeyCode.M; // вкл./выкл.
	[SerializeField] private KeyCode findPlayer = KeyCode.Tab; // поиск игрока на карте
	[SerializeField] private RectTransform parent; // родительский объект для иконок карты, игрока и т.п.
	[SerializeField] private RectTransform mapRect; // трансформ иконки карты
	[SerializeField] private RectTransform player; // трансформ иконки игрока
	private static WorldMap _internal;
	private Vector3[] worldCorners = new Vector3[4];
	private float zoom, width, height, aspect, offset;
	private Vector3 offsetPosition;
	private static bool _active;
	private bool isDrag;

	public static bool isActive // карта развернута или нет, можно использовать для блокировки управления персонажем
	{
		get{ return _active; }
	}

	void Awake()
	{
		aspect = target.localScale.x / target.localScale.z;
		zoom = 1;
		isDrag = false;
		Hide();
		_internal = this;
	}

	void CalculateScale() // масштабирование карты относительно текущего разрешения экрана
	{
		if(target.localScale.x > target.localScale.z)
		{
			width = (float)Screen.width - (zoomOffset * 2);
			height = width / aspect;
			offset = width / target.localScale.x;
		}
		else
		{
			height = (float)Screen.height - (zoomOffset * 2);
			width = height * aspect;
			offset = height / target.localScale.z;
		}

		ZoomControl(0);
	}

	void FindPlayer() // функция поиска игрока
	{
		if(player == null) return;

		Rect rect = new Rect(0, 0, Screen.width, Screen.height);

		if(!rect.Contains(player.position))
		{
			UpdateWorldCorners();
			Vector3 pos = new Vector3(Screen.width/2, Screen.height/2, 0) - player.position;
			if(worldCorners[0].x > 0 && worldCorners[2].x < Screen.width) pos.x = 0;
			if(worldCorners[0].y > 0 && worldCorners[2].y < Screen.height) pos.y = 0;
			parent.position += pos;
			UpdateWorldCorners();
			parent.position = PositionCorrection(parent.position);
		}
	}

	void MapControl()
	{
		if(Input.GetAxis("Mouse ScrollWheel") > 0 && !isDrag)
		{
			ZoomControl(0.1f);
		}
		else if(Input.GetAxis("Mouse ScrollWheel") < 0 && !isDrag)
		{
			ZoomControl(-0.1f);
		}

		if(Input.GetKeyDown(findPlayer) && !isDrag)
		{
			FindPlayer();
		}
	}

	void ZoomControl(float value) // зум карты
	{
		zoom += value;
		zoom = Mathf.Clamp(zoom, 1, zoomMultiple);
		mapRect.sizeDelta = new Vector2(width * zoom, height * zoom);

		if(value < 0)
		{
			UpdateWorldCorners();
			parent.position = PositionCorrection(parent.position);
			if(worldCorners[0].x >= 0 && worldCorners[2].x <= Screen.width &&
				worldCorners[0].y >= 0 && worldCorners[2].y <= Screen.height) parent.localPosition = Vector3.zero;
		}
	}

	void Show()
	{
		CalculateScale();
		_active = true;
		parent.gameObject.SetActive(true);
	}

	void Hide()
	{
		_active = false;
		parent.gameObject.SetActive(false);
	}

	void Update()
	{
		if(Input.GetKeyDown(showMap) && !_active) Show();
		else if(Input.GetKeyDown(showMap) && _active) Hide();

		if(!_active) return;

		MapControl();
	}

	float Round(float f) // необходимое округление до сотых, чтобы исключить погрешности вычислений
	{
		return ((int)(f*100f))/100f;
	}

	void UpdateWorldCorners() // получаем позиции углов иконки карты
	{
		mapRect.GetWorldCorners(worldCorners);
		worldCorners[0].x = Round(worldCorners[0].x);
		worldCorners[0].y = Round(worldCorners[0].y);
		worldCorners[2].x = Round(worldCorners[2].x);
		worldCorners[2].y = Round(worldCorners[2].y);
	}

	Vector3 PositionCorrection(Vector3 position) // функция "прилипания" к краю экрана
	{
		Vector3 pos = Vector3.zero;

		float x = Mathf.Max(worldCorners[0].x, Screen.width - worldCorners[2].x);
		float y = Mathf.Max(worldCorners[0].y, Screen.height - worldCorners[2].y);

		if(worldCorners[0].x > 0 && worldCorners[2].x > Screen.width) pos.x = -x;
		else if(worldCorners[0].x < 0 && worldCorners[2].x < Screen.width) pos.x = x;
		if(worldCorners[0].y > 0 && worldCorners[2].y > Screen.height) pos.y = -y;
		else if(worldCorners[0].y < 0 && worldCorners[2].y < Screen.height) pos.y = y;

		return position + pos;
	}

	bool CanMove(Vector2 delta, Vector3 position) // проверка на возможность "перетаскивания" карты
	{
		UpdateWorldCorners();

		if(worldCorners[0].x >= 0 && delta.x > 0 || worldCorners[2].x <= Screen.width && delta.x < 0
			|| worldCorners[0].y >= 0 && delta.y > 0 || worldCorners[2].y <= Screen.height && delta.y < 0)
		{
			offsetPosition = parent.position - position;
			return false;
		}

		return true;
	}

	public static Vector2 TransformPosition(Vector3 position)
	{
		return _internal.TransformPosition_internal(position);
	}

	public Vector2 TransformPosition_internal(Vector3 position)
	{
		Vector3 pos = position - target.position;
		return new Vector2(pos.x, pos.z) * zoom * offset;
	}

	public static void MapBeginDrag(Vector2 position)
	{
		_internal.MapBeginDrag_internal(position);
	}

	void MapBeginDrag_internal(Vector3 position)
	{
		isDrag = true;
		offsetPosition = parent.position - position;
	}

	public static void MapDrag(Vector3 position, Vector2 delta)
	{
		_internal.MapDrag_internal(position, delta);
	}

	void MapDrag_internal(Vector3 position, Vector2 delta)
	{
		if(CanMove(delta, position))
		{
			parent.position = position + offsetPosition;	
		}

		parent.position = PositionCorrection(parent.position);
	}

	public static void MapEndDrag()
	{
		_internal.MapEndDrag_internal();
	}

	void MapEndDrag_internal()
	{
		isDrag = false;
	}
}


Иерархия внутри Canvas должна выгладить так:


То есть, с начало создаем пустой объект, который будет родителем для всех иконок. А потом уже добавляем иконку карты, игрока и прочие. Когда мы импортируем ранее сделанный снимок уровня, как спрайт, добавляем эту картинку первую в иерархии родителя, чтобы другие иконки были поверх карты. Настраивать размеры карты ненужно, а вот размеры иконки игрока и прочих, настраиваем по вкусу.

На все иконки (кроме изображения карты) вешаем класс:

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

[RequireComponent(typeof(RectTransform))]

public class WorldMapComponent : MonoBehaviour {

	[SerializeField] private Transform target; // целевой объект (игрок, враг и т.п.)
	[SerializeField] private bool useRotation; // если иконка в виде стрелки, будет работать как ориентир, куда смотрит объект
	private RectTransform rectTransform;

	void Awake()
	{
		rectTransform = GetComponent<RectTransform>();
	}

	void LateUpdate()
	{
		if(useRotation)
		{
			float angle = Mathf.Atan2(target.forward.z, target.forward.x) * Mathf.Rad2Deg;
			rectTransform.rotation = Quaternion.AngleAxis(angle, Vector3.forward);
		}
	
		rectTransform.localPosition = WorldMap.TransformPosition(target.position);
	}
}

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


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

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

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

public class WorldMapTrigger : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler {
	
	void IBeginDragHandler.OnBeginDrag(PointerEventData eventData)
	{
		WorldMap.MapBeginDrag(eventData.position);
	}

	void IDragHandler.OnDrag(PointerEventData eventData)
	{
		WorldMap.MapDrag(eventData.position, eventData.delta);
	}

	void IEndDragHandler.OnEndDrag(PointerEventData eventData)
	{
		WorldMap.MapEndDrag();
	}
}

Он необходим для возможности перемещения карты мышкой.

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

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

У вас нет доступа!
Тестировалось на: Unity 5.5.0
Информация
Посетители, находящиеся в группе Гости, не могут оставлять комментарии к данной публикации.
  • Дешевый хостинг
  • Яндекс.Метрика