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:

using UnityEngine;
using System.Collections;

public class EventTrigger : MonoBehaviour {

	public delegate void newRoomEnter();
	public static event newRoomEnter RoomEnter;

	void OnTriggerEnter()
	{
		if (RoomEnter != null) {
			RoomEnter ();
		}
	}
}

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:

using UnityEngine;
using System.Collections;

public class ThrowBall : MonoBehaviour {

	public GameObject ball;

	void OnEnable()
	{
		EventTrigger.RoomEnter += Throw;
	}


	void OnDisable()
	{
		EventTrigger.RoomEnter -= Throw;
	}


	void Throw()
	{
		Instantiate (ball, gameObject.transform.position, gameObject.transform.rotation);
	}
}

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:

A method or delegate `ThrowBall.Throw(int)' parameters do not match delegate `EventTrigger.newRoomEnter()' parameters
Assets/ThrowBall.cs(10,30): error CS0428: Cannot convert method group `Throw' to non-delegate type `EventTrigger.newRoomEnter'. Consider using parentheses to invoke the method

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:

using UnityEngine;
using System.Collections;

public class ChangeColor : MonoBehaviour {

	void OnEnable()
	{
		EventTrigger.RoomEnter += TurnColor;
	}


	void OnDisable()
	{
		EventTrigger.RoomEnter -= TurnColor;
	}


	void TurnColor()
	{
		Color col = new Color(Random.value, Random.value, Random.value);
		GetComponent<Renderer> ().material.color = col;
	}
}

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.