ROZDZIAŁ 4: mój skrypt nie działa! Co teraz?! - Debugowanie czas zacząć...

Bez obaw, to normalne. Algorytm to zwykle złożony konstrukt nie wybaczający nawet drobnych omyłek, okazji do których daje tyle, z ilu znaków się składa.  Skrypty SQF w Armie 3 nie stanowią tu wyjątku. Stąd znaczną część czasu pracy skryptera nad kodem pochłania testowanie i sukcesywne "odrobaczanie" naszego dzieła, czyli usuwanie wykrywanych kolejno błędów. Szkoła cierpliwości i pokory wobec własnej omylności. 

Debugowanie w praktyce to serie testów, analiza logów, logiczne wnioskowanie i w razie potrzeby stopniowe wykluczanie możliwych przyczyn aż do znalezienia tej faktycznej.



1. O testowaniu skryptów SQF

Każde uruchomienie scenariusza zawierającego skrypt stanowi jego test. Jednak by skutecznie ustalać i usuwać przyczyny błędów, skrypty lepiej testować w warunkach "laboratoryjnych", sterylnych, nie podczas przygodnej rozgrywki. Co to oznacza? Oznacza to testowanie wyłącznie z tym, co niezbędne dla działania skryptu. 

Grę uruchamiamy bez żadnych modów (chyba, że skrypt ich wymaga, ale lepiej, by na etapie testów nie wymagał). Skrypt testujemy w scenariuszu w formie zwykłego foldera (nie spakowany do pliku .pbo). Scenariusz uruchamiamy w edytorze, używając funkcji "preview". Scenariusz powinien zawierać tylko i wyłącznie minimum elementów niezbędnych do działania skryptu, które chcemy testować. Scenariusz nie powinien uruchamiać żadnych innych skryptów. Wszystko po to, by w razie błędu mieć jak największą jasność, że przyczyna leży w testowanym kawałku kodu, nie zaś, co możliwie, w cudzym modzie czy innym skrypcie. Otóż kluczem do sukcesu w debugowaniu naszego algorytmu jest dokładne ustalenie miejsca w kodzie, które powoduje błąd i powód, dla którego błąd tam powstaje. Im więcej potencjalnych źródeł błędu, tym trudniej go "osaczyć". Zazwyczaj też lepiej skupić się na szukaniu jednego błędu na raz. Czasem cały szereg błędów ma jedną, wspólną przyczynę. Tropiąc kilka omyłek na raz łatwo się pogubić.

Cennym źródłem informacji o błędach jest odzew (tzw. feedback) użytkowników. Jeżeli publikujemy nasz skrypt, warto korzystających prosić o informacje o błędach. Inny człowiek często od razu zrobi coś, co nam podczas testów nigdy do głowy by nie przyszło... Bo jest inny. Stąd też nasza niby odpicowana wersja 1.00 szybko obrasta w komentarze typu "nie działa mi!". Należy się ich spodziewać i korzystać z nich dopytując użytkowników o szczegóły problemów.

Uwaga! Do testów grę uruchamiamy bez parametru -noLogs (opcja launchera Army 3 "Bez dzienników"). Ten parametr zapobiega umieszczaniu logów w pliku RPT, a my ich bardzo potrzebujemy. Jest on domyślnie wyłączony i tak powinno pozostać. Przypominajmy o tym również innym osobom, które prosimy o testy. 



2.  Plik RPT

Najlepszy przyjaciel skryptera. Ich gromadkę (dla systemu Windows 10) znajdziemy tu: 



Zamiast "Rydygier" będzie nazwa aktualnego użytkownika. Uwaga - folder AppData bywa domyślnie folderem ukrytym i trzeba to zmienić w ustawieniach lub obejść tworząc skrót do niego np. na pulpicie. W środku odnajdziemy szereg plików RPT - z 10 ostatnich uruchomień gry. W istocie to zwykłe pliki tekstowe. 

Zapisywane są w nich logi gry - zakulisowe komunikaty o tym, co się dzieje w bebechach "silnika" gry. Większość zwykle stanowią niewarte uwagi pomruki i bormotanie, a że czasem zbiera się tego bardzo dużo, trzeba umieć spośród nich rozpoznać i wyłowić te logi, które dotyczą skryptów. Zatem i do czytania plików RPT przydaje się zaawansowany edytor tekstu (np. Notepad++) który umożliwia szybkie przeszukiwanie pod kątem słów kluczowych takich, jak "Error".



3. Nasz pierwszy błąd 

Zależnie od ustawień gry błąd może się w momencie wystąpienia pojawić na ekranie - dotyczy to zwłaszcza uruchamiania scenariuszy w edytorze (tzw. preview). Oto nasz pierwszy błąd, macha do nas, i my pomachajmy do niego:



Wszystko, co tu widzimy, oprócz "|#|" jest równocześnie zapisywane w pliku RPT. Okienko na ekranie jest wyświetlane krótko, ale jego zawartość odnajdziemy utrwaloną wśród logów. Dla tego błędu logi wyglądają tak:

11:09:22 Error in expression <Side = side player;
while {not (isNull _arty) and {(alive _arty)}} do
{
_gunn>
11:09:22   Error position: <_arty) and {(alive _arty)}} do
{
_gunn>
11:09:22   Error Undefined variable in expression: _arty
11:09:22 File C:\Users\Rydygier\Documents\Arma 3\missions\Skrypt_Bum.Malden\init.sqf..., line 7
11:09:22 Error in expression <"
_handle = [] spawn
{
sleep 1;
_arty = arty1;
_mySide = side player;
while {no>
11:09:22   Error position: <arty1;
_mySide = side player;
while {no>
11:09:22   Error Undefined variable in expression: arty1
11:09:22 File C:\Users\Rydygier\Documents\Arma 3\missions\Skrypt_Bum.Malden\init.sqf..., line 4


Jeżeli przeszukamy plik RPT pod kątem frazy "Error in expression" lub "Error position" (lub ich odpowiednika w języku, w którym uruchamiamy grę), każdy znaleziony element będzie odpowiadał jednemu logowi błędu.  Tu widzimy, jak się okazuje, dwa (w grze widać na ekranie tylko ostatni z serii). Uwaga: to nie oznacza takiej samej ilości pomyłek w skrypcie. Dla kodu wykonywanego wielokrotnie/cyklicznie każde wykonanie jednej błędnej linijki wygeneruje nowy log. Często również niektóre stanowią konsekwencję innych, tzn. kod się wykłada w wielu miejscach z powodu błędu w jednym miejscu. To nasz przypadek.

Błędy dotyczą skryptu, który stworzyliśmy w poprzednim rozdziale. Widzimy, że log RPT zgłasza użycie niezdefiniowanej zmiennej arty1 w linii 4 pliku init.sqf. Usłużnie podaje nam też fragment naszego kodu, na którym skrypt się z powodu tego błędu wykopyrtnął (nie zawsze będzie to miejsce, które jest faktycznym powodem błędu!). Tego fragmentu można użyć do wyszukania odnośnego miejsca w naszym skrypcie, jeśli brak numeru linii w logu. Wyżej widzimy błąd w linii 7: użyta niezdefiniowana zmienna _arty. To jest konsekwencja pierwotnego błędu, ponieważ tą zmienną definiowaliśmy tak:

_arty = arty1;

Skoro więc arty1 jest niezdefiniowana, ten sam los spotyka zmienną _arty, bo jest pod nią ukryta zmienna arty1

Zatem skupiamy się na zmiennej arty1. Dlaczego jest niezdefiniowana? Pamiętamy, że nie jest ona definiowana w skrypcie, pochodzi od nazwy moździerza ustawionego na mapie. Tam zatem będzie przyczyna błędu. I rzeczywiście!



Omyłkowo jest wpisane "arty11" zamiast "arty1". Dlatego zmienna arty1 nie jest zdefiniowana. Znaleźliśmy źródło błędu! Pozostaje usunąć nadmiarową jedynkę i... można testować dalej.



4. Edycja skryptu na potrzeby debugowania

Często, by z rozumieć, skąd bierze się problem, chcemy wiedzieć, co dzieje się np. z jakąś zmienną w konkretnym miejscu kodu - jaką ma tam wartość. Możemy tę wartość albo wyświetlić sobie na ekranie, np. za pomocą komendy:

hintSilent

albo zapisać w pliku RPT:

diag_log

W obu wypadkach potrzebujemy też komendy np.:

format

która zamienia dowolną wartość na tekst, a więc coś, co można wyświetlić lub zapisać. 

Na przykład jeśli chcemy dowiedzieć się, co zawierają zbiory celów i sprzymierzeńców w naszym skrypcie artyleryjskim tuż przed wyborem celu, możemy to zrobić tak:

diag_log format ["cele: %1 sprzymierzeni: %2",_targets,_allies];




Przykładowe uzyskane w teście logi RPT:


11:15:30 "cele: [] sprzymierzeni: [B Alpha 1-1:1 (Rydygier),arty1G]"
11:15:52 "cele: [O Alpha 1-1:1,O Alpha 1-2:1,O Alpha 1-3:1] sprzymierzeni: [B Alpha 1-1:1 (Rydygier),arty1G]"
11:16:00 "cele: [O Alpha 1-2:1,O Alpha 1-3:1] sprzymierzeni: [B Alpha 1-1:1 (Rydygier),arty1G]"

Zauważmy, odstępy czasowe między logami są mniejsze, niż zadane 30 sekund na cykl (oczekujemy jednego logu na cykl w tym wypadku). W tym teście wykorzystałem funkcję edytora pozwalającą kompresować czas: wszystko działo się w przyspieszonym tempie. Czasem pozwala to znacznie przyspieszyć testy, ale należy pamiętać, że zwielokrotnia to również obciążenie procesora, co miewa wpływ na niektóre skrypty, wrażliwe na tzw. timing.

Wartości zmiennych globalnych i wyrażeń można również śledzić na bieżąco przy użyciu obecnej w edytorze konsoli (sekcja "Watch"), co niekiedy bywa wystarczające i może nam zastąpić edycję kodu:



Sekcja Execute wykona wpisany w nią kod (po naciśnięciu "LOCAL EXEC"), co też bywa przydatne, na przykład do szybkiego testowania pomysłów lub sprawdzania stanu rzeczy w toku symulacji (rozgrywki). 

Czasem zaś chcemy po prostu sprawdzić, czy i kiedy skrypt "dociera" do wskazanej linijki. Wówczas wystarczy:



Jeśli po przetestowaniu scenariusza w logach nie znajdziemy "test1" będzie to oznaczać, iż z jakiegoś powodu ta linijka, a więc i dalsze w tej porcji kodu, czyli:



nie są nigdy osiągane. W ten sposób - znajdując za pomocą podobnych dodatków linijkę, na której skrypt utyka - możemy poznać odpowiedź na pytanie "dlaczego to się nie dzieje?" - w którym miejscu "przestaje się dziać", a więc gdzie jest problem.



5. Błędy trudne do namierzenia

Zdarzają się i takie błędy, które nie zostawią logu w RPT (lub log ten nie pomoże nam znaleźć przyczyny), i nie są łatwe do wykrycia metodami opisanymi w p. 4. Omyłki stanowiące ich przyczynę bywają też trudne do zauważenia, zwłaszcza w rozległym i złożonym skrypcie. Należą do nich niektóre przypadki niesparowanych lub źle ustawionych nawiasów, zwłaszcza klamrowych.

Edytory typu Notepad++ udostępniają opcję zliczania znaków w danym pliku. W ten sposób możemy policzyć nawiasy otwierające i zamykające danego typu. Jeżeli ich ilość będzie różna, najpewniej mamy problem z niesparowanym nawiasem. Jednak w ten sposób nie dowiemy się wiele więcej ponad to, nie wykryjemy też problemu z nawiasami źle rozmieszczonymi. 

Pozostaje żmudne przeglądanie skryptu linijka po linijce? Można i tak, ale jeśli kod jest długi, będzie to mordęga w trakcie której i tak możemy przeoczyć to, czego szukamy. Na szczęście mamy jeszcze jedną sztuczkę w zanadrzu. Możemy wykorzystać symbole /**/, które "unieczynniają" ujętą w nie porcję kodu (staje się "komentarzem" niewidzialnym dla silnika gry) lub dwa ukośniki - // - które wyłączają kod w jednej linijce - następujący po nich. W ten sposób możemy selektywnie dezaktywować partie kodu tak dobrane, by reszta bez nich była poprawnie wykonywana. Jeśli wyłączymy połowę kodu i szukany błąd zniknie - wiemy, że przyczyna jest w wyłączonej połowie. W innym razie wiemy, że jest w połowie nie wyłączonej. Następnie skupiamy się na połowie, która zawiera przyczynę i ponawiamy procedurę: wyłączamy jedną jej część itd. W ten sposób  stopniowo zawężamy obszar poszukiwań, drogą eliminacji osaczamy wadliwą linijkę do czasu, aż podejrzana część skryptu będzie na tyle mała, by bez trudu ją przeszukać. 

Jak dobrać porcje kodu do wyłączania, aby nie udaremnić wykonania pozostałej części? Z poszanowaniem logiki i składni. 

Jeśli wyłączymy część, która definiuje zmienne używane przez kod pozostawiony - spowodujemy nowe błędy - logiczne. Przykład takiego błędu:

ŹLE


Jeśli chodzi o poszanowanie składni, nie należy "ciąć" kodu przez środek wyrażenia - składni komendy:

ŹLE


ŹLE


ŹLE


ŹLE


ŹLE


Należy ciąć pomiędzy kompletnymi wyrażeniami:

DOBRZE


DOBRZE


DOBRZE


DOBRZE



6. Błędy logiki

Bywa i tak, że skrypt jest napisany bez zarzutu a i tak nie robi tego, co zaplanowaliśmy. Przyczyną mogą być błędy logiczne - kod prawidłowo oddaje obmyśloną przez nas logikę, ale ta logika nie jest poprawna. Być może po prostu zapomnieliśmy uwzględnić w skrypcie jakiś istotny element naszego zamysłu. A może źle skonstruowaliśmy warunek. Jeśli bezbłędnie od strony składni napisany kod nie robi tego, co planowaliśmy, pozostaje przemyśleć i zmodyfikować jego konstrukcję logiczną. Problem logiczny może być efektem nie tylko błędu na etapie koncepcyjnym, ale i prostej omyłki, która co prawda nie psuje składni SQF, ale psuje zamierzoną logikę. Prosty przykład:



Nasz skrypt z tak napisaną linijką będzie działał bez błędów, ale moździerz prawie nigdy nie wystrzeli. Wszystko przez omyłkę w warunku: jest 2000, powinno być 200. W powyższej formie kod wykona linijkę ostrzału tylko wówczas, jeśli nie będzie sprzymierzeńca w promieniu aż 2 kilometrów od obranego, znanego celu, a w Armie 3 taki dystans często wyklucza wiedzę jednostki o wrogu. Niechcący poszerzyliśmy "bufor bezpieczeństwa" dziesięciokrotnie.  



7. Koniec początku

Na tym kończymy serię poświęconą podstawom. Mam nadzieję, że udało się zrozumiale przekazać wystarczająco wiele, by początek przygody ze skryptowaniem w języku SQF uczynić jak najłatwiejszym i na tyle bezbolesnym, na ile to możliwe. Jeśli nie - wszelkie pytania, życzenia, skargi i zażalenia proszę śmiało zostawiać w komentarzach. 

Dziękuję za uwagę i powodzenia!

Rydygier


Brak komentarzy:

Prześlij komentarz