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

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

Bawimy się już z Unity od dłuższego czasu. Ale często zdarza się, że mamy z naszymi grami jakieś problemy. Ja problemy dziele na dwa rodzaje, dlatego ten QuickTip będzie dwuczęściowy. Pierwszy rodzaj błędów, to błędy związane z samym Unity. Nie wyświetla się coś co powinno, albo wyświetla coś co nie powinno. Promień z Raycasta pada nie tam gdzie trzeba itp. Tym zajmiemy się w drugiej części. Dziś za to, na tapetę weźmiemy błędy związane ze skryptami, czyli tak zwane Exceptiony (czyt. Eksepszyn) – z polskiego: wyjątek.

Anatomia wyjątku

Zacznijmy od tego, skąd się biorą wyjątki i gdzie się o nich dowiemy? Wyjątki wynikają z sytuacji, gdy coś skiepścimy w kodzie. Tak ogólnie. Począwszy od tak błahej sprawy jak niedomknięty nawias czy brak średnika, przez złą nazwę pliku lub klasy, po wyjście po za zakres tablicy i inne dziwne (wyjątkowe) sytuację, które nie powinny mieć miejsca.

Wyjątkami najczęściej obdarowuje nas konsola.

Konsola w Unity3d
Konsola w Unity3d

To co jest ważne, to aby mieć zaznaczone w prawym, górnym rogu wyświetlanie błędów (ostatnia ikonka – coś na styl znaku stopu z wykrzyknikiem). Ikona  w kształcie trójkąta z wykrzyknikiem to ostrzeżenia (Warning). Nie powinniśmy ich mieć w kodzie, ale nie uniemożliwiają uruchomienia aplikacji. Więc jeśli tylko masz możliwość, staraj się usuwać ostrzeżenia. Jednak my, skupimy się na bardziej zabójczych i blokujących zabawę błędach.

Wszystkie wyjątki, niezależnie od typu mają tą samą składnie:

Assets/Scripts/EnemyAI.cs(10,14): error CS1519: Unexpected symbol `public' in class, struct, or interface member declaration

Czyli co tu mamy? Pierwszy fragment, to oczywiście plik wraz z lokalizacją w którym pojawił się błąd. Dane w nawiasie są niemal kluczowe. Jest to linia i kolumna kodu, gdzie wykryto błąd. W dużej liczbie przypadków, wystarczy udać się do wskazanej linii by znaleźć błąd.

Kolejna część, to kod błędu. Z reguły nic Ci nie powie. Ostatni fragment, to informacja o rodzaju błędu na jaki trafił kompilator (kompilator, to taki sprytny programik, co zmienia kod napisany przez nas, na taki zrozumiały dla komputera).

Może też się pojawić, jeszcze jedna składnia błędu:

IndexOutOfRangeException: Array index is out of range.
EnemyAI.Start () (at Assets/Scripts/EnemyAI.cs:19)

Różnice są niewielkie. Najpierw dostajemy rodzaj błędu, potem nazwę skryptu wraz z funkcją w której się pojawia, dopiero potem plik wraz ze ścieżką, a po dwukropku linię w której błąd napotkano.

Więc tak. W większości przypadków, wystarczy udać się do wskazanej linii i poprawić błąd. Jednak łatwiej go znaleźć jak wiemy czego szukamy. W sumie wystarczy, że sobie przetłumaczysz opis błędu i będziesz z grubsza wiedzieć o co chodzi. Jednak, żeby było łatwiej, omówimy sobie najczęściej pojawiające się błędy.

Jeszcze jedna uwaga. Czasami błędy nie sprawiają, że gra się wysypie. Tzn. mogą się pojawić w konsoli, ale nie uniemożliwić granie. (np. ten IndexOutOfRangeException tak zrobił). Dlatego należy uważnie obserwować w czasie gry dolny pasek Unity, który błędy będzie wyświetlał.

Ostatnia uwaga. Jeśli kiedyś zdarzy Ci się zobaczyć coś takiego:

Mała lista błędów
Mała lista błędów

Nie popadaj w panikę! Jeśli przyjrzysz się wszystkim komunikatom, zobaczysz że błędy znajdują się w liniach: 10, oraz 35-49. Sugeruje to, że prawdopodobnie, tak naprawdę mamy do czynienia z jedynie dwoma błędami. Ale jeden sprawia, że kompilator uważa, iż jest ich 14. W linii 10 usunąłem jedynie średnik. Za to linie 35-49 wyglądają tak:

Kod wywołujący błąd w liniach 35-49
Kod wywołujący błąd w liniach 35-49

Od razu widać co powoduje problem. Funkcja OnTriggerStay nie posiada nawiasu otwierającego. Przez to, skrypt uznaje za błąd, wszystko co mamy między pierwszym nawiasem otwierającym, a pierwszym zamykającym jakie napotka od miejsca błędu.

Czego nas uczy ten przykład? 10 błędów, może wynikać z braku jednego znaczka i nie należy panikować na ich widok. Po drugie, zauważmy, że linie błędu zaczynają się w linii 35, a klamra otwierająca funkcję, normalnie mieściłaby się w linii 33 lub 34. Wniosek? Linia błędu jest wskazówką, czasami błąd może być lekko nad, lub pod.

No to ogólne zasady znamy. Poznajmy teraz…

Najczęściej pojawiające się wyjątki w Unity3d

Omawiał będę oczywiście C#, ale błędy JS będą wyglądałby podobnie, jak nie identycznie. Kolejność przypadkowa.

Unexpected symbol ‘(‘ in class, struct, or interface member declaration

Jest to błąd z naszego przykładu. Sugeruje, że gdzieś w kodzie znalazł się znaczek, którego parser nie oczekiwał. Czyli najczęściej będzie to jakiś błąd składni języka. Np. brak nawiasu, cudzysłowów, średnika czy nadmiar tychże. Ew. wstawiony jakiś niepoprawny znak. Np. wstawienie dowolnego znaku zamiast <, > czy = w warunku if. Te błędy najczęściej zaznaczy nam już samo IDE (MonoDevelop) podkreślając wadliwy kod na czerwono.

Co zrobić? Poprawić błąd

IndexOutOfRangeException: Array index is out of range. 

Tutaj sprawa jest bardzo prosta. Wyszliśmy po za zakres tablicy. Kiedy tak się zdarza? Jeśli mając tablice złożoną z 3 elementów, odwołasz się do elementu 4. Elementy liczone są od 0! Czyli jak masz tablicę:

int numbers = new int[10];

Wtedy dozwolonymi elemntami są numbers[0] do numbers[9].  Błąd pojawi się również, gdy odwołasz się do elementu ujemnego.

Co zrobić? Sprawdzić jak duża jest nasza tablica i poprawić zakres (najczęściej błąd pojawia się w pętlach gdy pracujemy na tablicy).

Cannot implicitly convert type ‘float’ to ‘int’. An explicit conversion exists (are you missing a cast?)

Mogą się tam pojawiać inne typy niż float i int. Może być string, double, GameObject, GUITexture itp. O co chodzi? Próbujesz do obiektu (zmiennej) jakiegoś typu, przypisać zmienną innego typu. W tym przypadku do zmiennej typu int, próbuję przypisać zmienną typu float.

Co zrobić? Pierwsza opcja jest taka, że pomyliłeś typy i wystarczy je zmienić. Wariant drugi, wymaga rzutowania jednego typu obiektu na drugi. Czyli zamiast: int hp = 5.4, napiszesz: int hp = (int) 5.4 – ten przykład jest bez sensu, ale pokazuje o co chodzi.
W przypadku obiektów Unity, możesz brakować odniesienia. Tzn.:

GameObject obj;
Transform tf = obj;

Rzuci błędem. Poprawny kod, będzie wyglądał tak:

GameObject obj;
Transform tf = obj.transform;

The name ‘demage’ does not exist in the current context

Błąd mówiący o tym, że zmiennej (tutaj o nazwie demage), nie ma w danym kontekście. Czyli… Odwołujemy się do jakiejś zmiennej w funkcji, ale ta zmienna jest niezadeklarowana, czyli kompilator nie wie czym to w ogóle jest.

Co zrobić? Zadeklarować zmienną. w danej funkcji, albo jako zmienną dla całej klasy. Ew. mogłeś chcieć podać tą zmienną jako parametr funkcji.

Only assignment, call, increment, decrement, and new object expressions can be used as a statement

Takie coś zobaczymy, kiedy wykonamy wyrażenie. Czyli linię kodu złożoną z jednej strony (normalna składnia to polecenie operator polecenie, np.: zmienna = 4). Takie coś jak inkrementacja (zmienna++;), dekrementacja (zmienna–;), funkcja (fun();). Więc jeśli zrobimy np.: animator.bodyPosition, zobaczymy taki błąd, bo taki parametr oczekuje, że po niej będzie znak równości i nowa wartość. Czyli: animator.bodyPosition = new Vector3(0, 0, 0); Oczywiście jest to przykład. Może chodzić o zupełnie inny obiekt.

Co zrobić? Dodać wartość.

Type ‘FirstPersonController’ does not contain a definition for ‘CanRun’ and no extension method ‘CanRun’ of type ‘FirstPersonController’ could be found (are you missing a using directive or an assembly reference?)

Taki magiczny błąd zobaczymy gdy chcemy wywołać jakąś funkcję lub odwołać się do zmiennej innego skryptu, a ten skrypt takowej nie posiada.

Co zrobić? Dopisać brakującą funkcję. Jednak, może tak się zdarzyć, bo brakuje nam np. odpowiedniej przestrzeni nazw, wtedy stosujemy słówko using. Jednak częściej, zwyczajnie popełnimy literówkę, lub zapomnimy dopisać funkcję/zmienną.

The best overloaded method match for ‘UnityEngine.Object.Destroy(UnityEngine.Object, float)’ has some invalid arguments

Dość prosty error. W wykonaniu jakiejś funkcji podaliśmy złe typy argumentów. W przykładzie wywołanie funkcji Destroy oczekuje obiektu i ew. zmiennej float. Programista musiał podać np. obiekt i string, aby otrzymać taki błąd.

Co zrobić? Poprawić zmienne, lub dokonać rzutowania na odpowiedni typ.

MissingComponentException: There is no ‘CharacterController’ attached to the “FirstPersonCharacter” game object, but a script is trying to access it. You probably need to add a CharacterController to the game object “FirstPersonCharacter”. Or your script needs to check if the component is attached before using it.

Dość długie, ale znów proste. Idąc przykładem, w obiekcie FisrtPersonCharacter nie odnaleziono komponentu CharacterController, kiedy skrypt próbuje się do niego odwołać. Często występuje przy funkcji GetComponent.

Co zrobić? Dodać brakujący komponent. Jeśli komponentu może nie być, dodać ifa, który przed odwołaniem sprawdzi, czy taki komponent istnieje w obiekcie do którego się dobieramy.

NullReferenceException: Object reference not set to an instance of an object

Bardzo częsty błąd. Operujemy na zmiennej, która nie została zainicjowana. Tzn. tworzymy sobie zmienną jakiegoś typu, ale nie przypisujemy jej wartości, po czym próbujemy na niej operować. Np.:

Kalkulator calc;
calc.dodaj(2, 5);

Kod symuluje obiekt kalkulatora, który dodaje dwie zmienne. Tutaj dostaniemy ten błąd. Co zrobić? Dodać instancję obiektu:

Kalkulator calc = new Kalkulator();
calc.dodaj(2, 5);

Obiekt, który zwraca np. funkcja instantiate, również można przypisać jako wartość zmiennej.

Co zrobić? Stworzyć instancję obiektu, zamiast tylko go deklarować.

SendMessage takeHit has no receiver!

Nagminny wręcz problem. Przy wywołaniu funkcji SendMessage. Oznacza ni mniej ni więcej, że funkcja do której próbujemy się odwołać, nie może być znaleziona. Przyczyna problemu? Najprościej, jeśli popełniliśmy literówkę w nazwie funkcji, albo funkcji nie napisaliśmy. Gorszy problem, to jeśli szukamy w złym komponencie, albo złym obiekcie.

Co zrobić? Sprawdzić poprawność podanej nazwy funkcji i tej zadeklarowanej. Sprawdzić czy obiekt w którym szukamy funkcji. posiada odpowiedni skrypt zawierający funkcję. Sprawdzić czy obiekt na którym wywołujemy SendMessage w ogóle istnieje i został poprawnie podany.

The left-hand side of an assignment must be a variable, a property or an indexer

Błąd pojawi się, gdy próbujemy przypisać wartość za pomocą znaku równości, do czegoś co nie może przyjąć wartości w ten sposób. Przykładowo, gdy próbujemy podać w ten sposób wartość do funkcji, przykładowy błędny kod może wyglądać tak:

gameObject.transform.Translate() = new Vector3(0, 4, 5);

Co zrobić? Odnaleźć linijkę w której mamy błąd i przypisać wartość poprawnie, albo usunąć to przypisanie, bo nie koniecznie powinno ono występować.

Apel!

Jeśli trafi Ci się błąd, staraj się go samodzielnie rozwiązać. Nic nie nauczy Cię programowania tak dobrze, jak samodzielne odnajdywanie źródeł problemów i ich likwidowanie. Jest to nauka, którą zapamiętasz najlepiej, przez wysiłek jaki w to włożyłeś. Ta lista pomoże Ci na początku drogi. Jak trochę potrenujesz widząc błąd, będziesz go naprawiał automatycznie bez niczyjej pomocy. Zanim poprosisz o pomoc, zawalcz!

Przy okazji, jeśli spotkałeś jakiś błąd którego tu nie opisałem, napisz w komentarzu, a uzupełnimy listę.