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

Dzisiejszy odcinek: Prosty 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

Nie ma jej za wiele. W każdym RPG mamy do czynienia z inwentarzem w tej czy innej postaci. Dzisiaj spróbujemy sobie ogarnąć go w bardzo prostej formie. Będziemy zbierać rzeczy, te będą się pojawiać w otwieranym inwentarzu. Do tego przedmiotów będzie dało się użyć. Sam w sobie temat jest dość rozległy, bo inwentarze występują w masie form. Mamy takie z ograniczonym miejscem, z ograniczoną wagą, z przedmiotami zajmującymi jedno pole, zajmującymi kilka pól. Do tego wszystkiego dojdziemy. Jednak od czegoś trzeba zacząć.

Efekt końcowy pierwszego etapu prac
Efekt końcowy pierwszego etapu prac

Takie proste coś stworzymy dziś. Spokojnie, całość będzie bardziej na wypasie, ale nie od razu Kraków zbudowano.

Przygotowanie

Do zrobienia tego tutorialu, potrzeba tylko kilka rzeczy. Postaci gracza (standardowa paczka Characters), jakiejś podłogi (standardowa paczka Prototyping), modelu 3D jakiegoś obiektu (u mnie padło na survivalPack z Asset Store – jest za free i posiada różne przedmioty, więc na jego podstawie ogarnie się potem dużo innych rzeczy), ikony dla obiektów (z racji, że w pierwszym etapie użyłem tylko butelki wody, wziąłem sobie ikonkę butelki z iconfindera).

Iconfindera polecam do szukania ikon, ale pamiętajcie, żeby sprawdzić licencję!

OK. Rozstawiamy to sobie jakkolwiek i bierzemy się za kodzenie.

Przygotowanie gracza

Do gracza przypiszemy sobie dwa skrypty.

Pierwszy skrypt daje w całości (pamiętaj, żeby plik nazwać PlayerStats.cs), bo możecie go kojarzyć. Jest to skrypt wypisujący statystyki, z tutoriala o drzewku umiejętności. Zmieniłem tylko rodzaj i nazwy wypisanych statystyk. Wszystkie zmienne mają mutuatory (getery i setery), żeby łatwo z nich korzystać.

Drugi skrypt, będzie tym o który sobie stworzymy. Zaczynamy od deklaracji zmiennych:

Co tutaj mamy? Pierwsze zmienne deklarują wygląd inwentarza. Kolejno, określamy ile przedmiotów ma być w wierszu, jaka ma być szerokość i wysokość okienka przedmiotu, jaki margines między okienkami, oraz jaka jest pozycja całego inwentarza. Zmienne publiczne, żeby było łatwo to zmieniać.

Kolejna sekcja zmiennych to po pierwsze lista posiadanych przedmiotów (klasę ItemAbstract dopiszemy sobie później, więc póki co wszędzie będzie ją oznaczało na czerwono). Druga zmienna informuje czy gracz otworzył inwentarz (czy mamy go wyświetlić), a ostatnia to odwołanie do skryptu ze statystykami.

O ile ItemAbstract będzie wyświetlać się na czerwono i nie zrobimy z tym nic do póki nie stworzymy tej klasy, o tyle List pewnie też jest oznaczone jako błąd. Temu jednak możemy szybko zaradzić. Wystarczy doimportować bibliotekę:

Czas na określenie zmiennych:

Chyba nie ma tutaj nic do tłumaczenia. Jedynie okiem można rzucić na pozycję inwentarza. Te wyglądające na skomplikowane obliczenia, to zwykłe określanie szerokości i wysokości inwentarza. Jeżeli wiemy ile przycisków ma się mieścić w linii, jakie są ich wymiary i w jakiej odległości mają być od siebie, wyliczenie wymiarów jest proste. A lepiej zrobić to tak, niż zostawić obliczenia człowiekowi, który może się pomylić i zepsuć całość.

Kolejny fragment, który mógłbym zostawić bez komentarza. Jeśli gracz naciśnie I to zmieniamy wartość naszej zmiennej bool na przeciwny. Czyli? Jak mamy otwarty inwentarz to zamykamy, jak mamy zamknięty to otwieramy.

Ta funkcja już obraża swoją prostotą. Potrzebujemy jej, aby pozwolić danemu obiektowi dodać się do listy posiadanych obiektów, gdy go zbierzemy. Czas na największy kawałek kodu. Całość wygląda tak:

Pierwszy krok to sprawdzenie czy mamy wyświetlić inwentarz. W środku ifa, sprawdzamy sobie ile mamy w sumie przedmiotów (funkcja Count na obiekcie List zwraca liczbę elementów w liście). Obliczamy ile wierszy otrzymamy przy takiej liczbie obiektów, dzieląc ich liczbę przez ilość miejsc w wierszu. Dodajemy do całości jeden, żeby zawsze był co najmniej jeden wiersz.

Następnie mamy dwie pętle. Odpowiadają za wyświetlenie wszystkich okienek inwentarza. Jeden to wiersze, drugi to kolumny. Wewnątrz trafiamy na kolejnego ifa. On sprawdza, czy ma wyświetlić okienko puste czy z obiektem (chcemy wypełnić wszystkie wiersze i kolumny, aby nasz inwentarz był równy. Więc gdy braknie przedmiotów, żeby wypełnić wiersz, wyświetlimy kilka pustych okienek).

Czas na sam kod przycisku. Pozwolę go sobie skopiować, żeby było lepiej widać.

Położenie okienka, jest wyliczane na podstawie rozmiaru okienek i tego ile okienek już wyświetliliśmy. Wszystko obudowane dodatkowymi marginesami. Ostatnia zmienna, to to funkcja obiektu ItemAbstract, która zwraca obiekt typu Texture2D, czyli ikonkę zebranego obiektu.

Funkcja execute, wykonuje jakieś zadanie danego obiektu. Piszę tak ogólnie dlatego, że cały system zaprojektowany jest jak najbardziej uniwersalnie i funkcja execute raz będzie dodawała życia, a raz zmieniała pancerz. Efekt funkcji przypisujemy do zmiennej bool. Dlaczego? Bo mój projekt zakłada, że jeśli funkcja zwróci true, to przedmiot był jednorazowy i trzeba go z ekwipunku usunąć (np. butelka wody, jabłko). Tym zajmuje się funkcja RemoveAt. Nie mylić z funkcją Remove! Funkcja Remove, usuwa konkretny obiekt. RemoveAt, usuwa obiekt o podanym indeksie.

Tyle! Inwentarz gotowy. Przypisujemy go do gracza, a graczowi ustawiamy tag Player. Przyda się później.

Tworzymy abstrakcję!

Teraz czas na klasę abstrakcyjną. Czym jest klasa abstrakcyjna? To klasa, która ma jakąś swoją budowę, ale nie może posiadać instancji. Jak to rozróżnić? Jeżeli napiszemy public class MyClass, to stworzyliśmy klasę. Aby stworzyć jej instancję, musimy napisać: MyClass obj = new MyClass(); W przypadku klasy abstrakcyjnej, nie możemy tak zrobić.

Więc po co nam klasy abstrakcyjne? Jeśli chcemy na pewnej grupie klas wymusić pewną budowę, ale nie definiować wszystkiego.

Tak wygląda nasza klasa ItemAbstract. Czym różni się od typowej klasy? Pomiędzy public i class wstawiliśmy słowo kluczowe abstract. Co mamy w samej klasie? Deklarację kilku zmiennych. Przy czym, są to zmienne wspólne dla WSZYSTKICH obiektów. Czy opisywać będziemy broń, pancerz czy przedmiot do wykorzystania (klucz, jabłko, potiona), te zmienne pojawią się wszędzie! Czyli: Ikona obiektu (w inwentarzu), nazwa obiektu, opis obiektu, typ obiektu (to będzie definiowało czy mamy do czynienia z bronią, pancerzem czy czymś innym), waga, cena oraz czy obiekt jest jednorazowego użytku.

Dwie pierwsze funkcję są proste. Zwracamy ikonę – tej funkcji użyliśmy w inwentarzu i nazwę obiektu. Kolejna funkcja to klasyczne OnTriggerEnter. Sprawdzamy czy wszedł w nas gracz i jeśli tak, to znajdujemy skrypt Inventory i korzystamy z przygotowanej wcześniej funkcji addItem by dodać nasz obiekt. Sam obiekt usuwamy ze sceny gry.

Ostatnia funkcja to funkcja abstrakcyjna. Na czym to polega? Na tym, że definiujemy strukturę takiej funkcji (jej nazwę, typ zwracany, dostępność oraz przyjmowane parametry), ale nie mówimy jej jak ma być wykorzystana. Cały myk polega na tym, że obiekt, który dziedziczy po naszej klasie abstrakcyjnej, musi posiadać wszystkie metody abstrakcyjne. Właśnie dziedzicząca klasa – już konkretna klasa, której będziemy używać – definiuje, co ta funkcja właściwie zrobi.

Brzmi może zawile, ale gdy pokaże ostatni skrypt, wszystko się wyjaśni.

Konkretyzujemy obiekt

Dorabiamy ostatni skrypt. Będzie to EatItem.cs:

Co pierwsze rzuca się w oczy? Klasa zamiast po MonoBehaviour, dziedziczy po naszej abstrakcyjnej klasie ItemAbstract. Co nam to daje? Klasa posiada wszystkie wspomniane wcześniej funkcję i pola. Naszym zadaniem jest jedynie rozszerzenie i skonkretyzowanie całości.

Klasa EatItem ma reprezentować obiekty, które… da się zjeść. W moim zamyśle, takie obiekty regenerują życie. Dlatego dodałem zmienną publiczną regenerateHP, która określa, ile życia ma się odnowić.

Druga rzecz, to skonkretyzowanie funkcji execute. Słówko abstract, zostało zastąpione tutaj przez override, które oznacza, że nadpisujemy tamtą klasę abstrakcyjną. Jak widać, budowa jest dokładnie taka sama. Dowolne jest to, co robimy w środku. Ja skorzystałem z geterów i seterów klasy PlayerStats, aby pobrać ilość życia gracza i je zwiększyć o podany parametr. Potem zwracamy zmienną disposable, żeby określić czy obiekt ma być usunięty z inwentarza po usunięciu.

Zmiennej nie muszę definiować, bo zrobiłem to w klasie nadrzędnej i już dysponuje tym polem. Dlaczego nigdzie nie ustalam jego wartości? Bo to zmienna publiczna i designer ustawi ją i tak wedle upodobania.

Tworzymy obiekt do inwentarza

Na starcie zaczniemy od czegoś prostego. Przygotujemy sobie ikonę. Wybieramy w panelu Project naszą ikonę i ustawiamy Texture Type na Editor GUI and Legacy GUI. Drugi krok, to wrzucenie do sceny jakiegoś obiektu 3D, ja wrzuciłem butelkę.

Ustawiamy odpowiedni Texture Type
Ustawiamy odpowiedni Texture Type

Dodajemy do butelki SphereCollider i ustawiamy mu odpowiednio większy promień (radius) oraz włączamy opcję isTrigger.

Teraz przeciągamy na butelkę skrypt EatItem. Zauważysz, że w inspektorze pojawią się opcję ze skryptu EatItem oraz ItemAbstract. To dzięki dziedziczeniu! Zostaje tylko wypełnić wszystkie pola.

Ustawienia zmiennych dla obiektu
Ustawienia zmiennych dla obiektu

Jedno z pytań jakie może się tutaj nasuwać, to: „Czemu lista obiektów w skrypcie Inventory jest typu ItemAbstract, a my dodaliśmy EatItem?” Odpowiedź jest prosta. Jeśli klasa dziedziczy po jakiejś klasie, jest również reprezentantem tej klasy. Prosty przykład. Kwadrat jest figurą. Jeśli go rozciągniemy stanie się prostokątem, ale nie przestaje być figurą. Więc jeśli żądamy podania kwadratu, przyjmiemy tylko kwadrat. Jeśli żądamy figury, to skrypt przyjmie i kwadrat i prostokąt. Dzięki temu, nasz skrypt będzie do inwentarza przyjmował jedzenie, broń, pancerze itd. My będziemy jedynie dopisywać sobie klasy, które będą reprezentować dany typ ekwipunku.

Co zostało do zrobienia? Zmienić butelkę w prefabrykant, dodać kilka prefabrykantów na scenę i przetestować całość. Dla ścisłości: Butelka posiada skrypt EatItem, a obiekt gracza Inventory i PlayerStats. Skrypt ItemAbstract, nie powinien być dodany nigdzie!

Co dalej?

Przyznaje, ten inwentarz jest nieco bidny. W momencie kiedy piszę to zdanie, na liczniku mam już 1913 słów. Było tu sporo teorii dotyczącej polimorfizmu (dziedziczenia) i pojęcia abstrakcji. Nie ma się co martwić. Za tydzień rozbudujemy całość. Co planuję?

  • Grupowanie przedmiotów (gdy mamy np. 3 butelki wody, żeby ikonka była jedna)
  • Opcję wyrzucania przedmiotów
  • Ograniczenie wagowe/ilościowe
  • Krótki opis (po najechaniu myszką na obiekt w inwentarzu, pojawi się okienko z opisem)
  • Ekwipunek (po prawej wyświetlimy postać gracza i pozwolimy go wyposażyć w broń czy pancerz)

Nie wiem czy ogarnę całość za jednym zamachem, ale zrealizuje na pewno wszystkie punkty. Jeśli macie jakieś sugestię, co fajnego można zrobić z inwentarzem – komentarze są wasze.

  • mWin, mógłbyś mi podesłać jakiś tut co znaczy get i set albo coś podpowiedzieć na co to się przydaje?

    • Mutuatory (getery i setery) służą do ustalania i pobierania wartości zmiennej. Dzięki temu, można odwołać się do zmiennej w skrypcie A ze skryptu B.
      Taki zapis jak w przykładzie, oznacza, że możemy dowolnie manipulować zmienną. Zapis np.:
      public int B { get; private set; }
      Oznaczałby, że zmienną można pobrać z innego skryptu, ale nie można zmienić jej wartości.
      Jeżeli nie zraża Cię angielski i chciałbyś poczytać więcej, to tutaj jest ładnie opisane wszystko:
      http://answers.unity3d.com/questions/556033/c-setter-getters.html

    • Dzięki.

  • Powinno pisać „Uwaga! Zagrożenie! Możliwość zarażenia się C#, prosimy o ostrożność!”

  • Świetnia robota, dzięki tobie poznaje unity

  • zrobił byś tut jak zrobić siatkę hexa jak w heroes =D

  • Barti

    Świetne wychodzą Ci te poradniki ;D Jak możesz zrób po ekwipunku budowanie :)

  • Ammon

    Sporo się dowiedziałem :)

    Ale nie wszystkiego co potrzebuję ;/
    Na sam przykład tego, jeżeli ustawię sobie obiekt na scenie chcę aby był on wodą albo chlebem, czyli system sam wylosuje co za ikona ma się pokazać i ile hp ma uzupełniać przedmiot.

    Nie wiem jak dodać i co dodać żeby można było losować między 20 itemami, czyli stawiam na starcie same skrzynie.
    Po odpaleniu gry [void Start] losuje w każdej skrzyni inny przedmiot, dodaje do niego model 3d całą resztę.

    Czyli startuje gra, idę do domu i znajduję kromkę chleba na stole, wychodzę z gry i wracam a tam zamiast kromki leży woda, znów reset a tam leży ciasto

    Sam system losowania ogarniam ale nie wiem jak zrobić zmienianie się dodanego obiektu w inny aby te losowanie ukończyć

    • Zwyczajna tablica np. 20 obiektów, z której losujesz jeden i ten wyświetlasz.

  • Krystian Wysocki

    Mi nie czyta Int’ów. Wyświetla mi się Unexpected symbol „int”.

    • Czy Twój skrypt na pewno posiada dyrektywy:
      using UnityEngine;
      using System.Collections;

      Jest to dziwna sytuacja, która raczej nie powinna wystąpić. Jeżeli dyrektywy są, a mimo to nie działa, to pokaż swój skrypt (przez pastebin), może coś jest nie w tym miejscu co trzeba.

    • Krystian Wysocki

      using UnityEngine;

      using System.Collections;

      using System.Collections.Generic;

      public int itemsInRow = 3;

      public int buttonWidth = 50;

      public int buttonHeight = 50;

      public int buttonMargin = 10;

      public Rect inventoryPosition = new Rect(20, 20, 0, 0);

      private List itemsList = new List ();

      private bool openInventory;

      private PlayerStats ps;

      void Start()

      {

      openInventory = false;

      ps = gameObject.GetComponent ();

      inventoryPosition = new Rect (inventoryPosition.x, inventoryPosition.y, ((itemsInRow * buttonWidth) + ((itemsInRow + 1) * buttonMargin)), ((itemsInRow * buttonHeight) + ((itemsInRow + 1) * buttonMargin)));

      }

      void Update()

      {

      if (Input.GetKeyDown (KeyCode.I)) {

      openInventory = !openInventory;

      }

      }

      public void addItem(ItemAbstract item)

      {

      itemsList.Add (item);

      }

      void OnGUI()

      {

      if (openInventory) {

      int numerOfItems = itemsList.Count;

      int rows = Mathf.CeilToInt(numerOfItems / itemsInRow) + 1;

      GUILayout.BeginArea (inventoryPosition);

      for(int i = 0 ; i < rows ; i++) {

      for(int j = 0 ; j < itemsInRow ; j++) {

      if((i * itemsInRow) + j < numerOfItems) {

      if (GUI.Button(new Rect((buttonMargin + buttonMargin * j + buttonWidth * j),

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

      buttonWidth,

      buttonHeight), itemsList[(i * itemsInRow) + j].getItemIcon())) {

      bool disposable = itemsList[(i * itemsInRow) + j].execute(ps);

      if(disposable) {

      itemsList.RemoveAt((i * itemsInRow) + j);

      }

      }

      } else {

      if (GUI.Button(new Rect((buttonMargin + buttonMargin * j + buttonWidth * j),

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

      buttonWidth,

      buttonHeight), "")) {

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

      }

      }

      }

      }

      GUILayout.EndArea ();

      }

      }

      if (GUI.Button(new Rect((buttonMargin + buttonMargin * j + buttonWidth * j),

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

      buttonWidth,

      buttonHeight), itemsList[(i * itemsInRow) + j].getItemIcon())) {

      bool disposable = itemsList[(i * itemsInRow) + j].execute(ps);

      if(disposable) {

      itemsList.RemoveAt((i * itemsInRow) + j);

      }

      }

      Oto skrypt

    • Tak jak pisałem, prosiłbym o kod wrzucony przez PasteBin, bo w takiej formie nie da się tego kompletnie czytać.

  • Maciek

    A co z takim problemem?
    ItemAbstract.execute(PlayerStats2)' is abstract but it is declared in the non-abstract class ItemAbstract’

    • Prawdopodobnie brakuje Ci słówka Abstract w deklaracji klasy.

    • Maciek

      Nie wydaje mi się, kod jest raczej w 100% zbieżny z Twoim, (pomijając nazewnictwo skryptów). Chodzi Ci o ten fragment? :
      public abstract bool execute (PlayerStats2 ps);
      Dokładnie jest tak zadeklarowany.

    • Maciek

      Zgadza się. Dziękuję :)