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

Dzisiejszy odcinek: Eventy w Unity3d

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

Często zdarza nam się, że chcemy zareagować jakimś kodem na konkretne zdarzenie. Np. wyświetlić napis porażki, gdy gracz zginie. Dodatkowy problem to fakt, że czasami 2 skrypty, będące całkowicie różne, oczekują tego samego zdarzenia. Np. gdy gracz wejdzie do pomieszczenia, mają się w nim pojawić przeciwnicy, światło ma migać, a gdzieś mają wybuchnąć drzwi.

Niby możemy w takim przypadku wstawić sobie w funkcji Update kod czekający na takie wydarzenie, ale… to będzie bardzo niepraktyczne. Gdy takich kodów czekających się nazbiera, to nagle zużyjemy masę zasobów procesora, właściwie nie robiąc nic.

Tutaj z pomocą przychodzą nam eventy, czyli zdarzenia. Jest to taki sprytny kodzik, który właśnie wyczekuje na jakieś zdarzenie. Możemy wyczekiwać na naciśnięcie jakiegoś klawisza na klawiaturze, ruch myszy, wejście w trigger czy naciśnięcie przycisku w GUI.

Dobra, wiemy o co chodzi. Ale jak to zastosować?

Przygotowanie

Do testowania wystarczy prosta scena. U mnie składa się ona z podłogi, dwóch ścian, które tworzą wąskie przejście (symulujemy, że gracz wchodzi do pomieszczenia). Podłoga składa się z 4 segmentów. Dodatkowo dodałem sobie FPSController ze standardowych assetów, jako postać gracza.

Kolejny krok to ustawienie sobie Triggera przy wejściu. U mnie zbudowany jest z Empty GameObjectu, z komponentem BoxCollider w trybie isTrigger. Ten obiekt nazwałem EventTrigger. Tak przygotowana scena wygląda tak:

Przygotowana scena
Przygotowana scena

Żeby bardziej pokazać, jakim fajnym narzędziem są eventy, dodamy sobie dodatkowy GameObject, który będzie pustym GameObjectem zawieszonym nieco w powietrzu, który nazwałem sobie Spawner.

Teraz stwórzmy sobie prosty prefab, który będzie zwykłą sferą (Sphere).

Na razie tyle. Resztę uzupełnimy w trakcie.

EventTrigger

Zaczniemy sobie od skryptu, który wywoła zdarzenie. Tworzymy sobie nowy skrypt o nazwie EventTrigger.cs i dodajemy go od razu do obiektu EventTrigger. Skrypt wygląda tak:

Kluczowe są tutaj 2 pierwsze zmienne.

Pierwszy jest delegat. Czym są delegaty?

Delegaty służą do przekazywania metod jako parametrów. Delegat tworzy nam jakby matrycę, czyli jaki typ zwracany ma mieć funkcja, oraz jakie są wymagane parametry. Trochę rozjaśnia to kolejna linijka.

W drugiej linii mamy znane nam static, czyli zmienna dostępna jest dla wszystkich skryptów. Później mamy słówko kluczowe event, które definiuje nam zdarzenie. Później pojawia się delegat, który określa jakiego rodzaju funkcję, zdarzenie może obsłużyć – to chyba w miarę wyjaśnia o co chodzi z tą matrycą. Na koniec nazwa zdarzenia.

W funkcji OnTriggerEnter wywołujemy zdarzenie. Gracz wszedł do pomieszczania, więc informujemy wszystkie skrypty, że takie zdarzenie miało miejsce, podając jego nazwę z nawiasami okrągłymi, niczym funkcję.

Wcześniej sprawdzamy sobie, czy RoomEnter nie jest nullem, taka sytuacja będzie miała miejsce, kiedy żaden skrypt nie nasłuchuje (nie oczekuje) wystąpienia wydarzenia. Zwyczajnie bez sensu obwieszczać światu „ej, gracz wszedł do pomieszczenia”, kiedy żadnego skryptu to nie obchodzi.

Dzięki temu skryptowi jesteśmy gotowi ogłaszać zdarzenie. Ale teraz fajnie, jak by jakiś skrypt na to reagował.

Reagujące skrypty

Napiszemy sobie dwa skrypty reagujące na event. Zaczniemy od skryptu spawnującego. Tworzymy nowy skrypt: ThrowBall.cs:

Mamy tutaj zmienną publiczną typu GameObject (ten obiekt będziemy spawnować).

Wewnątrz funkcji OnEnable – czyli gdy obiekt się pojawi, albo zostanie aktywowany – wykonujemy sobie prosty kodzik. O co tu chodzi?

EventTrigger to nazwa naszego skryptu, który przetrzymuje event. (Skryptu, nie obiektu! U nas nazywają się identycznie, ale nie musi tak być!). RoomEnter to nazwa, którą nadaliśmy eventowi. Później pojawia się operator dodania i nazwa funkcji (która pojawia się na końcu skryptu). W ten sposób, informujemy event, że ta funkcja czeka na zdarzenie. Teraz chyba jest jasne, czemu sprawdzaliśmy czy event nie jest nullem.

W funkcji OnDisable, czyli jak by przeciwieństwie OnEnable, mamy niemal identyczny kod, różniący się operatorem. Zamiast + mamy -. Po co to? Bez tego kodu, gdybyśmy o tym zapomnieli i w czasie gry, usunęli obiekty ze skryptami czekającymi na event, to event i tak by je chciał wykonać, powodując błędy.

No i została sama funkcja Throw. Co w niej jest ważne? Żeby była skonstruowana tak, jak wskazywał na to delegat. Czyli w naszym wypadku musi to być zwracająca void i bez parametrów. Co wstawimy w funkcji to już nasza sprawa. My tworzymy instancję naszego prefabu. Jeśli struktura funkcji, nie pokryje się z delegatem, w konsoli zobaczymy coś takiego:

W tym przypadku, dodałem tylko parametr int x, do funkcji Throw.

OK. Skrypt przypisujemy do obiektu Spawner i uzupełniamy zmienną publiczną naszym przygotowanym wcześniej prefabem.

Drugi reagujący skrypt

Drugi skrypt wygląda tak:

Jego zadaniem jest zmiana koloru obiektu. Struktura jest identyczna jak w poprzednim skrypcie, więc nie widzę potrzeby tłumaczyć wszystkiego jeszcze raz. Znów ważne, jest aby dodać i usunąć funkcję z nasłuchiwania na event (funkcję OnEnable i OnDisable), oraz żeby funkcja wykonująca faktyczny kod, pokrywała się strukturą z delegatem.

Ja sobie ten kod dodałem do dwóch segmentów podłogi. Teraz jeśli wejdę postacią w trigger i wywołam event otrzymuję coś takiego:

Efekt wejścia w trigger
Efekt wejścia w trigger

Podsumowanie

Najpierw jedna sprawa wyjaśnienia, część z was może się zastanawiać, czemu event jest ustawiony jako zmienna statyczna, a nie zwykła zmienna publiczna. Odpowiedź jest dość prosta. Dając dostęp publiczny, moglibyśmy programistycznie nieco namieszać w kodzie. Zmienna statyczna pozwala łatwo dodawać i usuwać kolejne funkcję, ale nie możemy sobie tego w żaden sposób nadpisać, czy usunąć.

Wyjaśnijmy sobie też inną rzecz. Dlaczego eventy są takie fajne? Powodów jest kilka:

  • Nie musimy sprawdzać co klatkę czy coś się stało, tylko gdy nastąpi oczekiwane wydarzenie, to wszystkie nasłuchujące skrypty są o tym informowane. Dzięki tego oszczędzamy sporo mocy obliczeniowej.
  • Skrypt wywołujący event, nie musi nic wiedzieć o innych, po za tym kogo ma poinformować. Dzięki temu, każdy wywołany skrypt może zrobić kompletnie co innego. Tak jak u nas jeden zmieniał kolor, drugi tworzył obiekt.
  • Wywołane skrypty, nie muszą wiedzieć o sobie. Skrypt zmieniający kolor nie musi wiedzieć, że skrypt tworzący obiekt istnieje. Po prostu gdy zostanie wywołany, robi swoje.

Ostatecznie? Eventy to bardzo fajne narzędzie do optymalizacji i porządkowania kodu.

 

  • Chom1k

    Witam, warto też wspomnieć, że istnieje „domowa” wersja eventów w Unity – UnityEvents, są one serializowane więc można ich używać w edytorze, bardzo fajna sprawa, często pomijane i niedoceniane. W porównaniu do typowo C# eventów są znacznie wolniejsze ale przy normalnym użytkowaniu raczej niezauważalne :) Ogółem fajnty QuickTip, biorę się za kolejne!
    Pozdrawiam!

  • wojak

    A jedno zdarzenie może mieć kilka triggerów? Oczywiście w różnych klasach.

    • Jeśli trigger w innej klasie będzie miał identycznie zbudowanego delegata to nie ma problemu. W przeciwnym wypadku, musisz sobie dopisać funkcję, która delegata obsłuży.

      Tzn. jeśli Trigger1 oczekuje funkcji o układzie: void funkcja(void) – takiego jak w przykładzie), a Trigger2 oczekuje void funkcja(int) – inta jako parametr, to wtedy potrzebujesz 2 funkcji, idąc znów przykładem:
      void Throw() i void Throw(int x)
      Nie musisz w drugiej wykorzystać parametru, ale musi obsłużyć delegata. Jeśli w obu delegat będzie ten sam, to tylko trzeba dopisać odpowiednią linijkę w OnEnabled i OnDisabled.

    • wojak

      Chmm… nie do końca rozumiem. Jeśli dałem EventTrigger.RoomEnter() wywaliło mi błąd że event obsługuje tylko instukcje += i -=. Dokładniej chodziło o to że jeśli zabije jakiegoś zombie to wywoła on event który został zdeklarowany całkiem innej klasie (dobrze mówie zdeklarowany?) a odbiera to tylko jeden skrypt który sprawdza ile zombie zostało.

    • W tej drugiej klasie nie wywołuj tego zdarzenia, tylko stwórz sobie analogiczne zdarzenie i przy funkcjach gdzie dodawałeś nasłuchiwanie zdarzenia 1, dodaj też nasłuchiwanie zdarzenia 2. Tak powinno zadziałać.

    • wojak

      Tak tylko problem może być w tym że na początku jest np. 5 zombie a potem już 15 tak więc raczej nie tędy droga. Zamiast robić zdarzenie zrobiłem sobie statyczną zmienną tyle że w każdej klatce muszę sprawdzić czy przypadkiem nie jest prawdziwa (true).

    • Wtedy lepszym rozwiązaniem dalej są eventy. Sprawdzenie co klatkę zmiennej to if w każdej klatce. Event wykona się raz, wtedy kiedy go wywołasz.

      Ewentualnie możesz opisać, co próbujesz zrobić, bo może istnieć inne, lepsze rozwiązanie problemu.

    • wojak

      No tak jak mówiłem. Chciałem zrobić system który kończy falę w momencie gdy wszystkie zombie zginą, jednak wyszukiwanie co klatkę ile obiektów pozostało raczej nie widzi mi się jako dobre rozwiązanie. Tak więc próbowałem z eventami. Jednak jak mówię jeśli w skrypcie zombiaka próbuje wywołać eventa dostaje błędy o których pisałem wcześniej. Jeśli utworze kilka eventów o takiej samej nazwie zapewne otrzymam komunikat o duplikacji, no a jeśli inne nazwy to nawet nie wiem jak je generować (pytanie jak generować nazwy (o ile się da) jak byś dał rade to walnij jakiegoś linka do c a nawet asm), przy czym raczej bezsensu jest obserwowanie np 20 eventów które robią to samo. Generalnie no to event miałby się wywołać gdy zombie umrze.

      Mam jeszcze takie pytanie czy jest możliwość żeby w skrypcie napisać jakieś działanie które obliczałby kompilator?? Chodzi o to że mam np. 3 stałe i na ich podstawie kompilator powinien wyliczyć coś (np. początkowy stan jakiejś zmiennej). Do tego jakaś osobna biblioteka, czy coś?

      I ostatnia sprawa wpadłem na taki pomysł: pomagasz wielu osobom które zadają Ci pytania w komentarzach. Czemu odpowiedzi na te pytania regularnie nie umieszczać na jakiejś stronie? Stworzył by coś w rodzaju FAQ na których ludzie znajdowali by przydatne odpowiedzi. Rozumiem że nie które pytania sprowadzają się „skopiowałem skrypt, czemu wywala błędy?” jednak te bardziej ogólne myśle bez problemowo by przeszły. Jednak ostatecznie to twoja strona i sam stwierdź co chcesz i co jesteś w stanie zrobić :)

    • Takie coś. No to moim zdaniem 2 proste rozwiązania:
      – Na starcie tworzysz listę zombiaków. Jak któregoś uśmiercasz to usuwasz go z listy
      – To samo, tylko masz zmienną typu int.
      Wtedy całość trzymasz w jakimś pustym GameObjecie, do którego dostęp ma każdy inny skrypt. Taki GameController. Albo robisz go statycznym, albo każdy skrypt który go potrzebuje, wyszukuje go w funkcji Awake.

      Jeśli chodzi o kompilator, to nie mam pojęcia. Ale w sumie jeśli to faktycznie stałe, to wynik zawsze będzie ten sam, możesz go policzyć ręcznie i wprowadzić wartość.

      W sumie ciekawy pomysł, mógłbym rozważyć. Ale teraz byłoby właśnie o tyle ciężko, że faktycznie zdecydowana większość to „Skopiowałem skrypt, ne przeczytałem poradnika i nie wiem czemu nie działa”. :)

    • wojak

      Dzięki! W sumie nie wiem czemu nie wpadłem na pomysł zmniejszania liczby zombiaków zamiast za każdym razem wyszukiwania. A jak teraz myślę to z poziomu skryptu zombie mogę wywoływać funkcje (eh zaczynałem od c) czyli obiekt który będzie zmniejszy a następnie sprawdzi czy jest więcej niż 0.

      I mam pytanie często używasz zmienne typu byte, sbyte lub short? Niby mniej zajmują jednak nie wiem czy za to dostęp do nich nie jest dłuższy.

    • Praktycznie w ogóle. Głównie string, bool, int, float i double z tych podstawowych. ;)

      Ja zaczynałem od PHP. :)

    • wojak

      Dzięki za pomoc. Rozważ moją propozycję. :) Zdrówka.

    • Tworzysz sobie klasycznie instancję, ale przypisujesz ją do zmiennej:
      GameObject zombie = Instantiate ( … )
      A potem robisz taki myk:
      zombie.transform.parent = JakisGameObject;