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

Dzisiejszy odcinek: Ulepszamy ekwipunek (inwentarz)

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

Tydzień temu zaczęliśmy tworzyć inwentarz. Dzisiaj będziemy go ulepszać. Więc aby skorzystać z tego poradnika, należy najpierw wykonać poprzedni. Link podrzucam oczywiście.

Przygotowanie

W sumie potrzebna będzie tylko jakaś broń (w zestawie poleconym w poprzednim odcinku, jest kij), oraz ikona do niego (znów polecam iconfindera).

Grupowanie przedmiotów

Dziś zaczniemy nieco od tyłu, bo najpierw poprawimy sobie skrypt przedmiotu, a dokładniej nasze kluczowe itemAbstract:

Co się zmieniło? Dodaliśmy sobie getery dla każdej zmiennej. Bardzo się to przyda w późniejszych edycjach kodu – możemy teraz pobierać więcej wartości, które nam będą potrzebne.  Dodatkowo, mamy ifa, który sprawdza, czy możemy dodać przedmiot. Jednak póki co zakomentowałem ten fragment, aby nie rzucał błędu, że funkcja nie istnieje. Zajmiemy się tym później.

Aby móc grupować przedmioty (czyli, żeby zamiast wyświetlać 3 butelki wody w oddzielnych polach, wyświetlić jedną ikonę z cyferką 3), musimy dokonać zmian w sposobie zapisu i używania przedmiotów. Zacznijmy od zapisu. Dodajemy parę zmiennych:

W pierwszej trzymamy sobie informację o przyciskach (ikonach w menu),  w kolejnej mamy liczbę przedmiotów na danej pozycji, a ostatnia zmienna to informacja o liczbie posiadanych przedmiotów.

Teraz czas zmienić dodawanie przedmiotów, wcześniej wystarczyło dopisanie do listy. Teraz mamy coś takiego:

Wygląda potwornie. Zacznijmy od tego, że aby wykorzystać Predicate musimy znów dodać kolejny namespace:

Teraz chwila na teorię. Czym jest Predicate? Jest to klasa, upraszczająca wyszukiwanie obiektu o zadanym kryterium w liście/tablicy obiektów. W naszym przypadku z listy przedmiotów, chcemy znaleźć przedmiot o konkretnej nazwie – żeby sprawdzić, czy to co dodajemy, jest już w ekwipunku. Normalnie trzeba by było zrobić pętle, która leci po obiektach i porównuje wartości pola itemName. Predicate pozwala określić, jaki jest ten „if”, czyli co trzeba porównywać i jak. Funkcja FindIndex, jest tą naszą pętlą lecącą po tablicy. Przyjmuje jako parametr Predicate i dzięki temu wie, jak ma szukać.

Jeżeli dostaniemy jakiś index, wtedy dowiadujemy się, że dysponujemy obiektem – czyli gracz zebrał już np. butelkę wody. I tutaj wchodzi pierwszy if. Jeśli mamy już dany przedmiot, tylko zwiększamy liczbę posiadanych sztuk. W przeciwnym wypadku jest zabawa.

Dodajemy przedmiot do listy, zwiększamy liczbę posiadanych (różnych) przedmiotów. A następnie… Dodajemy przedmiot do tablicy liczby posiadanych elementów. Tylko dlaczego wygląda to tak strasznie?

Tablice są niestety dość niewdzięczne. O ile zwiększenie liczby elementów i rozmiaru listy jest banalnie proste o tyle, tablica ma raz deklarowany rozmiar. Czyli jak zadeklarujemy 10 elementów, tyle będzie miała i koniec. Więc, jeśli chcemy mieć dynamiczny rozmiar tablicy, trzeba ją redeklarować. Co to oznacza w praktyce? Tworzymy tablicę tymczasową (zmienna tmp) o rozmiarze większym o 1 od obecnie posiadanej tablicy (czyli równą liczbie unikatowych przedmiotów – już zwiększonej o ten, który właśnie dodajemy). Pakujemy tam wszystkie elementy ze starej tablicy i na koniec dodajemy nowy element. Teraz redeklarujemy rozmiar naszej oryginalnej tablicy i wstawiamy to co stworzyliśmy wcześniej.

Spytacie więc… czemu nie użyłem listy? Dlatego, że lista mogłaby źle traktować nieunikatowe elementy. Np. Gdy miałbym 2 butelki wody i 2 kawałki szynki, musiałbym dwa razy wstawić inta o wartości 2, co lista mogłaby źle interpretować (jako jeden obiekt).

Ostatnia linia, to dodanie wagi przedmiotu do obecnej wagi posiadanych przedmiotów. Znów zakomentowana, bo to zrobimy później.

Jeżeli liczyliście, że to koniec wielkich zmian, to muszę was zmartwić… Jeżeli chodzi o zmianę wykorzystania przedmiotu, to zmienia się tylko to, co w podwójnej pętli (na razie), więc wrzucam tylko to:

Cóż to za zmiany? Najpierw drobna kosmetyczna, mianowicie pozycja przycisku jest obliczana na początku i trzymana jako pomocnicza zmienna. Dodatkowo, jeśli danego przycisku (ikony w menu) nie mamy w liście, to sobie do listy go dodajemy – przyda się, przy wyświetlaniu okienka opisu przedmiotu.

Kolejna zmiana, to fakt, że nie wykonujemy od razu kodu klikniętego przedmiotu. Najpierw sprawdzamy, czy ma on zniknąć, czy jest stały. Jeśli jest przedmiotem użytkowym (np. odnawia manę, dodaje życia etc.) to zmniejszamy liczbę o 1  i wykonujemy jego zadanie. Jeśli liczba wystąpień przedmiotu jest równa 0, to znaczy, że użyliśmy wszystkie egzemplarze przedmiotu i trzeba go „zniknąć” z ekwipunku. Aby to zrobić, kolejno usuwamy go z listy przedmiotów i… przesuwamy wszystkie przedmioty w tablicy liczącej ich liczbę. Zmniejszamy liczbę posiadanych unikalnych przedmiotów.

Po tym wszystkim widać, że łatwiej byłoby zastosować tablicę asocjacyjną. Jednak są z nią pewne problemy. Np. dodanie 2 butelek, kończy się uznaniem ich zawsze za dwa różne obiekty i uniemożliwia to grupowanie przedmiotów. Stąd tak rozwlekłe rozwiązanie.

Zostało jeszcze wyświetlić liczbę posiadanych przedmiotów danego rodzaju.

Wewnątrz ifa, mówiącego że inwentarz jest otwarty, ale za serią tworzącą ikonki wstawiamy nasz kod:

 

Dla porządku. Kod wstawiamy w zaznaczonym miejscu:

Co robi kod? Lecimy po wszystkich przyciskach, i gdzieś w ich dole wstawiamy sobie wartość z tablicy liczby przedmiotów. Tyle.

Ograniczenie wagowe/ilościowe

Teraz, żeby nieco ochłonąć coś znacznie łatwiejszego. Jak nadać ograniczenia wagowe i/lub ilościowe? Możesz odkomentować wszystkie linijki, które komentowaliśmy we wcześniejszym fragmencie. Jak mówiłem, pierwsza przed dodaniem przedmiotu do inwentarza sprawdza czy możemy – tylko trzeba dopisać funkcję, a druga dodaje wagę, a trzeci odejmuje wagę po użyciu przedmiotu. Jednak, żeby waga działała, do zmiennych w Inventory.cs trzeba dodać trzy zmienne: (OK, jedna sprawia, że ograniczenie ilościowe działa)

Oznaczenie jest chyba proste. Dwie pierwsze to maksymalna liczba i waga przedmiotów, a trzecie to aktualna waga niesionych przedmiotów. Czas dopisać funkcję sprawdzającą czy możemy podnieść dodatkowy przedmiot. Całość ląduje w klasie Inventory.cs:

Chyba nie trzeba tutaj niczego tłumaczyć.

Okienko z opisem przedmiotu

Tutaj sprawa jest na szczęście dość prosta, dzięki temu co już napisaliśmy. Wewnątrz pętli, którą napisaliśmy do wyświetlania liczby przedmiotów dodajemy takie coś:

Magii tutaj nie ma, z wyjątkiem ifa. Funkcja Contains sprawdza czy w danym obiekcie typu Rect zawiera się jakaś współrzędna. Nasza współrzędna to pozycja myszki (jeśli chodzi o wysokość musimy odjąć pozycję myszki od wysokości ekranu, bo nie wiedzieć czemu pozycja Y myszki jest liczona odwrotnie niż wszystkiego innego).

Reszta to klasyczne wyświetlenie Labeli i innych fragmentów GUI, aby wypisać co ciekawsze informację.

Wykorzystanie broni

Czas na wykorzystanie innego typu przedmiotu. Zaczniemy od tego co trzeba dodać do… ciągle Inventory.cs. Najpierw kilka bonusowych zmiennych:

handTexture, to tekstura gołej ręki – gdy gracz nie posiada broni. currentWeapon to tekstura dla obecnie posiadanej broni. currentWeaponId służy do określenia, jaka broń w ekwipunku, obecnie jest wyposażona. Ostatnia zmienna określa, gdzie ma się wyświetlać ikona broni.

Teraz ustawiamy wartości kilku ze zmiennych:

Robimy to w funkcji start, zamiast przy deklaracji zmiennej, bo do określenia np. położenia, gdzie wyświetlić miejsce na broń wykorzystujemy np. buttonWidth, które określa się z poziomu Unity, więc skrypt dopiero w funkcji Start wie, jaką on ma wartość.

Teraz szukamy tego ifa:

Bo dopiszemy mu else:

If załatwia przedmioty użytkowe. Co z przedmiotami stałymi, jak broń czy pancerz? Zajmiemy się nimi tutaj. Robimy sobie switcha, czyli takiego ifa, który dostaje zmienną i leci po kolejnym przypadku (case) sprawdzając, czy wartość zmiennej podanej w switch jest równa temu co pomiędzy słówkiem case i znakiem dwukropka. Jeżeli znajdzie odpowiedni przypadek, wykonujemy to, co pomiędzy dwukrokiem, a słówkiem break.

Co robimy w przypadku broni? Jeżeli obecnie wybrana broń jest inna od tej, którą wybraliśmy klikając w menu, to podmieniamy teksturę obecnie wybranej broni, zapamiętujemy index obecnie wybranej broni i wykonujemy zadanie broni, uprzednio wykonując funkcję takeOff, ale tylko gdy jakaś inna broń była założona – o tym później.

Gdy wybraliśmy broń, która aktualnie jest wybrana to znaczy, że chcemy ją zdjąć. W tym wypadku jako obecnie wyświetlana broń zostanie przypisana powrotnie dłoń, a indeks wybranej broni ustawiamy na -1. Wykonujemy też funkcję takeOff.

Ostatnie co zostało to wyświetlić ikonę używanej właśnie broni:

Serio, tylko tyle.

Wszystko bangla, ale żeby całość przetestować, trzeba dodać jeszcze broń. Obiekt tworzymy tak samo jak butelkę wody w poprzednim odcinku. Zmieni się za to skrypt. Tworzymy nowy o nazwie Weapon.cs, który wygląda tak:

W sumie nic strasznego, a całość wygląda znajomo – bo to kopia kodu z eatItem.cs Zmieniamy tylko HP na obrażenia. Niestety dodanie funkcji takeOff, wiąże się ze zmianami w itemAbstrack i etaItem, wyglądają one teraz tak:

Na końcu dopisaliśmy deklarację takeOff – bez tego, skrypt burzyłby się wszędzie, że obiekt typu itemAbstract takiej funkcji nie ma. Niestety wymusza to na nas, dopisanie tej funkcji do eatItem, który jej nie wykorzystuje. Dlatego dopisujemy ją pustą:

I to by było na tyle! Pamiętaj, żeby uzupełnić wszystkie zmienne w Unity!

  • Ale to już wszystko na wersji 5?

    • Od dobrych kilku tutoriali tak. Przy każdym jest podany tag na jakiej wersji był robiony tutorial. Przy czym gdy tutorial w większości opiera się na skryptach to powinien działać i tak na wszystkich wersjach.

  • Eryk Proxy

    Możesz dodać cały skrypt Inventory.cs ?

    • Całych skryptów najczęściej nie dodaję celowo. Gdy jest taka opcja, wiele osób kopiuje skrypt, bez czytania tekstu, po czym pada masa pytań, na które odpowiedzi są w tekście.
      Jeśli masz problem z jakimś konkretnym fragmentem, to mogę przesłać większość kodu, ale raczej wolałbym nie udostępniać kodu w jednym kawałku.

    • Eryk Proxy

      Nie wiem gdzie mam wpisać ten kawałek skryptu:

      Rect btnPosition = new Rect((buttonMargin + buttonMargin * j + buttonWidth * j),

      (buttonMargin + buttonMargin * i + buttonHeight * i),

      buttonWidth,

      buttonHeight);

      if(!buttonsList.Contains(btnPosition)) {

      buttonsList.Add(btnPosition);

      }

      int index = (i * itemsInRow) + j;

      if(index < numerOfItems) {

      if (GUI.Button(btnPosition, itemsList[(i * itemsInRow) + j].getItemIcon())) {

      if(itemsList[index].getDisposable()) {

      itemsCounter[index] = itemsCounter[index] – 1;

      //currentWeight -= itemsList[index].getWeight();

      itemsList[index].execute(ps);

      if(itemsCounter[index] == 0) {

      itemsList.RemoveAt(index);

      for(int d = index ; d < itemsCounter.Length – 1 ; d++) {

      itemsCounter[d] = itemsCounter[d + 1];

      }

      itemsInInventory–;

      }

      }

      }

      } else {

      if (GUI.Button(btnPosition, "")) {

      Debug.Log("Clicked the button!");

      }

      }

    • W poprzedniej części w funkcji OnGUI napisaliśmy sporo kodu i był tam taka podwójna pętla kod leci w środek:

      for(int i = 0 ; i < rows ; i++) {
      for(int j = 0 ; j < itemsInRow ; j++) {
      // Tutaj
      }
      }

    • Eryk Proxy

      Skrypt się już kompiluje ale nie jestem w stanie otworzyć ekwipunku, a przy próbie zebrania obiektu włącza się pauza.

    • Włączenie pauzy może sugerować jakiś błąd. Sprawdź zawartość konsoli, przy czym upewnij się, że włączone jest wyświetlanie błędów. Fakt, że inwentarz się nie otwiera, może spowodowany być jakimiś brakami w kodzie.

    • Eryk Proxy

      Przy próbie otworzenia ekwipunku pojawia się ten błąd:

      InvalidOperationException: Operation is not valid due to the current state of the object

      System.Collections.Stack.Peek ()

      Zaraz po tym włącza się pauza.

    • Podsyłam kodzik w całości, możesz sobie porównać:
      https://dl.dropboxusercontent.com/u/7693850/Inventory.cs

    • Eryk Proxy

      Dzięki za pomoc. Skrypt od inventory już działa. Zrobiłem resztę poradnika i wywala mi ten błąd:

      Assets/Scripts/Weapon.cs(14,21): error CS0533: Weapon.takeOff(PlayerStats)' hides inherited abstract member ItemAbstract.takeOff(PlayerStats)’

    • Prawdopodobnie przy funkcji takeOff w skrypcie Weapon.cs brakuje słówka override, jeżeli słówko jest, to będę musiał zobaczyć cały skrypt.

    • Eryk Proxy

      Już wszystko działa. Wielkie dzięki za pomoc!

  • Pablo111

    Dlaczego skrypt Weapon wywala mi taki błąd „Assets/Skrypty/Weapon.cs(14,21): error CS0533: Weapon.takeOff(PlayerStats)' hides inherited abstract member ItemAbstract.takeOff(PlayerStats)’ ”

    Wszystko sprawdzałem 10x i mam identyczne, nie posiadam jedynie kodu z stackowaniem się itemów bo tego nie potrzebuję, reszta jest myślę że poprawnie

    • Możesz podesłać swoje skrypty (np. linki z wklej.to). Próbowanie określić co jest nie tak bez tego, to trochę wróżenie z fusów. ;)

  • Votan

    Jest jakiś algorytm sortujący obiekty w liście np. wg wagi przedmiotu (od przedmiotu z największą wagą do przedmiotu z najmniejszą)?

  • Eryk Proxy

    Mam problem, kiedy przesuwam cały ekwipunek to liczba przedmiotów zostaje na swoim miejscu, a okienka się przesuwają.

    • Co znaczy, że przesuwasz cały ekwipunek? Zmieniasz położenie kamery?

    • Eryk Proxy

      Zmieniam miejsce, w którym ma się wyświetlać. Chodzi o Inventory Position.

    • To dlatego, że labele są wypisywane po za:
      GUILayout.BeginArea (inventoryPosition);

      Możesz je umieścic wewnątrz tego kodu (sam nie wiem czemu tak nie zrobiłem). Albo w kodzie rysującym opisy dodać po prostu pozycję całego menu.