📜 ⬆️ ⬇️

How to use Canvas to compile a clickable world map on Unity3d

There was a task to collect a map of the world. And it is to collect from many countries, countries of the region, because countries should be clickable. Yes, there is nowhere easier, you will say, all you have to do is sift the whole map and hang out polygon-colliders, pfff by country ... But no, it means that the country will have to change color to red or black and will appear white when clicked. In addition, over time, red points should appear on the country (yes, yes ... I know what you thought). These points should be quite a lot on the map.

It was decided to assemble a map using Canvas. Convenient thing, saves a lot of time. But not at this time.

The 1st problem is a country of different sizes and a situation arises when one country closes the ocean or other countries with a transparent section, and the click doesn’t go where it’s needed, more precisely, not where it would be logical to (sprites would say a little bit what you would saw the horror of what is happening).

image
')
My first thought: to hang a Button on an Image-object and it’s in the bag, but no, the problem doesn’t solve it, Button is based on Image and it doesn’t let transparent sections, that is, transparent sections will still be buttons.
The second thought: get a pixel of the image at the point of pressing, and if the pixel is not transparent, then we clicked where we need it, if transparent, then see what other pixels there are at the clicked point.

And here is a stupor. How to get a pixel of a picture is not a problem, there are a lot of examples, but how to get canvas objects at the point of depression? Canvas does not have a collider, so letting Raycast useless is not going to return anything. And to push on each image-country a polygon-collider wildness.

Well, after reading the help and viewing a manual video with English-speaking Indians on Youtube, I came to the conclusion that it was time to use the features of EventSystem.

Created a script for the CountryMap countries and inherited it from the IPointerClickHandler interface, which is included in the above namespace. The only method of this interface OnPointerClick accepts a variable of type PointerEventData as input. From this variable, you can get a lot of interesting information, but I only need the position of depression.

Okay, the country is clickable (thanks to the interface), we know the position of tapa, it remains to get the pixel of the image under this position. We write a small method:

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 - rx, localCursor.y - ry); 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); } } 

If it is interesting, I will describe in more detail. In short, we transform the position from the screen coordinates to the local coordinates of the picture, get the position relative to the center of the picture, calculate the pixel coordinates of the picture, get the pixel, check for the alpha channel.

All fire, run!

image

There is a ban on reading the texture, we find the sprite image and expose it to the following parameters:

image

Now everything is fine, however ...

2nd problem. The pixel we found, the alpha channel was determined. But under the transparent layer is still another country.

Again EventSystem comes to the rescue, which has its own Raycast with blackjack and gameObjecta'mi.

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

The list of objects received, now you can work with it:

  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); } 

Well, that's all, now even if over an opaque pattern will be even 100 or more transparent, we will still see opaque.

I will give the full script code:
 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 - rx, localCursor.y - ry); 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(); } } } 


And now the bonus from solving the puzzle with the help of pixel viewing. Remember that picture where we put the parameters to the sprite? So, there is such a tick Read / Write Enabled, it is thanks to her that we can get access to the PS. As is clear from the word Write - not only for reading.

We can change pixels as we like!

Example, clarifying the sprite:

 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.5f); } } newTex.Apply(); CountryImg.sprite = Sprite.Create(newTex, CountryImg.sprite.rect, new Vector2(0.5f, 0.5f)); 

It was:

image

Result:

image

That's all. I really hope that this article will somehow help someone. If you have questions or comments, please write comments.

Source: https://habr.com/ru/post/260715/


All Articles