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

Dzisiejszy odcinek: Komunikacja skryptów

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

Jest to jeden z najczęściej spotykanych problemów, zwłaszcza u początkujących programistów. Jak zmienić wartość w skrypcie A, będąc w skrypcie B? Dziś omówimy sobie kilka technik, które mogą nam to umożliwić.

Zacznijmy od tego, że mamy kilka przypadków:

  1. Skrypty A i B, są komponentami tego samego obiektu.
  2. Skrypt A jest komponentem w obiekcie nadrzędnym (parent), a skrypt B jest komponentem w obiekcie podrzędnym (child).
  3. Skrypty A i B są komponentami dwóch różnych obiektów.

Skrypty A i B są w tym samym obiekcie

Tutaj sytuacja jest oczywiście najprostsza. Ponieważ do komunikacji wystarczy coś takiego

Dokumentacja: GetComponent<Type typ>()

SecondScript to oczywiście nazwa naszego skryptu, do którego się odwołujemy. Jedyne o czym musimy pamiętać w takim wypadku, jest fakt, że odwołać się możemy tylko do funkcji i zmiennych publicznych!

Skrypty A i B są w relacji Parent – Child

Metoda automatyczna

W tym przypadku, mogłoby być nieco trudniej, ale na szczęście Unity dostarcza fajnych rozwiązań. Otóż skrypt wygląda następująco:

 Dokumentacja: GetComponentInChildren<Type typ>()

W takim przypadku, skrypt będzie poszukiwał pierwszego pasującego komponentu, zaczynając od najmniej zagnieżdżonych obiektów. Jednak, co  w sytuacji, gdy mamy np. dwa tak samo nazwane skrypty, w dwóch różnych childach i chcemy się odwołać do tego bardziej zagnieżdżonego? Wtedy z pomocą przychodzi kolejna metoda!

Metoda ręczna

Sytuacja będzie podobna do pierwszego przypadku, jednak obiekt do którego się odwołamy, znajdziemy sobie sami, a posłuży do tego funkcja Find z biblioteki Transform (co jest istotne!)

Dokumentacja: Transform.Find(string nazwa)

Jako parametr podajemy string, który jest hierarchią. Przykładowo  załóżmy, że mamy taką hierarchię obiektów:

– Gracz
– Lewa Reka
– Prawa Reka
– Pistolet

Jeżeli chcemy znaleźć obiekt pistolet, będąc w obiekcie Gracz, skrypt wyglądać będzie tak:

Jeżeli chcemy znaleźć obiekt pistolet, będąc w obiekcie Prawa Reka, wystarczy coś takiego:

Teraz w zmiennej ourChild mamy dostęp do obiektu pistolet. Odwołanie się do zmiennej w komponencie, będzie analogiczne jak w pierwszym przypadku. Więc skrypt kompletny, da nam coś takiego:

A co z relacją Child – Parent?

Wiemy, że da się odwołać z obiektu nadrzędnego, do obiektu podrzędnego. A co w przypadku, gdy chcemy odwoływać się w przeciwną stronę? Otrzymujemy wtedy bliźniaczą do GetComponentInChild funkcję. Nazywa się: GetComponentInParent, co ciekawe, składniowo nieco inna:

Dokumentacja: GetComponentInParent(Type typ)

Zanika trójkątny nawias, a nazwę komponentu podajemy jak zwykły parametr. Funkcja znów przeszukuje drzewo w górę. Z racji, że dany child, ma zawsze tylko jeden obiekt nadrzędny (parent), nie ma tutaj niejasności. Uniemożliwia to, odwoływanie się w bok. Tzn. Jeśli przypomnimy sobie naszą hierarchię z początku postu, stanie się jasne, że z obiektu prawej ręki, nie przeszukamy obiektu lewej ręki. Jeżeli skrypt uruchomimy z obiektu pistolet, skrypt będzie szukany tylko w obiektach prawa ręka i gracz.

Metoda z indeksem

Jest jeszcze jedna sztuczka. Jeśli znamy indeks dziecka (childa) i będzie on niezmienny, możemy dostać się do obiektu, za pomocą funkcji GetChild, otrzymamy wtedy coś takiego:

Dokumentacja: GetChild(int Indeks)

Może to być szczególnie przydatne, gdy lista dzieci jest stała przez całą grę. Np. Gdy wiemy, że gracz zawsze pod obiektem prawa ręka, ma tylko jednego childa, lub od samego początku gry, do samego końca będzie ich zawsze np. 5.

Skrypty A i B, są w różnych obiektach

Kolizja przyjacielem

Tutaj, właściwie są trzy metody. Najczęściej zdarzy się tak, że obiekt na których chcemy wpłynąć wejdzie z nami w kolizję. Np. zbierając przedmioty, gracz znajduje się w obszarze kolizji z nimi. Co nam to daje? Przyjrzyjmy się funkcji OnTriggerEnter:

Dokumentacja: OnTriggerEnter(Collider kolider) 

Collider other! Other, to nic innego jak obiekt, wchodzący w kolizję z nami! Ponownie, możemy wykorzystać sztuczki z poprzednich przykładów. Jednak, ponownie Unity, dostarcza nam fajne narzędzie. Funkcja SendMessage – bo o niej tu mowa, nie wymaga od nas podania komponentu. Sama znajdzie sobie funkcję w danym obiekcie, oczywiście jeśli ona istnieje i jest publiczna. Wadą tego rozwiązania, jest niestety fakt, że pozwala nam przesłać tylko jeden parametr do funkcji – są sztuczki znoszące to ograniczenie, ale nie zawsze skuteczne. Zobaczmy jak to działa, składnia samej funkcji wygląda tak:

Dokumentacja: SendMessage(string nazwaFunkcji, Object parametr)

Pierwszy parametr, to nazwa funkcji, której szukamy, a druga to parametr.

Dodatkowa Wiedza
Sztuczka!

Jeżeli chcesz przesłać np. dwie lub trzy wartości liczbowe, możesz jako parametr przesłać obiekt typu Vector2 lub Vector3. Dla bardziej skomplikowanych wartości, można nawet utworzyć własną klasę.


Ostatecznie, nasz skrypt, wyglądał by tak:

Stały partner

Czasami, nie dochodzi do kolizji między dwoma obiektami, a potrzebujemy połączyć ich skrypty. Jeżeli obiekty, znajdują się zawsze w grze, możemy to wykorzystać, czyniąc z jednego obiektu ze skryptem, parametr drugiego skryptu. Wystarczy przygotować zmienną publiczną typu GameObject:

Następny krok, to oczywiście przypisanie z poziomu Unity drugiego obiektu na miejsce tej zmiennej (np. przeciągając obiekt).

Teraz sprawdzi, się dowolna z naszych sztuczek, tylko zamiast gameObject, czy other, podawać będziemy partnera. Przykładowo:

Gdzieś na planie

Ostatni, zdecydowanie najgorszy przypadek, to taki w którym wiemy, że obiekt jest, ale nie wiemy gdzie. Jedyne co możemy zrobić, to go szukać. Znów korzystamy z funkcji Find, jednak tym razem, z biblioteki GameObject. Składniowo, nie różni się ona kompletnie od tej, poznanej na samym początku:

Dokumentacja: GameObject.Find(string nazwa)

Jednak tutaj warto pamiętać o tym, że ta funkcja jest potwornie zasobożerna. Użycie jej w funkcji Update, to praktycznie zamordowanie gry. Dlatego mimo, że tak funkcja jest tak prosta i wygodna, powinniśmy korzystać z niej w ostateczności, a do tego w funkcjach wykonywanych raz, takich jak Start czy Awake.

  • Jacob

    Witam, mam problem:

    ControllerScript sc = partner.GetComponent();
    sc.Left();

    error CS0122: `ControllerScript.Left()’ is inaccessible due to its protection level

    Wszystko poprawnie zrobione w ControllerScript jest funkcja Left(); odpowiadająca za chodzenie w lewo, niestety skrypt jej nie widzi. Jeżeli będzie to konieczne umieszczę cały skrypt, aczkolwiek nie widzę sensu, gdyż jest on poprawnie napisany, problem jest z tym, że nie widzi tej funkcji. Czekam na odpowiedź, pozdrawiam.

    • Skrypt funkcje widzi. Tylko nie moze jej uzyc bo pewnie poprzedza ja slowko private (lub nie ma nic, co domyslnie ustawia private). Aby moc uzywac funkcji z innego skryptu, funkcja musi byc publiczna, czyli poprzedzona slowem kluczowym „public”.

    • Jacob

      Dziękuje za szybką odpowiedź :) Po dodaniu public wszystko działa jak należy ;)

    • Deldukan

      Dzień dobry!

    • Deldukan

      Przepraszam, jeżeli moja wypowiedź wskazuje na braki w wiedzy elementarnej lecz dopiero zaczynam pracować z Unity, proszę więc o wyrozumiałość. Tworzę program do gry w szachy. Każda bierka na planszy opatrzona jest skryptem odpowiadającym jej typowi (wieża, goniec itp.), a ponieważ pionek posiada umiejętność promocji, pozostałe bierki muszą pobierać informację o aktualnym typie pionka. Przy wieży, gońcu i innych figurach wszystko odbywa się bez problemów lecz kiedy pionek usiłuje pobrać typ innego pionka powstaje problem, skrypty pionków są identyczne, a zmienne zapisujące typ traktowane jako tożsame. Czy istnieje sposób na rozwiązanie tego kłopotu?

    • Pewnie istnieje, ale w sumie nie bardzo rozumiem co próbujesz zrobić. ;)

  • KAW

    W „A co z relacją Child – Parent?” jest błąd:
    SecondScript sc = gameObject.GetComponentInParent(SecondScript); ,
    a powinno być SecondScript sc = gameObject.GetComponentInParent(); .

  • Michał L

    To nie działa… Mam same błędy. Próbuję połączyć skrypt broni ze skryptem pocisku (pocisk jest tylko w assetach) aby niszczył się po czterech sekundach od wystrzelenia

    • Nie wiem w jaki sposób próbujesz to zrobić i jak wygląda Twój kod, nie znam też treści błędów, to nie jestem w stanie określić czemu nie działa. Domyślam się, że to jakiś NullReference i nie znajduje Ci tworzonego dynamicznie obiektu.

      Ale do osiągnięcia tego co próbujesz zrobić jest łatwiejsza metoda. Domyślam się, że obiekt tworzysz funkcją Instantiate. Zasadniczo, nie musisz mieć żadnej komunikacji. Tylko w skrypcie pocisku w metodzie Start daj sobie takie coś:
      Destroy (gameObject, 5);
      gdzie 5 to liczba sekund. Funkcja zostanie uruchomiona gdy tylko Twój pocisk się pojawi, a to co robi to usunięcie obiektu po 5 sekundach.

  • Michał L

    Trochę nie rozumiem tego poradnika, ponieważ chce (ogólnie) zrobić coś na zasadzie że w jednym skrypcie mamy Gameobject Kuli, a w drugim powiedzmy skalujemy Kulę. Chodzi o to żeby w jednym skrypcie były zmienne drugiego skryptu!

    • Poradnik służy do przekazywania danych między dwoma skryptami. Jeśli chcesz przekazać do skryptu A, wszystkie zmienne ze skryptu B, to prawdopodobnie istnienie skryptu B jest zbędne. Po prostu duplikujesz wszystkie informację. Twoja pamięć raczej Ci za to nie podziękuję.

      Rozważ ograniczenie przekazywanych danych albo przemieszczenie skryptu w inne miejsce. Na 100% odpowiedzią nie są dwa skrypty dysponujące tymi samymi danymi.

  • Michał L

    Chodzi mi o takie przejście z dwoma triggerami że jak przejdziesz przez 1 to w drógim pojawia się informacja że rozmiar kuli to 0,5 i jeżeli rozmiar kuli to 0,5 i przejdziesz przez triggerami to powiększa kule. Taki zwięszacz i zmniejszacz w jednym ;) O to mi chodzi, możesz mi pokazać jak to napisać w kodzie!

    • Nie przesyłaj rozmiaru kuli między triggerami, tylko pobieraj go bezpośrednio z kuli, co jest dużo łatwiejsze, bo kula wchodzi w bezpośrednią interakcję z triggerami.

  • Pingback: Unity3d QuickTip #21 - Współpraca C# i JavaScript | mWin()

  • Marcin

    Witam ma mały problemik otóż postanowiłem wzbogacić mój projekt o Opcje : Dzwiękowe w tym graficzne i z tymi graficznymi mam problem bo skrypt na Opcje jest z moją camerą w relacji Parent – Child i chciałem się odwołac w moim skrypcie na opcje do skrypu na antyaliasing ale nie wiem jak to zrobić napisałem coś takiego. http://pastebin.com/rDBExgBD

    • Witam, pokaż screen ze swojego panelu hierarchy pokazujący hierarchię obiektów oraz screen panelu inspector dla obiektu gdzie masz skrypt i komponent do którego chcesz się odwołać.

    • Marcin
    • I ta konstrukcja kodu nie znajduje Ci tego komponentu? Bo jak na to patrzę, to wygląda, że wszystko jest OK, po za jedną rzeczą, raczej nie używaj GetComponent w funkcji Update, bo wtedy co klatkę pobierasz komponent, co niepotrzebnie obciąża procesor. Wstaw go sobie lepiej do funkcji Start. Może tutaj leży błąd.

    • Marcin

      dzięki za pomoc zaraz to sprawdzę :) wystarczy jeden głupi błąd brak przecinka lub dwukropka może świadczyć o tym czy kod jest prawidłowy czy nie :P

    • Marcin
    • Marcin

      zrobiłem tak jak mi kazałeś nic to nie dało wyskoczył mi błąd tylko że missing component Antialiasing coś tam coś tam… nie napisze ci całego błędu bo później zniknął

    • Bez treści błędu raczej nie pomogę. Jeśli błąd się pojawia tylko w pierwszej klatce, to naciśnij sobie symbol pauzy i dopiero uruchom grę, wtedy ta się zatrzyma na pierwszej klatce.
      Aczkolwiek, błędy nie powinny same znikać z konsoli.

    • Marcin

      problem mam z tą konsolą taki że raz mi się wyswietla bład a drugi raz nie mimo to że błąd wciaz istnieje i program uruchomić się nie chce. Na dodatek jest czasami tak że jesli zamkne konsole i zrobie nawet celowo błąd w kodzie to żaden błąd mi nie wyskakuje i znów program nie idzie odpalić. aaa przy okazji jest jeden problem natury techicznej że jeśli wywołuje przyciski GUI.Button to podaje współrzędne położenia przycisku na sztywno więc nie moge manipulować tym przez jakąś zmienną przez co przyciski przystosowują się do mojej obecnej rozdzielczości monitora czyli 1920×1080 a jesli chce zmienić na mniejszą rozdzielczość to przyciski robią sie wielkie i na dodatek nie zachowują swojego położenia x-y co mogę zrobić żeby przemieszczały się one wraz z rozdzielczością? bo jeśli chodzi o skalacje to nie ma problemu bo w jednym z twoich poradników wyczytałem jak rozwiązać ten problem więc sobię poradzę. :)

    • Możliwe, że w konsoli masz wyłączone wyświetlanie błędów. Możesz pokazać screen samej konsoli to łatwo to sprawdzimy.
      Jeśli chodzi o pozycjonowanie przycisków, to sprawa wygląda bardzo podobnie jak przy skalowniu. Screen.height i Screen.width dają Ci wysokość i szerokość ekranu, więc przykładowo ustawienie pozycji X w taki sposób: Screen.width * 0.1, ustawi Ci przycisk w 10% ekranu od lewej strony.

    • Marcin

      no dobra to jak z tą konsolą jest że mi nie wyświetla błędów ?https://uploads.disquscdn.com/images/fac8aa8d3bb77a68a8c335b6f1e1bbb26af06c4015922a34d594c541f2da7fac.png

    • A konsola wewnątrz Unity?

    • Marcin

      nie mam czegos takiego tylko to

    • W Unity masz wewnętrzną konsolę, która wyświetli Ci błędy. Nie wiem na ile konsola z Visual Studio jest zintegrowana z tą wewnątrz Unity i czy pokaże Ci wszystko.

      Jeżeli nie masz tego okienka z obrazka dostępnego, to możesz je sobie włączyć z: Window -> Console, albo skrótem klawiszowym CTRL + SHIFT + C

      https://uploads.disquscdn.com/images/357d54653faed23b9b508def8071e46d813697336bec157a5cf13544f0d8f683.jpg

    • Marcin

      Dziękuję wkońcu nie bagująca się konsola :D mam jeszcze tle pytań ale już chyba dużo ci życia zatrułem tymi pytaniami :p chyba że masz jeszcze chęć to by bylo świetnie :p

    • Jak nie są związane bezpośrednio z tym poradnikiem, to możesz napisać np. maila ze wszystkimi pytaniami, to w wolnej chwili odpiszę. ;)

  • Michał Burda

    Witam mam problem z dodawaniem script do gracza a pisze dokładnie tak Can’t add script componet ‚player’ because script dass cannot be found. make sure that there are no compile errors and that the file name and class name match

    • Jedno z dwóch:
      1) Albo w konsoli masz jakieś błędy. Wtedy trzeba je rozwiązać.
      2) Nazwa skryptu (pliku) i nazwa klasy wewnątrz nie są identyczne. Wielkość liter ma znaczenie. Więc jak masz nazwę pliku: player.cs, a nazwa klasy to Player, to będzie błąd.

  • Seweryn Woliński

    Niewiem czy dobrze trafiłem, ale pisałeś(chyba ty), że lepiej zrobić własny input menager. Myślisz, że dałoby się zrobić to tak, czy ten sposób jest kiepskim pomysłem?

    • Raczej nie pisałem o tym żeby robić własny manager, bo wyznaję zasadę żeby nie wynajdywać koła na nowo – więc jeśli dostajemy działający i sprawdzony manager od Unity to lepiej go wykorzystać niż męczyć coś od nowa. Wyjątkiem byłaby tu sytuacja kiedy manger od Unity jest zbytnio rozbudowany, bo np. nam wystarczy tylko żeby ludek podskakiwał na kliknięcie spacji. Wtedy aby kod zoptymalizować warto napisać to samemu.