ROZDZIAŁ 3: piszemy pierwszy skrypt SQF

Przyswoiwszy sobie dwa poprzednie wpisy spróbujmy teraz przejść do praktyki i napisać skrypt dla gry Arma 3. Nie, nie będziemy pisać na ekranie "hello world!", skrypt powinien robić coś fajnego w grze - to kluczowe! Rozdział jest długi, nie ma potrzeby łykać go w całości za jednym posiedzeniem. Po trochu. Tyle, ile ogarniamy na raz. 



1. Nasz cel

Oto chcemy, aby wskazana jednostka artylerii automatycznie ostrzeliwała do skutku znanych jednostce gracza przeciwników. 

Za cel obieramy tutaj pewne minimum - dla prostoty przykładu. Naturalnie pozostaje szerokie pole do dalszej rozbudowy i wzbogacania skryptu: lepszy wybór celów, automatyczne wykrywanie dostępnej artylerii, celowanie z uwzględnieniem ruchu celu,  symulacja błędów celowniczego/obserwatora (niecelny ogień), kontrola gracza nad prowadzeniem ognia i inne. W taki własnie sposób z kilkudziesięciu linijek stopniowo i niepostrzeżenie mogą się z czasem zrobić tysiące... 



2. Co musi zawierać skrypt, który nam to zrobi?

Część wymogów jesteśmy w stanie przewidzieć z góry, inne wyjdą na jaw podczas testowania skryptu. Nasz algorytm:

a) musi sprawdzać, czy są jakieś cele na mapie;
b) musi sprawdzać, czy jednostka gracza o nich wie;
c) musi sprawdzać, czy wskazana jednostka artylerii ma czym strzelać;
d) musi sprawdzać, czy istnieją cele będące w zasięgu artylerii;
e) musi wybrać jeden cel spośród nich;
f) powinien sprawdzać, czy aby cel nie jest za blisko gracza lub jednostek sprzymierzonych (jeśli chcemy uniknąć ostrzeliwania swoich);
g) musi spowodować ostrzał wybranego celu;
h) powyższe powinien robić cały czas, nie tylko raz.



3. Szukamy pomocnych komend SQF

Może się nam z tym zejść przy braku wprawy, koniec końców otrzymamy dla poszczególnych podpunktów np. coś takiego:


a)

allUnits - daje nam zbiór wszystkich żywych jednostek na mapie;

foreach - rodzaj pętli - przydatna, gdy chcemy coś zrobić z każdym elementem jakiegoś zbioru (allUnits);

if - jeden z filarów konstrukcji logicznych skryptu. Dany kod będzie wykonany tylko, jeśli spełniony zostanie dany warunek (opcjonalnie inny kod może być wykonany, gdy warunek nie jest spełniony);

and, or, not - operatory logiczne często potrzebne przy formułowaniu warunków. Kolejno znaczą "i to, i to", "to lub to", "nie to";

getFriend - mówi, czy dane strony są sobie przyjazne, czy też wrogie;

player - zwraca jednostkę gracza - to względem niej będziemy stwierdzać, kto przyjaciel, a kto wróg;

side - podaje stronę, do której przynależy dana jednostka;

pushBack - dodaje wskazaną wartość do podanego zbioru - jako ostatnią ze zbioru. Typ wartości, którą nazywam zbiorem (array), ściślej i trafniej tłumaczyłoby się np. jako szereg, ponieważ jest to nie tylko worek z innymi wartościami, one są w nim uszeregowane jedna za drugą. Słowo zbiór wydaje mi się tu jednak na początek bardziej intuicyjne, wyraźniej wskazuje, że ta wartość zawiera zebrane razem inne wartości;

count - podaje ilość elementów w podanym zbiorze. Pozwoli nam się upewniać, że jest do kogo strzelać.

Dla każdej żywej jednostki sprawdzimy, czy należy do strony wrogiej stronie jednostki gracza. Jeśli tak - dodamy ją do zbioru celów. Jeśli nie, dodamy do zbioru przyjaciół - na użytek podpunktu f).


b)

knowsAbout - wroga jednostka trafi do zbioru potencjalnych celów tylko, jeśli jednostka gracza o niej "wie".


c)

someAmmo - chyba najprostszy ze sposobów by sprawdzić, czy jednostka ma czym strzelać.


d)

inRangeOfArtillery - sprawdza, czy podana pozycja jest w zasięgu podanej jednostki artylerii dla danego typu amunicji artyleryjskiej;

position - podaje pozycję wskazanego obiektu (koordynaty [x,y,z] w metrach od lewego dolnego rogu mapy);

currentMagazine - podaje aktualnie używany typ amunicji.


e) 

selectRandom - podaje losowo wybrany element danego zbioru. Metoda wyboru celu to ciekawe zagadnienie samo w sobie. Zdanie się na los to bodaj najprostsza droga, dlatego na niej poprzestaniemy, ale domyślam się, że wiele osób będzie wolało obmyślić coś bardziej zaawansowanego, coś, co będzie cele rozsądnie priorytetyzowało.


f) 

findIf - przeszukuje podany zbiór aż znajdzie element spełniający dany warunek;

distance - podaje odległość między dwoma pozycjami/obiektami.


g)

doArtilleryFire - wyspecjalizowana komenda nakazująca podanej jednostce artylerii ostrzał podanej pozycji na mapie danym typem amunicji i ilością pocisków.


h)

while - rodzaj pętli. Zawarty w niej kod będzie wykonywany cyklicznie, raz za razem, jak długo dany warunek jest spełniony, np. jak długo podana jednostka artylerii nie jest zniszczona i jest obsadzona;

sleep - wstrzymuje dalsze wykonanie kodu, którego jest częścią, o podaną ilość sekund;

alive - sprawdza, czy dana jednostka jest żywa;

gunner - zwraca jednostkę obsadzającą w pojeździe/statycznym systemie uzbrojenia pozycję strzelca (zwraca "brak obiektu" - "null" - jeśli strzelca brak);

isNull - sprawdza, czy podana zmienna zawiera "brak obiektu". Dzieje się tak, gdy obiekt zapisany w danej zmiennej zostanie np. skasowany komendą skryptową (znika/przestaje istnieć w symulacji);

spawn - wykonuje podany kod "osobno", to jest równolegle, symultanicznie. Wówczas dalsze linijki skryptu są wykonywane bez czekania, aż kod uruchomiony tą komendą zostanie wykonany do końca. Przyda się, żeby nasza pętla nie zablokowała wykonania ewentualnego kodu umieszczonego pod nią. Teraz nie planujemy nic dodawać, ale kto wie, może zechcemy później. Minus: skrypty wykonywane symultanicznie muszą dzielić między siebie w każdej klatce udostępnioną im moc procesora. Może to powodować opóźnienia  ich wykonania przy wielu intensywnych obliczeniowo skryptach. Tutaj nam to nie grozi (ani specjalnie nie przeszkadza).


Zauważmy, dokumentacja każdej z tych komend podaje jej składnię (lub składnie), to jest prawidłowy zapis komendy wraz z wymaganymi przez nią wartościami tworzący kompletne wyrażenie. Podaje też, jaki rodzaj wartości, jeśli jakikolwiek, zostanie zwrócony. 

Dodatkowo każde takie wyrażenie musimy kończyć znakiem średnika by oddzielić je od kolejnych. Wyjątkiem jest wyrażenie ostatnie, bo nie ma go już od czego dalej oddzielać - tu średnik opcjonalny. Dobrą praktyką sprzyjającą czytelności skryptu jest kończenie wiersza i zaczynanie nowego po każdym średniku. Podobnie rzecz się ma z przypisywaniem wartościom zmiennych, czyli definiowaniem zmiennych. Zaraz zobaczymy, jak to wszystko właściwie wygląda.

Kod umieszczony //po dwóch ukośnikach lub /*pomiędzy takimi symbolami*/ jest ignorowany przez silnik gry - nie jest wykonywany. Pozwala to umieszczać komentarze wewnątrz kodu lub wedle potrzeb (głównie podczas testów i debugowania) dezaktywować wybrane partie skryptu.



4. Mamy potrzebne składniki, składamy rzecz do kupy...

Oto gotowy kod:

_handle = [] spawn
{
sleep 1;
_arty = arty1;
_mySide = side player;

while {not (isNull _arty) and {(alive _arty)}} do
{
_gunner = gunner _arty;

if (not (isNull _gunner) and {(alive _gunner)}) then
{
_targets = [];
_allies = [];
_cMag = currentMagazine _arty;

{
_uSide = side _x;
if ((_uSide getFriend _mySide) < 0.6) then
{
if ((player knowsAbout _x) > 1) then
{
if ((position _x) inRangeOfArtillery [[_arty],_cMag]) then
{
_targets pushBack _x
}
}
}
else
{
_allies pushBack _x
};
}
foreach allUnits;
if ((count _targets) > 0) then
{
_tgt = selectRandom _targets;

if ((_allies findIf {((_tgt distance _x) < 2000)}) < 0) then
{
_arty doArtilleryFire [(position _tgt),_cMag,2]
};
};
};

sleep 30;
};
};


Linijki będą wykonywane w takiej kolejności, w jakiej czytamy tekst: od lewej do prawej i od góry do dołu. 

Jeśli teraz nie interesuje nas nic, poza działaniem tego skryptu, przechodzimy od razu do punktu 7, acz pominięta w ten sposób część wyjaśnia kwestie, które bez niej mogą pozostać niezrozumiałe.



5. Omówienie skryptu - uwagi ogólne

Dlaczego kod jest "rozstrzelony" na wiele linijek? Musi tak być? Nie. Teoretycznie wszystko można zapisać w jednej, bardzo długiej linijce i będzie działać tak samo. Komputerowi różnicy to nie robi. Robi nam. Luźny układ ze specyficznym rozmieszczeniem nawiasów ma na celu maksymalne ułatwienie czytania i edycji kodu - czyni go przejrzystszym i zrozumialszym. Detale zależą od osobistych preferencji w tym względzie. 

Wyrażenia rozpoczęte _podkreślnikiem to właśnie zmienne. Podkreślnik na początku oznacza, iż są to tzw. zmienne lokalne. Na razie nie ma powodu wiedzieć dokładnie, cóż to znaczy. Mówiąc ogólnie - istnieją one tylko w tej porcji kodu (patrz niżej - nawiasy klamrowe), w której zostały zdefiniowane (znakiem równości), reszta skryptu ich nie widzi. Istnieją też zmienne globalne, w tym skrypcie tylko jedna: arty1. Zdefiniowanych zmiennych globalnych można używać również poza kodem/skryptem, w którym zostały zdefiniowane. Istnieje tu trochę niuansów, ale to nie jest wiedza nam teraz potrzebna. Sam akt definiowania zmiennej jest oznaczony znakiem równości. Zmienna nie istnieje dla linijek kodu wykonywanych przed jej zdefiniowaniem. 

Nazwy zmiennych są prawie dowolne - zależą od naszej inwencji. Dla higieny pracy powinny nam się kojarzyć z tym, co oznaczają. Nie mogą zaczynać się cyfrą i nie mogą być identyczne z nazwami komend skryptowych. Nie tolerują też niektórych egzotycznych symboli (stąd konwencja anglojęzyczna to nie tylko nawyk, ale też kwestia pewnej elegancji - polskie znaki nam odpadają). Istnieją też pewne roztropne praktyki odnośnie nazw zmiennych globalnych, które pozwalają unikać sytuacji, gdy dwa różne skrypty uruchomione w scenariuszu definiują globalne zmienne o tych samych nazwach "podkradając" je sobie nawzajem, co kończy się zwykle litanią błędów (nie mogą równocześnie istnieć dwie zmienne o tej samej nazwie). 



6. O nawiasach

Zanim przystąpimy do omówienia kodu linijka po linijce, musimy poświęcić nieco uwagi nawiasom i ich użyciu. Po pierwsze i najważniejsze - nawiasy w kodzie występują parami (z nieistotnymi teraz wyjątkami). Niesparowany nawias to jeden z częstszych i trudniejszych do wyłowienia błędów psujących kod. 

Można o parach nawiasów myśleć jak o workach, w które coś wkładamy, w tym, nader często, inne worki. Niesparowany nawias to worek dziurawy, z którego wszystko się wysypuje. 

Po drugie, w skryptach SQF używamy przede wszystkim trzech rodzajów nawiasów. 


nawiasy okrągłe - (weź ze mnie nie to, co zawieram, lecz wynik tego, co zawieram). 

Typowe zastosowania: 

a) definiowanie warunków, czyli wyrażeń, których efektem jest wartość typu Boolean, tzn. taka, która może równać się prawdzie (true) albo fałszowi (false). Stosowane w konstrukcjach if-then(-else) (jeśli-to(-w innym razie)) i w składni innych komend, które wymagają stwierdzenia prawdy lub fałszu. Przykład:

((_tgt distance _x) < 200)

czytamy tak: "obiekt _tgt jest odległy od obiektu _x o mniej, niż 200 metrów". Otóż to zdanie może być dla danych dwu obiektów prawdą, wówczas zawartość czerwonej pary nawiasów jest "true", lub fałszem, a wówczas jest ona "false". Efektem tej linijki jest więc wartość Boolean. 

b) gdy zapis bez nawiasów nie zostanie poprawnie odczytany przez komendę lub gdy nie mamy pewności w tym względzie i nie chce nam się sprawdzać (nadmiarowe pary nawiasów tu nie szkodzą)... :) Przykład:

((_tgt distance _x) < 200)

Czy bez tych nawiasów kod będzie zrozumiały dla silnika gry?

(_tgt distance _x < 200)

nieraz nie jest to jednoznacznie opisane w dokumentacji komendy a bywa z tym różnie. W takiej sytuacji możemy albo przetestować wariant bez nawiasów i przekonać się, jak jest dla danej komendy, albo wstawić coś w nawiasy "w razie czego". 

Znak < oczekuje z obu stron liczby

_tgt distance _x

daje liczbę, ale jeśli nie mamy pewności, że kod nie zostanie odczytany tak:

_tgt distance | _x < 200

wstawiamy komendę wraz ze "świtą" w nawias:

(_tgt distance _x)

To daje gwarancję, że silnik najpierw wyliczy dystans między obiektami, a dopiero wynik tego wyliczenia - ów dystans - przyrówna z liczbą 200 by przekonać się, czy ten dystans jest mniejszy od 200 metrów. Bez nawiasu problemem może być takie odczytanie linijki, w którym kod próbuje porównać z liczbą 200 obiekt _x (_x < 200), co jest absurdem, zaś skutkiem tego w składni komendy distance uzna, że brakuje mu drugiego punktu wymaganego do pomiaru odległości (_tgt distance ?) - dwa błędy psujące kod. 

Jeśli zdecydujemy się przetestować powyższy kod bez wewnętrznej pary nawiasów okaże się co prawda, że nie jest ona akurat tu konieczna. Bez niej też kod działa. Oczywiście, taki test wybija nas z rytmu podczas pisania kodu i wymaga więcej czasu, niż proste wstawienie pary nawiasów. Osobiście też preferuję mieć wyrobiony nawyk umieszczania takich rzeczy w nawiasach - wolę pewną nadmiarowość od szukania potem w kodzie problemów w miejscach, gdzie nawiasy akurat były konieczne, a ich z rozpędu nie wstawiłem. Na przykład z kolei brak nawiasów zewnętrznych w tym kontekście:

if _obj1 distance _obj2 < 200 then

powoduje błąd. Zapisy działające poprawnie:

if (_obj1 distance _obj2 < 200) then
if ((_obj1 distance _obj2) < 200) then
if (((_obj1) distance (_obj2)) < 200) then //no, ale bez przesady...

Dodatkowe nawiasy pomagają mi również w kwestii czytelności kodu. Koniec końców - jak kto woli.

Jak widzimy, w skryptowaniu nie chodzi tylko o to, by nasz kod był poprawnie wykonany, ale też o to, by praca z nim była dla nas osobiście jak najwygodniejsza. To jak z porządkiem w warsztacie - jak długo mamy wszystkie potrzebne surowce i narzędzia, jesteśmy w stanie wykonać naszą pracę niezależnie od tego, czy wszystko jest intuicyjnie, porządnie poukładane w osobnych pudełkach, czy też zwalone na jedną, wielką kupę, bo pudełka uznaliśmy za nadmiarowe i zbędne. Pytanie tylko, w której sytuacji wykonamy naszą pracę szybciej, z mniejszym wysiłkiem i w lepszym nastroju. A że intuicyjność to rzecz subiektywna, dwa skrypty robiące identyczną rzecz napisane przez dwie osoby mogą się między sobą różnić i może nie być tu jednoznaczniej odpowiedzi, który jest napisany lepiej.  


nawiasy kwadratowe - [stanowię zbiór/szereg wartości, które zawieram].

Tu nie ma wiele do dodania. Pary tych nawiasów stanowią wartość typu array, czyli szereg (zbiór) wartości. Uwaga: jak najbardziej poprawna i częsta jest sytuacja, gdy będzie to zbiór pusty, bez żadnej wartości wewnątrz - zwykle tymczasowo. 


nawiasy klamrowe - {zawieram porcję kodu}.

Ten typ nawiasów oznacza wartość typu kod. Zawarty skrypt może, lecz nie musi, generować jakąś wartość, którą para zawierających go nawiasów zwraca, o ile ten kod zostanie wykonany, np. za sprawą komendy call, jeśli ta wartość jest umieszczona na samym końcu zawartego w nich kodu. 

Przykład 1:

_dst = call {(obiekt1 distance obiekt2)}

zwróci i zapisze pod zmienną _dst liczbę równą odległości wyrażonej w metrach między obiektem obiekt1 a obiektem obiekt2, bo to jest ostatnia rzecz w nich zawarta. Na tej zasadzie opiera się definiowanie i używanie tzw. funkcji. 

Przykład 2:

call {arty1 doArtilleryFire [(position player),(currentMagazine arty1),2]

Te nawiasy nie zwracają żadnej wartości - zgodnie z dokumentacją, komenda doArtilleryFire nie zwraca niczego, niemniej kod spowoduje w scenariuszu ostrzał artyleryjski przez jednostkę arty1 (uwaga - na pozycję gracza!), jeśli zostaną spełnione warunki zasięgu itp. 

Typowe zastosowania:

a) tak ujętą porcję kodu możemy zapisać pod nazwą zmiennej tak samo, jak inne typy wartości, jako tzw. funkcję. Nie wchodząc zanadto w detale, funkcje to właśnie tak zapisane kawałki kodu, które zwykle zwracają jakąś wartość. Zmienne funkcji są bardzo użyteczne szczególnie w dużych skryptach, gdzie identyczny kawałek kodu jest potrzebny wielokrotnie. Oszczędzamy wówczas na objętości kodu używając zamiast kopii tego kodu krótkiej zmiennej, pod którą jest on zapisany, zaś zmieniając kod w funkcji wprowadzamy tę samą zmianę za jednym zamachem wszędzie, gdzie jest ona użyta w skrypcie. W naszym przykładowym skrypcie nie korzystamy z funkcji. 

Przykład funkcji, która wylicza azymut w stopniach od jednego punktu do drugiego z ewentualnym rozrzutem losowym:



 I jej przykładowe użycie w skrypcie:



Mamy więc obliczony kąt (_angle) w jednej linijce kodu, zamiast w kilkunastu. _angle przybiera tę samą wartość, co obecna w funkcji zmienna _angleAzimuth0 gdyż stanowi ona ostatni element kodu funkcji. 

b) składnia wielu komend wymaga wartości tego typu. Np. konstrukcje w rodzaju "jeśli to, wykonaj taki oto {kod}, w innym razie wykonaj {inny kod}". To zastosowanie jest powszechne i widoczne w naszym przykładowym skrypcie. Ba. Mamy w nim takie porcje kodu, które zawierają porcje kodu zawierające porcje kodu etc. Zabawne, co nie?



7. Omówienie skryptu - szczegóły

Uff. Wreszcie czas na szczegółowe omówienie naszego skryptu ostrzału artyleryjskiego. Znaczenie linijek jedna po drugiej:

_handle = [] spawn

Ta linijka uruchamia cały nasz skrypt. Robi to komendą spawn która, jak pamiętamy, uruchamia podany kod równolegle do tego, co dalej/niżej. U nas co prawda, jeśli przyjrzymy się nawiasom klamrowym, pod kodem odpalanym tą komendą nic już nie ma. 

Zmienna _handle przechowuje wartość, którą zwraca komenda spawn. Można się tu bez niej obyć:

[] spawn

też zadziała. Niemniej wartość zapisana pod _handle może być użyta gdzieś później np. do ręcznego przerwania naszego skryptu (skoro opiera się o potencjalnie nie kończącą się pętlę...). 

Para nawiasów prostokątnych to zbiór wartości - parametrów, jakie chcemy przekazać do wewnątrz "spawnowanego" kodu w zgodzie ze składnią komendy spawn. Jak widać - niczego nie chcemy przekazać, zbiór jest pusty. Teoretycznie więc i ten nawias byłby do pominięcia. Samo:

spawn

 też zadziałałoby. 

Znak równości oznacza akt definiowania zmiennej - przypisania jej wartości. Od tego punktu zmienna _handle istnieje i posiada wartość równą temu, co zwraca komenda spawn. 

Po komendzie spawn jej składnia przewiduje wstawienie kodu, który komenda ma uruchomić. W tej linijce go nie ma. Zaczyna się w następnej (pamiętamy - kwestie klarowności kodu). Dla komputera to jednak nadal część tycząca się tej komendy, bo ignoruje on takie przeniesienia, za koniec "zdania" uważa średnik.

A zatem wbrew pozorom cała reszta naszego skryptu to w istocie element składni tej tutaj komendy spawn. 

Idźmy dalej:

{

W kolejnej linijce otwieramy "worek" z kodem, który domyka dopiero ostatni z nawiasów }; w naszym skrypcie. To właśnie ten kod doczepiony do komendy spawn. 

Poziome przesunięcie kodu za pomocą tabulacji ułatwia nam rozpoznanie tego faktu - każdy krok przesunięcia to kolejny "worek w worku". 

sleep 1;

Czekamy 1 sekundę, nim pójdziemy dalej. Nie zawsze taki postój jest konieczny, ale to raczej dobra praktyka - sam początek scenariusza to czas, gdy pewne sprawy się dopiero kolejno inicjalizują. Niektóre skrypty uruchamiane na samym starcie scenariusza bez takiej pauzy mogą z tego powodu nie zadziałać, jeśli wymagają czegoś, co się jeszcze w tym ułamku sekundy nie zainicjowało.  

_arty = arty1;

Kolejna linijka znajduje się już wewnątrz spawnowanego kodu. Definiuje zmienną lokalną _arty przypisując jej wartość typu obiekt - wedle naszej intencji będzie to jednostka artylerii ustawiona w scenariuszu via edytor i nazwana arty1. U nas to będzie moździerz BLUFOR. Powinien go obsadzać strzelec - jednostka AI, jako załoga. Już sama ta nazwa obiektu stanowić będzie w scenariuszu zdefiniowaną zmienną globalną, więc na dobrą sprawę moglibyśmy pominąć tę linijkę i wszędzie w skrypcie, gdzie piszemy _arty, pisać zamiast tego arty1. No, ale chcieliśmy być tacy eleganccy i używać wszędzie zmiennych lokalnych... Ma to i ten sens, że dzięki temu w razie czego łatwiej zmienić w skrypcie nazwę użytej jednostki artylerii - musimy to zrobić tylko w jednym wierszu, zamiast w wielu. 

Na wszelki wypadek ilustracja pokazująca, jak nazwać w edytorze obiekt (klikamy obiekt prawym przyciskiem, wybieramy Attributes... - atrybuty/właściwości), upewniamy się, że kliknęliśmy sam moźdzerz, a nie jego załogę, wpisujemy nazwę i potwierdzamy przyciskiem OK:



_mySide = side player;

Jako zmienna _mySide definiujemy na późniejszy użytek wartość typu side oznaczającą stronę, po której walczy postać gracza (powinna być ta sama lub sprzymierzona ze stroną ustawionej jednostki artylerii!). 

Zapisaliśmy więc dwie zmienne wewnątrz "spawnowanego" kodu, ale jeszcze przed umieszczoną w nim pętlą - są to wartości, co do których spodziewamy się, że już nie będą się w toku gry zmieniać, więc nie ma potrzeby definiować ich na nowo w każdym cyklu pętli. W ten sposób oszczędzamy odrobinę mocy obliczeniowej.

Kolejno następuje pusty wiersz, który ma znaczenie wyłącznie dla mojej subiektywnej czytelności kodu. 

while {not (isNull _arty) and {(alive _arty)}} do

Inaugurujemy naszą zasadniczą pętlę, w której będzie się dziać większość ciekawych rzeczy. Jest to pętla uruchamiana nierozdzielnym tandemem komend while-do. Zgodnie ze składnią tej pętli, pomiędzy nimi znajduje się porcja kodu, której zadaniem jest zwrócić wartość Boolean (prawda/fałsz). Wartość ta jest sprawdzana na samym początku każdego cyklu pętli i jeśli będzie fałszem (false), pętla natychmiast zakończy się. Zatem porcja kodu między while a do stanowi warunek trwania pętli sprawdzany raz na cykl (nie w czasie rzeczywistym!). Warunek składa się z dwóch składowych połączonych operatorem "and" co oznacza, że obie składowe muszą być "true" aby cały warunek był "true". Jeśli którakolwiek z tych składowych stanie się "false" - pętla zakończy się. 

Składową pierwszą: not (isNull _arty) rozumiemy tak: "obiekt _arty istnieje na mapie w scenariuszu". Pętla zakończy się, jeżeli coś nam "skasuje" nasz moździerz. Jeśli nasz skrypt to jedyny kod uruchamiany w scenariuszu - taka sytuacja nie powinna się zdarzyć. Ale jeśli z jakiegoś powodu używamy również innego skryptu, który może usunąć jednostkę z mapy, trzeba być przygotowanym na taką ewentualność. Inaczej w razie usunięcia moździerza skrypt może zgłupieć i wyrzucić błędy (z powodu komendy próbującej zrobić coś z moździerzem, którego nie ma), a to jest nieeleganckie. 

No dobrze, zapyta ktoś bystry, ale skoro ten warunek jest sprawdzany raz na cykl, może się zdarzyć, że usunięcie moździerza nastąpi w trakcie cyklu i nim się cykl skończy błędy się pojawią. Prawda. Szansa na to w naszym przypadku jest nikła, ale niezerowa. Ten warunek nie jest więc zabezpieczeniem wystarczającym na 100%. Aby być pewnym, należałoby ustawić dodatkowe linijki opuszczające pętlę, jeśli moździerz zniknie, tuż przed każdym wierszem, który odwołuje się do zmiennej _arty. Nie robię tego dla przejrzystości logiki kodu. 

Składową drugą: {(alive _arty)} rozumiemy tak: "obiekt _arty nie jest zniszczony". Pętla zakończy się, jeżeli w toku gry nasz moździerz zostanie zniszczony - jego obiekt nadal istnieje, ale prezentuje już tylko wrak. Obowiązują tu te same zastrzeżenia, które tyczą się pierwszej składowej. Zauważmy, iż ta składowa, w odróżnieniu od pierwszej, dostała dodatkową parę nawiasów klamrowych. Jest to sztuczka stosowana w wyrażeniach warunków logicznych. Kod działałby i w tej formie:

while {not (isNull _arty) and (alive _arty)} do

Różnica polega na tym, że tu druga składowa jest sprawdzana nawet, gdy już wiadomo, że całość zwróci "false", bo pierwsza składowa jest "false" (a obie musiałyby być "true" by dać ogólny wynik "true"). Dodanie tych nawiasów powoduje, że testowanie drugiej składowej (i podobnie kolejnych, jeśli występują) pomija się, jeśli wynik całości jest już przesądzony przez już sprawdzone składowe warunku. Oszczędza to moc obliczeniową a w niektórych sytuacjach (nie u nas) zapobiega błędom, jeśli dalsze składowe są poprawnie obliczane tylko, gdy uprzednie dadzą konkretny wynik. Po "do" następuje porcja kodu, który będzie cyklicznie (raz za razem) wykonywany - ponownie jest ona przeniesiona do kolejnych linii:

{

Ten wiersz otwiera wspomnianą porcję kodu zapętlonego (a cały czas siedzimy też w "worku" kodu spawnowanego)).

 _gunner = gunner _arty;

Pod zmienną _gunner zapisujemy jednostkę AI (wartość typu obiekt) - żołnierza, który obsadza stanowisko strzelca w naszej jednostce artylerii. Tę zmienną dla odróżnienia definiujemy wewnątrz pętli, a więc od nowa w każdym cyklu, ponieważ może się zdarzyć, iż początkowy strzelec zostanie zabity, skasowany lub opuści moździerz, a z kolei inna jednostka (np. za sprawą osobnego skryptu) może zająć jego miejsce. Musimy więc w każdym cyklu "odświeżać" wartość zmiennej _gunner. Po co nam ten "gunner"? Tylko i wyłącznie na użytek linijki kolejnej:

if (not (isNull _gunner) and {(alive _gunner)}) then

Artyleria nie może strzelać bez strzelca, dalsza część kodu nie ma sensu, jeżeli strzelec nie istnieje lub jest martwy. A zatem sprawdzamy to, zanim wykonamy kolejny krok. Używamy tandemu komend if-else, pomiędzy którymi znajdziemy warunek podobny, jak ten pomiędzy "while" a "do", lecz ujęty w nawiasy zwykłe. Rządzi się on tymi samymi prawami, jedyna różnica polega na tym, że ten odnosi się do obiektu strzelca, nie do moździerza. Dołączona po "then" porcja kodu (prawie cała reszta skryptu de facto) będzie wykonywana tylko, jeżeli moździerz jest obsadzony przez żywego strzelca.

Podsumujmy dotychczasowe kroki:
    • uruchomiliśmy kod (spawn);
    • w tym kodzie uruchomiliśmy pętlę (while-do);
    • w pętli ustawiliśmy warunek, od którego zależy wykonanie w każdym cyklu zasadniczej cześci kodu (if-then).

_targets = [];

Zmienna _targets to będzie nasz worek, na razie pusty, do którego będziemy wrzucać możliwe cele do ostrzelania. W każdym cyklu tworzymy go od nowa.

_allies = [];

Z kolei do worka _allies będziemy wrzucać obecnych na mapie sojuszników (w tym i postać gracza).

_cMag = currentMagazine _arty;

Jako _cMag zapisujemy sobie zmienną typu string (prosty tekst) - nazwę aktualnie używanego przez moździerz "magazynka", czyli amunicji. Nie jest to nazwa potoczna, jaka wyświetla się na ekranie, lecz tzw. nazwa klasy, jaką legitymuje się każdy rodzaj magazynka, ale też obiekt, broń i inne. Nazwy klas są wykorzystywane m. in. w skryptach. Przykładowa nazwa klasy magazynka:
"30rnd_9x21_mag"

{

Rozpoczynamy pętlę typu forEach otwierając doczepiony do niej kod. W jej przypadku składnia wygląda tak, że kod doczepiamy przed komendą, zatem odwrotnie, niż to było w przypadku pętli while-do. 

_uSide = side _x;

Widzimy tu zmienną _x. Nie definiowaliśmy jej. Zmienna _x to specjalna zmienna zdefiniowana automatycznie wewnątrz kodu doczepionego do pętli forEach. Pętla forEach polega na tym, że wykonuje doczepiony kod dla każdego z elementów zbioru (szeregu), który jest doczepiony po tej komendzie (jak zobaczymy niżej). Otóż wewnątrz doczepionego kodu zmienna _x oznacza aktualny element tego zbioru. Jeśli do pętli forEach doczepimy zbiór liczb [1,2,3], wówczas doczepiony kod zostanie wykonany dla każdej z nich, po czym pętla zakończy się. W pierwszym jej cyklu _x będzie równe 1, w drugim: 2, a w ostatnim: 3. 

Tu doczepiliśmy zbiór, jaki zwraca komenda allUnits, zawierający wszystkie żywe jednostki (ludziki) obecne aktualnie na mapie. A zatem nasza pętla będzie miała tyle cykli, ile jest na mapie jednostek, a w każdym kolejnym cyklu _x będzie oznaczać kolejną jednostkę. 

A zatem pod zmienną _uSide zapisujemy stronę, po której walczy jednostka _x.

if ((_uSide getFriend _mySide) < 0.6) then

Jeśli strona jednostki _x jest wroga stronie jednostki gracza, to...

if ((player knowsAbout _x) > 1) then

...jeśli jednostka gracza wie wystarczająco wiele o jednostce _x, to...

if ((position _x) inRangeOfArtillery [[_arty],_cMag]) then

...jeśli pozycja jednostki _x na mapie jest w zasięgu naszego moździerza, to...

_targets pushBack _x

...jednostkę _x wrzucamy do worka, czyli zbioru _targets przygotowanego dla potencjalnych celów ostrzału.

else

W innym razie, czyli jeśli jednostka _x nie jest wroga jednostce gracza...

_allies pushBack _x

...bez dalszych warunków trafia do worka przechowującego sprzymierzeńców gracza. Co ciekawe, trafi tu również jednostka samego gracza.

}

Domykamy kod doczepiony do komendy forEach...

foreach allUnits;

...i w zasadzie omówiona już reszta składni pętli forEach.

Mamy z powyższego dwa worki (zbiory): jeden z potencjalnym celami, drugi z wszystkim sprzymierzeńcami. Do sedna!

if ((count _targets) > 0) then

Liczymy zawartość worka z celami. Jeśli coś tam jest - można strzelać.

_tgt = selectRandom _targets;

Wybieramy cel metodą na potrzeby przykładu uproszczoną, czyli losujemy jeden z worka. _tgt oznacza wrogą jednostkę - nieszczęśnika, którego obierze za cel nasz moździerz. Gdyby zbiór _targets był pusty, tutaj skrypt by nam się zbuntował, wypluł logi błędu i poszedł sobie. Unikamy tych ekscesów dzięki uprzedniemu warunkowi, który nam liczy zawartość worka. 

if ((_allies findIf {((_tgt distance _x) < 200)}) < 0) then

Ten warunek jest spełniony, jeżeli żadna jednostka z worka sprzymierzeńców nie znajduje się bliżej celu, niż 200 metrów. Aby lepiej zrozumieć powyższy zapis, warto przestudiować linkowaną wcześniej dokumentację komendy findIf. Pod pewnymi względami przypomina działaniem pętlę forEach: posiada doczepiony kod ze specjalną zmienną _x, oraz posiada doczepiony zbiór, dla elementów którego wykonuje doczepiony kod. Ale doczepiony tu kod musi zwracać wartość Boolean a pętla kończy się, gdy kod dla któregoś z elementów zbioru (_x) odda wartość "true" - prawdę. Stanie się tak, jeśli którykolwiek ze sprzymierzeńców jest bliżej, niż 200 metrów od wybranego celu. Zakończywszy pracę pętla findIf zwraca nam liczbę: indeks (numer pozycji) elementu zbioru, dla którego otrzymaliśmy "true", czyli pokazuje nam palcem, który delikwent jako pierwszy z szeregu spełnia warunek zawarty w doczepionym kodzie.

Uwaga: ci dziwni ludzie, programiści, lubią zaczynać odliczanie nie od 1, a od 0 (tak, wiem...). Jest tak i w przypadku pozycji w szeregu (zbiorze): pierwszy element ma indeks 0, nie 1. Drugi: 1 i tak dalej. Dopiero ten fakt tłumaczy nam całe powyższe wyrażenie. Wszystko, co jest na lewo od znaku < oddaje indeks pierwszego w zbiorze elementu spełniającego dany warunek. Jeśli zaś żadnego nie znajdzie - zwraca liczbę -1. A zatem -1 na lewo od < oznacza, iż nie ma żadnego sprzymierzeńca, który byłby bliżej celu, niż 200 metrów, czyli: wolno strzelać! Stąd na prawo od < jest 0 - czekamy na wartość mniejszą od zera z lewej strony. Każda inna będzie oznaczała, że ktoś jest zbyt blisko celu, by strzelać. 

_arty doArtilleryFire [(position _tgt),_cMag,2]

Co tu dużo mówić... Wreszcie strzelamy! Dwa razy! Bo tak!

Kolejne trzy linijki domykają kolejne worki z kodem. Zostaje ostatnia rzecz do zrobienia w cyklu while-do, czyli fajrant:

sleep 30;

Każdy cykl kończy się 30-sekundową "drzemką". Po co? Bez pauzy pętla wykonywałaby cykle w tempie jeden na klatkę albo może nawet częściej. W efekcie moździerz nie nadążałby wycelować, przeładować i strzelić a sam kod byłby bardzo "ciężki obliczeniowo" dla procesora. Zżerałby cały czas (pętla jest warunkowo "wieczysta") sporo więcej zasobów, niż musi. Pauza rozrzedza nam więc atmosferę wytężonego liczenia.

A dlaczego akurat 30? Z doświadczenia i na wyczucie. Tyle wydaje się nie za dużo i nie za mało na nasze potrzeby: moździerz będzie mógł razić po jednym celu na cykl - co pół minuty. A że pocisk może lecieć dłużej, niż 30 sekund, możliwe jest wystrzelenie nowej salwy, nim poprzednia osiągnie cel.

A dlaczego sleep tutaj, a nie na początku pętli while-do? Tu jest nam lepiej, ponieważ to na początku cyklu pętla while-do sprawdza swój warunek trwania. Jeśli zaraz po tym sprawdzeniu damy sleep 30;, będzie aż 30 sekund na to, by dopiero co sprawdzony warunek przestał obowiązywać, jednak pętla nam tego nie wykryje aż nie dokończy całego cyklu, co oznacza błędy. Lepiej, by wszystkie obliczenia i czynności zostały wykonane od razu po sprawdzeniu warunku, póki jest "świeży", a dopiero po tym następuje pauza. 

Potem pozostaje domknąć worek z kodem pętli while-do i worek całego kodu, któryśmy uruchomili komendą spawn. 



8. A teraz sprawdźmy, czy algorytm działa

Otóż należy sobie uświadomić, iż świeżo napisany skrypt z reguły nie działa. Im większy, tym częściej tak będzie. Więcej o tym w rozdziale o debugowaniu, ten rozdział prezentuje kod już sprawdzony, uczesany i działający. 

Tu leży przygotowany scenariusz demonstracyjny do uruchomienia w edytorze:

Skrypt_Bum (plik .zip, Dropbox)

Na deser próbny rozruch:








Brak komentarzy:

Prześlij komentarz