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

Dzisiejszy odcinek:  Scriptable Objects, część pierwsza.

Dzisiejszy odcinek powstaje we współpracy z Marcinem Sroczyńskim, który już raz pisał dla was. Tym razem wraca z bardzo ważnym tematem jakim są Scriptable Objects. Artykuł będzie miał dwie części. Ta jest bardziej teoretyczna, druga będzie pokazywała pełen potencjał tego narzędzia. Ja już nie przedłużam i oddaje głos Marcinowi.

Wprowadzenie

W ostatnim czasie w ręce wpadło mi niesamowicie przydatne narzędzie, o którym z niewiadomych dla mnie powodów nie wiedziałem. O czym mowa? O ScriptableObjects. Być może wielu z Was również o tym, nie słyszało, a muszę przyznać, że wykorzystanie tej właśnie klasy przy tworzeniu np. Inwetarza jest bardzo, bardzo przydatne.

Poradnik podzielę na 2 części. W części pierwszej zajmę się ogólnym omówieniem tematu i posłużę się bardzo prostym przykładem w celu wyjaśnienia czym są ScriptableObjects. I w zasadzie na 1 części można by skończyć, jednak sam nienawidzę suchej wiedzy bez konkretnych przykładów, dlatego w drugiej części zajmiemy się już konkretnym zastosowaniem omawianego tematu.

Zatem już bez przedłużania zabierzmy się do roboty.

Teoria

Czym w ogóle jest ScriptableObject ?

ScriptableObject to klasa, z której możemy dziedziczyć zupełnie tak samo jak dziedziczymy z klasy MonoBehaviour. Obie te klasy dziedziczą z bazowej klasy Unity – klasy Object. Klasę tę używamy w przypadku, gdy chcemy utworzyć obiekt, który nie może zostać dołączony do żadnego obiektu GameObject.

Tak na chłopski rozum to klasa ScriptableObject umożliwia nam po prostu stworzenie elementu bezpośrednio w Assetach i przeznaczona jest do przechowywania danych (na prawdę wielu danych – w dokumentacji wspominają o milionach typów np. intów).

Przygotowanie

W przykładzie utworzymy 2 obiekty przechowujące dane. Danymi będzie pozycja (Vector3), oraz kolor obiektu (Color). Następnie utworzymy skrypt, który korzystając z tych danych utworzy nam 2 kostki w pozycjach i kolorach określonych w naszych kontenerach danych.

Na początku, tworzymy foldery dla assetów które znajdą się w naszym projekcie. Na pewno będziemy potrzebować foldery: „Scenes”, „Scripts”, „Data”. W folderze „Scenes” zapisujemy naszą scenę z dowolną nazwą.

Przygotowujemy Scene

Następnie utworzymy 2 skrypty:

  • MyScriptableObject.cs – skrypt, który będzie dziedziczył z klasy ScriptableObject, czyli będzie zawierał dane,
  • UseMyScriptableObject.cs – skrypt, w którym zajmiemy się użyciem danych z wyżej utworzonego skryptu.

Ważne, aby MyScriptableObject zamiast dziedziczyć z MonoBehaviour dziedziczył ze ScriptableObject. Dodajemy nasze 2 zmienne: Vector3 objectPosition oraz Color objectColor. Prz czym pamiętamy o tym aby zmienne te były publiczne.

Następnie nad nazwą naszej klasy dodajemy atrybut [CreateAssetMenu()]. Atrybut ten dodaje nowy element do edytora w Assets/Create, czyli mamy do niego dostęp zarówno z górnego paska, jak i po kliknięciu prawym przyciskiem myszy.

Dzięki [CreateAssetMenu()] możemy dodawać nasz skrypt z górnego menu.
[CreateAssetMenu()] umożliwia dodania skryptu z poziomu menu kontekstowego.
Atrybut ten ma też swoje opcjonalne parametry, które możemy podać w nawiasach. Przykładowo:

  • fileName = „MyObject” – nadaje nazwę obiektowi, który utworzymy poprzez Assets/Create
  • menuName = „MyMenu/MyObject” – tworzy podfolder MyMenu, w którym znajdzie się obiekt MyObject
  • order = 1 – umieszcza elementu na danej pozycji(tutaj na 1 pozycji)

Możemy sobie skonfigurować co się ma wyświetlać w menu. Wykorzystany kod: [CreateAssetMenu(fileName = „MyObject”, menuName = „MyMenu/MyObject”, order = 1)]
Teraz utwórzmy sobie 2 obiekty danych. Ja nazwę je MyObject1 oraz MyObject2.

  • MyObject1 będzie miał pozycję (x, y, z) = (7, 4, -1) oraz kolor zielony
  • MyObject2 będzie miał pozycję (x, y, z) = (-1, -2, 0) oraz kolor czerwony
Obiekt MyObject1
Obiekt MyObject2

Utwórzmy sobie jeszcze kostkę na scenie i zapiszmy ją jako prefab (przeciągając kostkę do panelu Project), żebyśmy mogli z niej za chwilę skorzystać. Utworzoną kostkę wrzućmy do nowego folderu Prefabs.

Tworzymy prefab kostki

Teraz zajmiemy się stworzeniem kostek na pozycjach i kolorach zawartych w naszych Data Containerach.

W tym celu otwórzmy skrypt UseMyScriptableObject.cs i dodajmy do niego 3 prywatne zmienne z atrybutem

Dodatkowa Wiedza
Dla osób, które nie spotkały się wcześniej z atrybutem [SerializeField].
Standardowo w Unity z poziomu edytora mamy dostęp do zmiennych publicznych. Tworzenie wszystkich zmiennych jako publiczne nie jest prawidłowe, ponieważ do tych zmiennych mają dostęp wszystkie pozostałe klasy.

Atrybut [SerializeField] pozwala nam na edycję zmiennej (obiektu) w edytorze tak jakby była ona publiczna przy czym jej modyfikator dostępu pozostaje prywatny, czyli żaden skrypt nie jest w stanie odczytać ani zmodyfikować tej zmiennej. Oczywiście poza skryptem, w którym utworzyliśmy tę zmienną

Teraz na scenie tworzymy pusty obiekt, który nazwiemy UseScriptableObjects, i dodajemy do niego nasz skrypt UseMyScriptableObject.cs. Dodajmy w puste miejsca referencje do naszych obiektów: kostki, oraz 2 kontenerów z danymi

Wypełniamy referencję do obiektów

I na zakończenie wchodzimy w edycję skryptu UseMyScriptableObject.cs. w funkcji Start tworzymy 2 instancje naszej kostki i przypisujemy pozycje oraz kolory. Po uruchomieniu gry powinniśmy zobaczyć stworzone 2 kostki z 2 różnymi kolorami w 2 różnych pozycjach.

Efekt Końcowy

Podsumowanie

Przykład zastosowany tutaj był bardzo prosty i miał na celu zaprezentowanie działania klasy ScriptableObject. W manualu podana jest też zaleta stosowania ScriptableObjects. Pozwolę, sobie przetłumaczyć przykład tam podany, aby był zrozumiały dla wszystkich.

Dokumentacja Unity
Jeżeli weźmiemy na przykład prefab ze skryptem zawierającym tablicę miliona integerów, to taka tablica zajmie 4MB pamięci. Za każdym razem, gdy stworzona zostanie instancja takiego obiektu zostanie stworzona kopia tej tablicy. W efekcie tworząc 10 instancji obiektów będziemy mieli 40MB pamięci zajętej przez 10 milionowych tablic z danymi.

Jeżeli natomiast zamiast przechowywać tablicę przechowywalibyśmy tam ScriptableObject, wówczas zostałoby utworzonych 10 instancji obiektów, które posiadają referencję do ScriptableObject, który zajmuje 4MB pamięci.

W efekcie zajmujemy tylko 4MB, a nie 40MB pamięci – spora różnica.

Na samym końcu chciałbym podać dwie moim zdaniem istotne zalety stosowania takiego typu przechowywania danych:

  • Po pierwsze ScriptableObject są bardzo proste w użyciu oraz wprowadzają porządek w warstwie danych naszego projektu. Na przykład przy tworzeniu systemu broni wszystkie dane o każdej z broni możemy zapisać w assetach i w dowolnym momencie je modyfikować.
  • Po drugie i to uważam za niesamowicie istotny element, przy mergowaniu projektu z osobami z zespołu. Szansa, że dane o naszych broniach zostaną w jakiś sposób uszkodzone lub zniszczone są praktycznie zerowe.

Niestety wielokrotnie zdarzało mi się, że dane o obiektach, przetrzymywane w tablicy czy na liście w scenie po błędach podczas mergowania zostawały usunięte. Trzeba było wówczas wszystkie dane wpisywać na nowo i niepotrzebnie tracić czas i nerwy.

Przy korzystaniu ze ScriptableObjects, dane z listy ze sceny mogą zostać usunięte ale wtedy zaznaczam wszystkie elementy z folderu assets i przerzucam je na nowo, nie martwiąc się o zawarte w nich dane.

Dodatkowe materiały:

  • https://docs.unity3d.com/ScriptReference/ScriptableObject.html
  • https://docs.unity3d.com/Manual/class-ScriptableObject.html
  • https://docs.unity3d.com/ScriptReference/SerializeField.html
  • https://docs.unity3d.com/ScriptReference/CreateAssetMenuAttribute.html
  • https://unity3d.com/learn/tutorials/modules/beginner/live-training-archive/scriptable-objects

Dla ambitnych:

  • W kolejnej części wykorzystamy ScriptableObjects do stworzenia prostego systemu inwentarza do przechowywania broni. Spróbuj taki projekt wykonać samodzielnie, aby móc go porównać z gotowym rozwiązaniem.
  • Mateusz Grzonka

    W dodatkowej wiedzy jest błąd, „Standradowo”