Як використовуючи Canvas зібрати клікабельну карту світу на Unity3d

Виникла завдання зібрати карту світу. Причому саме зібрати з безлічі країн, країн, регіонів, тому що країни повинні бути клікабельні. Так простіше нікуди, скажете ви, всього-то й треба запив цілу мапу та розвісити по країнам полігон-колайдери, пффф… Але ні, мається на увазі, що країна повинна буде змінювати колір на червоний або чорний і при кліці буде виділятися білим. Крім того, з часом на країні повинні з'являтися червоні поінти (так-так… я знаю, про що ви подумали). Цих поінтів має бути досить багато на карті.

Було прийнято рішення зібрати карту за допомогою Canvas. Зручна штука, економить масу часу. Але не в цей раз.

1-я проблема — країна різних розмірів і виникає ситуація, коли одна країна прозорим ділянкою закриває океан, або інші країни, і клік доводиться не туди, куди потрібно, точніше, не туди, куди логічно хотілося б (трохи обілив спрайт, що б ви побачили весь жах того, що відбувається).

image

Перша думка: проблем-то навісити Button на Image-об'єкт і справа в капелюсі, але ні, проблему це не вирішить, Button ґрунтується на Image і прозорі ділянки він не пропускає, тобто прозорі ділянки все одно будуть кнопками.
Друга думка: отримати піксель зображення в точці натискання і якщо піксель не прозорий, то ми перейшли туди, куди треба, якщо прозорий, то подивитися, які ще є пікселі в точці натисканні.

І ось тут ступор. Як отримувати піксель зображення, це не проблема, тут маса прикладів, а от як отримати канвасовские об'єкти в точці натискання? Канвас не має коллайдера, тому пускати Raycast марно, нічого не поверне. А пхати на кожну image-країну полігон-колайдер дикість.

Що ж, після прочитання довідки переглядів мануал-відео з англомовними індусами на Youtube прийшов до висновку, що настав час використовувати можливості EventSystem.

Створив скрипт для країн CountryMap і наслідував його від інтерфейсу IPointerClickHandler, який входить в вищезазначений нэймспэйс. Єдиний метод цього інтерфейсу OnPointerClick приймає на вхід змінну типу PointerEventData. З цієї змінної можна отримати багато цікавої інформації, але мені потрібна тільки позиція натискання.

Окей, країна кликабельна (завдяки інтерфейсу), позицію тапа ми знаємо, залишилося дістати піксель зображення під цією позицією. Пишемо невеликий метод:

private bool IsAlphaPoint(PointerEventData eventData)
{
Vector2 localCursor;
RectTransformUtility.ScreenPointToLocalPointInRectangle(GetComponent<RectTransform>(), eventData.position, eventData.pressEventCamera, out localCursor);
Rect r = RectTransformUtility.PixelAdjustRect(GetComponent<RectTransform>(), GetComponent<Canvas>());
Vector2 ll = new Vector2(localCursor.x - r.x, localCursor.y - r.y);

int x = (int)(ll.x / r.height * CountryImg.sprite.textureRect.height);
int y = (int)(ll.y / r.height * CountryImg.sprite.textureRect.height);
if (CountryImg.sprite.texture.GetPixel(x, y).a > 0) return false;
else return true;
}
public void OnPointerClick(PointerEventData eventData)
{

if(!IsAlphaPoint(eventData))
{
print(gameObject.name);

}
}

Якщо буде цікаво, опишу детальніше. Якщо коротко, то перетворюємо позицію з координат екрана в локальні координати картинки, отримуємо позицію щодо центру картинки, обчислюємо координати пікселя зображення, дістаємо піксель, перевіряємо на альфа-канал.

Все, вогонь, запускаємо!

image

Варто заборона на читання текстури, знаходимо спрайт картинки і виставляємо йому наступні параметри:

image

Тепер все нормально, проте…

2-я проблема. Піксель ми знайшли, альфа-канал визначили. Але під прозорим шаром все одно знаходиться інша країна.

Знову ж на допомогу приходить EventSystem, у якого є свій Raycast з блекджеком і gameObjecta'мі.

List<RaycastResult> raycastResults=new List<RaycastResult>();
EventSystem.current.RaycastAll(eventData, raycastResults);

Список об'єктів отримали, тепер можна з цим працювати:

public void MayBeYouWantClickMe(List<CountryMap> ResultsCountryMap, PointerEventData eventData)
{
if (!IsAlphaPoint(eventData))
{
print(gameObject.name);
if (TapEvent != null) TapEvent(this);
}
else
{
ResultsCountryMap.Remove(this);
if (ResultsCountryMap.Count > 0) ResultsCountryMap[0].MayBeYouWantClickMe(ResultsCountryMap, eventData);
}
}
public void OnPointerClick(PointerEventData eventData)
{

if(!IsAlphaPoint(eventData))
{
print(gameObject.name);
if (TapEvent != null) TapEvent(this);
}
else
{
List<RaycastResult> raycastResults=new List<RaycastResult>();
EventSystem.current.RaycastAll(eventData, raycastResults);
List<CountryMap> ResultsCountryMap = raycastResults.Select(x => x.gameObject.GetComponent<CountryMap>()).ToList();
ResultsCountryMap.RemoveAll(x => x == null || x.gameObject==gameObject);
if (ResultsCountryMap.Count > 0) ResultsCountryMap[0].MayBeYouWantClickMe(ResultsCountryMap, eventData);

}

Ну, ось і все, тепер навіть якщо над непрозорим малюнком буде навіть 100 і більш прозорих, ми все одно побачимо непрозорий.

Наведу повний код скрипта:
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using UnityEngine.UI;
using UnityEngine.EventSystems;
using System.Linq;
public class CountryMap : MonoBehaviour,IPointerClickHandler {
Image CountryImg;
Image SelectCountry;
public event CountryMapEvent TapEvent;

void Awake()
{
CountryImg = GetComponent<Image>();
SelectCountry = transform.GetChild(0).GetComponent<Image>();
SelectCountry.sprite = Resources.Load<Sprite>("Image/Countries/" + CountryImg.sprite.name);
}
private bool IsAlphaPoint(PointerEventData eventData)
{
Vector2 localCursor;
RectTransformUtility.ScreenPointToLocalPointInRectangle(GetComponent<RectTransform>(), eventData.position, eventData.pressEventCamera, out localCursor);
Rect r = RectTransformUtility.PixelAdjustRect(GetComponent<RectTransform>(), GetComponent<Canvas>());
Vector2 ll = new Vector2(localCursor.x - r.x, localCursor.y - r.y);

int x = (int)(ll.x / r.height * CountryImg.sprite.textureRect.height);
int y = (int)(ll.y / r.height * CountryImg.sprite.textureRect.height);
if (CountryImg.sprite.texture.GetPixel(x, y).a > 0) return false;
else return true;
}
public void MayBeYouWantClickMe(List<CountryMap> ResultsCountryMap, PointerEventData eventData)
{
if (!IsAlphaPoint(eventData))
{
print(gameObject.name);
if (TapEvent != null) TapEvent(this);
}
else
{
ResultsCountryMap.Remove(this);
if (ResultsCountryMap.Count > 0) ResultsCountryMap[0].MayBeYouWantClickMe(ResultsCountryMap, eventData);
}
}
public void OnPointerClick(PointerEventData eventData)
{

if(!IsAlphaPoint(eventData))
{
print(gameObject.name);
if (TapEvent != null) TapEvent(this);
}
else
{
List<RaycastResult> raycastResults=new List<RaycastResult>();
EventSystem.current.RaycastAll(eventData, raycastResults);
List<CountryMap> ResultsCountryMap = raycastResults.Select(x => x.gameObject.GetComponent<CountryMap>()).ToList();
ResultsCountryMap.RemoveAll(x => x == null || x.gameObject==gameObject);
if (ResultsCountryMap.Count > 0) ResultsCountryMap[0].MayBeYouWantClickMe(ResultsCountryMap, eventData);

}
}

public void StopSelect()
{
StopAllCoroutines();
SelectCountry.color = new Color32(255, 255, 255, 0);
}
public void StartSelect()
{
StartCoroutine(Selecting());
}
IEnumerator Selecting()
{
int alpha=0;
int count = 0;
while (true)
{
alpha = (int)Mathf.PingPong(count, 150);
count = count > 300 ? 0 : count + 5;
SelectCountry.color = new Color32(255, 255, 255, (byte)alpha);
yield return new WaitForFixedUpdate();
}
}
}



А тепер бонус від рішення задачки з допомогою перегляду пікселя. Пам'ятаєте ту картинку, де ми ставили параметри спрайту? Так от, є там така галочка Read/Write Enabled, саме завдяки їй ми можемо отримати доступ до пискселю. Як зрозуміло з слова Write — не тільки для читання.

Ми можемо змінювати пікселі як нам завгодно!

Приклад, освітлення спрайту:

Texture2D tex = CountryImg.sprite.texture;
Texture2D newTex = (Texture2D)GameObject.Instantiate(tex);
newTex.SetPixels32(tex.GetPixels32());
for (int i = 0; i < newTex.width; i++)
{
for (int j = 0; j < newTex.height; j++)
{
if (newTex.GetPixel(i, j).a != 0f) newTex.SetPixel(i, j, newTex.GetPixel(i, j)*1.5 f);

}
}

newTex.Apply();
CountryImg.sprite = Sprite.Create(newTex, CountryImg.sprite.rect, new Vector2(0.5 f, 0.5 f));

Було:

image

Результат:

image

На цьому все. Дуже сподіваюся, що ця стаття хоч комусь допоможе. Якщо є питання чи зауваження, будь-ласка, пишіть коментарі.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.