Unity3d QuickTip – czyli szybkie porady, rozwiązania częstych problemów i sztuczki w Unity3d!

Dzisiejszy odcinek: Minimapa z własną teksturą

Uwaga! Jest to poradnik typu QuickTip. Zatem skupia się on na osiągnięciu założonego celu. Zatem zakładamy że użytkownik zna na tyle program Unity3d, aby samodzielnie wykonać najprostsze czynności, jak np. dodanie modelu kostki do sceny czy dodanie modelowi jakiegoś komponentu. Jeżeli brakuje Ci tej podstawowej wiedzy, zapraszam do tutoriala:
Unity Tutorial – Podstawy

Teoria

Minimapy. Czyli mały kwadracik najczęściej w prawym dolnym rogu, który wspomaga gracza w rozbudowanych światach. Kiedyś budowaliśmy taką mapę w oparciu o kamerę, co było niby spoko, ale trochę brzydkie i niepraktyczne. Dużo częściej stosuje się mapy z teksturą jako podkładką. Mamy dwa warianty takich map. Albo widzimy mapę w całości oraz ikonę gracza przesuwającego się po niej, albo gracza w jednym miejscu i poruszającą się mapę.

Minimapa w GTA
Minimapa w GTA
Mapa w grze Gothic II
Mapa w grze Gothic II

My zrobimy sobie obie wersje.

Przygotowanie

Do zrobienia takiej minimapy potrzeba nam kilku rzeczy:

  • Terenu gry
  • Gracza
  • Tekstury mapy
  • Tekstury reprezentującej gracza na mapie

Aby życia wam nie utrudniać:

UnityQuickTip_27 – Paczuszka ze wszystkim co się może przydać.

Wyświetlamy teksturę mapy

Pierwszym krokiem naszej zabawy, będzie wyświetlenie samej mapy. Dałem wam gotowy przykład, ale szybko objaśnię, jak sobie taką teksturę przygotowałem. Otóż zrobiłem sobie screen kompletnej mapki z gry:

Screen mapy gry z Unity
Screen mapy gry z Unity

Następnie w Photoshopie (lub innym programie graficznym) nakładamy sobie w odpowiednich miejscach odpowiednie kolory, symbolizujące różne elementy. U mnie ciemny to teren do chodzenia, a jasny szary to góry.

Aby wyświetlić samą mapę, nie musimy się za bardzo natrudzić. Tworzymy sobie najpierw skrypt (u mnie MapController.cs). Dodajemy go od razu do gracza. Zaczynamy od deklaracji kilku zmiennych:

Możemy sobie teraz szybko wrócić do Unity i jako zmienną playerTexture przypisać teksturę dla gracza. Zmienna miniMap będąca zmienną typu GUIStyle wydaje się bardziej skomplikowana, bo ma masę opcji. Ale nas interesuje tylko jedna, czyli parametr Background dla opcji Normal. Wstawiamy tam teksturę mapy.

Ustawiamy teksturę mapy
Ustawiamy teksturę mapy

Teraz czas obrać odpowiednią pozycję mapy:

mapPosition ma typ Rect, więc będziemy mogli go bezpośrednio wykorzystać przy umiejscawianiu mapy. Wysokość i szerokość, to u mnie szerokość ekranu. Czemu? Bo chce mieć kwadratową mapę, dlatego w obu przypadkach daje ten sam wymiar. Mnożę go przez mapSize – teraz się wyjaśnia, czemu przy deklaracji ta zmienna była ułamkiem. W ten sposób ustalamy jaki procent szerokości ekranu, będzie stanowić mapa.

Pozycja x i y mapy, to szerokość/wysokość ekranu minus szerokość/wysokość mapy i margines. Co nam to daje? Pamiętajmy, że pozycje x i y w przypadku GUI to odległość lewego górnego rogu obrazka, od lewego górnego rogu ekranu. Jeśli podamy jako x samą szerokość ekranu, cała mapa wyląduje za ekranem. Odjęcie szerokości mapy sprawi, że mapa się pojawi, a dodatkowy margines jest tylko po to, żeby mapa nie stykała się z końcem ekranu.

Samo narysowanie mapy załatwia ten kod:

Tworzymy sobie grupę o pozycji ustalonej wcześniej, oraz ze stylem zawartym w zmiennej miniMap. Ustaliliśmy tam jako tło, teksturę mapy. Dzięki temu cała grupa będzie miała tło mapy. Co tutaj zyskujemy? Jeśli narysujemy coś wewnątrz grupy i przesuniemy mapę w lewy górny róg, nie musimy modyfikować ustawień tego, co rysujemy na mapie, bo zachowa się to jako zawartość grupy.

Teraz czas na mapie, narysować gracza.

Obiekt gracza

Dodajemy nowe zmienne:

Teren i dwie pomocnicze zmienne. Można szybko skoczyć do Unity i dodać teren do zmiennej. Może być to nieco mylące. Wszystko zaraz się wyjaśni. W funkcji start dopisujemy dwie linijki:

Pierwsza linia zapewnia nam informację o wymiarze terenu. Jego długość, szerokość i wysokość w jednostkach długości. Druga linia zapewnia informację o położeniu terenu w świecie gry – czyli współrzędne X, Y, Z.

No i czas na samo rysowanie gracza:

Samo korzystanie z DrawTexture jest proste i oczywiste, nie ma za bardzo co tutaj tłumaczyć. Podajemy pozycję i rozmiar tekstury, oraz samą teksturę, którą zdefiniowaliśmy na początku. Za to zagadkowe mogą być obliczenia px i py.

Zacznijmy od prostego rysunku, który może nakreśli naszą sytuację:

Zmiana skali
Zmiana skali

Po pierwsze musimy pobrać położenie gracza w świecie i jakoś przekształcić je na położenie na mapie. Chcemy to zrobić niezależnie od rozmiaru mapy i terenu. Drugi problem jest taki, że osie nam się nie pokrywają – tzn. oś x jest OK. Ale nasza oś Y jest, nie tylko osią Z, ale do tego ma przeciwny zwrot.

Zaczniemy od tego jak obrać położenie X. Całość to prosta proporcja:

Pozycja Gracza w Świecie – Rozmiar Terenu
Pozycja Gracza na mapie – Rozmiar Mapy

Z tego otrzymujemy:
Pozycja Gracza na mapie = (Rozmiar Mapy * Pozycja Gracza w Świecie) / Rozmiar Terenu
Wszystkie dane mamy. Ale jest jeden haczyk. Teren nie koniecznie musi być zlokalizowany w punkcie (0, 0, 0). Dlatego, aby wszystko działało od pozycji gracza odejmujemy pozycję terenu. Dzięki tej translacji, gracz będzie miał określoną pozycję tak, jak by teren był zlokalizowany w punkcie (0, 0, 0), co jest dla nas wygodne.

W przypadku osi Y, musimy operować osią Z. W Unity osi Y dla GUI odpowiada oś Z w świecie gry, gdy pracujemy na 3 wymiarach. Ale skąd się bierze odejmowanie od wysokości mapy na początku? Przez obrócenie wektora osi między światem i GUI. Tam gdzie kończy się GUI, w świecie mamy 0 i odwrotnie.

Np. jeśli mamy mapę 200px, to tam gdzie w GUI mamy pozycję: (30, 200) to w świecie gry, będzie to lokacja (30, y, 0). Dlatego odejmujemy od maksymalnej wartości wyliczoną pozycję, otrzymując faktyczne położenie gracza, zamiast lustrzanego odbicia.

Trochę to skomplikowane, ale tylko matematycznie.

A co z dużą mapą?

No właśnie. Co jeśli napisaliśmy nowe GTA, mamy ogromny świat i nie chcemy wyświetlić całej mapy, gdzie gracz jest wielkości piksela? Potrzebujemy wtedy zmienić tylko kilka rzeczy. Zmieni się nieco zasada działania. Zamiast skalować mapę, pokażemy graczowi jedynie jej fragment. Po pierwsze, dodajemy sobie dodatkową zmienną:

Oczywiście jest to tekstura mapy, którą należy uzupełnić z poziomu Unity.

Teraz modyfikujemy tylko funkcję OnGUI:

Na pierwszy rzut oka, zmiana może być nie widoczna. Ale zmieniamy mapPosition.width i mapPosition.height na mapTexture.width i mapTexture.height. Teraz naszym punktem odniesienia nie jest wymyślony przez nas rozmiar minimapy, tylko cała tekstura.

Następnie samo rysowanie:

Najpierw usuwamy styl z grupy. Całość dalej trzymamy w grupie, która określa położenie wyświetlania minimapy i rozmiar minimapy w pikselach. Styl usuwamy, ponieważ, aby „wycinanie” zadziałało, teksturę mapy musimy wyrysować oddzielnie. Jak widać, podajemy całkowity wymiar tekstury mapy jako parametry. Manewrujemy jedynie pozycją tekstury. Przez to, że grupa jest ograniczona, zawsze pokazuje jedynie wycinek całej tekstury, my przesuwając teksturę zmieniamy wyświetlany fragment. Dlatego położenie tej tekstury uzależnione jest od wyliczenia położenia postaci gracza. Ujemna wartość występuje dlatego, że musimy przenieść mapę po za obszar wyświetlania. Dodatkowe 80, to jakiś margines błędu. Nie udało mi się ustalić z czego wynika.

Druga linia to wyrysowanie postaci gracza na środku miejsca na mapę – czyli na środku grupy.

Problem pojawia się przy krawędziach mapy. Jeśli gracz do niej dotrze, tekstura zwyczajnie zniknie zostawiając puste miejsce. Rozwiązaniem tego, jest stworzenie odpowiednio większej tekstury mapy, tak aby graczowi zawsze coś się wyświetlało.

Uprzedzając pytania:

Jeśli chodzi o dodanie np. przeciwnika, zrobi się to analogicznie. Musimy tylko znać jego pozycję.

Jeśli chcecie mieć mapę na cały ekran jak w Gothicu, trzeba jedynie przeskalować teksturę na 100% ekranu. Dzięki procentowej wartości zmiennej mapSize, można to zrobić bardzo prosto – wystarczy ustawić tą wartość na 1 i uzależnić wyświetlanie mapy od naciśnięcia przycisku.

Czy da się do gracza dodać rotację? Da się, ale animacją. GUI nie obsługuje rotacji tekstur.

  • Votan

    Jest już drzewko umiejętności, mapa, przydałby się jeszcze ekwipunek i jakiś prosty dziennik ;)

  • Patryk Soboń

    A co jeśli chciałbym żeby minimapa była okrągła ?

    • Trzeba wykorzystać shader uwzględniający przeźroczystość i jako maskę przeźroczystości dodać tekturę o kształcie białego koła na czarnym tle.

      Możliwe, że trzeba się będzie pobawić z dwoma warstwami tekstur, żeby mapa się mogła przesuwać i być widoczna w całości.

      Nie robiłem tego, więc trochę zgaduję. ;)

  • Patryk Soboń

    co zrobić zeby mapa skalowała się z wielkościa ekranu ? przy widoku w unity pokazuje mi dobrze pozycje a gdy zbuliduje gre i odpale w duzej rozdzielczosci to pozycja gracza na mapie jest przesunieta

    • Problemem jest skalowanie czy przesunięcie pozycji gracza? Tzn. czy rozmiar mapy jest ten sam niezależnie od wybranej rozdzielczości, czy tylko gracz jest w złym miejscu?

    • Patryk Soboń

      im większa rozdzielczość tym gracz jest bardziej oddalony od faktycznego polozenia.

    • Problem występuje w obu osiach, czy tylko w jednej?

    • Patryk Soboń

      W obu osiach. Chciałbym by mapa była nieskalowalna tylko zawsze miala wielkosc np 100×100 px.

    • Jeżeli chcesz stałe wartości dla mapy, to w tym fragmencie:
      mapPosition.width = Screen.width * mapSize;
      mapPosition.height = Screen.width * mapSize;
      Zamiast mnożenia, wystarczy wstawić stałe wartości. Tylko wtedy musisz pokombinować z oznaczeniem gracza na tej mapie, bo kod może się nie zgrać.

    • Patryk Soboń

      Po zmianie wielkości na stałą błąd nadal występuje tak wygląda to dokładnie. https://uploads.disquscdn.com/images/49ea9ec0b26f51c6e6ef7459bd5b0706d47f9c46502f503bf0b696f9bca8ce7e.png
      na czerwono zaznaczyłem gdzie powinien znajdować się wskaźnik postaci

    • Hmmm… skrypt powinien trzymać gracza w centrum mapy, więc gracz jak by dobrze się wyświetla, tylko centrum mapy jest w złym miejscu.

      Pokombinuj z tymi parametrami:
      float px = (mapPosition.width * (transform.position.x – terrainPosition.x)) / terrainSize.x;
      float py = mapPosition.height – (mapPosition.height * (transform.position.z – terrainPosition.z)) / terrainSize.z;

      Gdzieś tam musi wychodzić kiepski wynik obliczeń.