Upload
others
View
1
Download
0
Embed Size (px)
Citation preview
SVEUČILIŠTE U ZAGREBU
FAKULTET ORGANIZACIJE I INFORMATIKE
V A R A Ž D I N
Marko Alerić
Agentni pristup u računalnim igrama
DIPLOMSKI RAD
Varaždin, 2016.
SVEUČILIŠTE U ZAGREBU
FAKULTET ORGANIZACIJE I INFORMATIKE
V A R A Ž D I N
Marko Alerić,
Redoviti student
Broj indeksa: 43578/14-R
Studij: Informacijsko i programsko inženjerstvo
Diplomski studij
Agentni pristup u računalnim igrama
DIPLOMSKI RAD
Mentor: Doc. dr. sc. Markus Schatten
Varaždin, rujan 2016.
I
Sadržaj
1. Uvod ..................................................................................................................... 1
2. Agenti u računalnim igrama ................................................................................. 2
2.1. Agenti u igrama ................................................................................................ 2
2.2. Umjetna inteligencija ........................................................................................ 3
2.3. Primjeri igara sa umjetnom inteligencijom kroz povijest ................................. 3
2.3.1. Početak umjetne inteligencije u igrama ...................................................... 4
2.3.2. Creatures ..................................................................................................... 4
2.3.3. The Sims ..................................................................................................... 4
2.3.4. Black & White ............................................................................................ 5
2.3.5. Halo ............................................................................................................ 6
2.3.6. F.E.A.R. ...................................................................................................... 7
3. Umjetna inteligencija u igrama ............................................................................ 8
3.1. Model umjetne inteligencije u igrama .............................................................. 9
3.2. Kretanje ............................................................................................................ 9
3.3. Automati ......................................................................................................... 11
3.4. Hijerarhijski automati ..................................................................................... 13
3.5. Stabla odlučivanja ........................................................................................... 16
3.5.1. Odluke ...................................................................................................... 17
3.6. Stabla ponašanja ............................................................................................. 19
3.6.1. Uvjeti ........................................................................................................ 19
3.6.2. Akcije ....................................................................................................... 19
3.6.3. Kompoziti ................................................................................................. 20
3.6.4. Dekoratori ................................................................................................. 21
3.6.5. Primjer stabla ponašanja ........................................................................... 21
3.7. Navigacija ....................................................................................................... 24
3.7.1. Algoritmi traženja putanje ........................................................................ 26
3.8. Tehnike učenja ................................................................................................ 28
3.8.1. Osnove učenja .......................................................................................... 28
3.8.2. Učenje pomoću stabla odlučivanja ........................................................... 29
3.8.3. Ostale tehnike učenja ................................................................................ 29
4. Plan implementacije ........................................................................................... 31
4.1. Unity ............................................................................................................... 31
4.2. Opis igre ......................................................................................................... 33
4.2.1. Neprijatelji pomoću automata .................................................................. 34
4.2.2. Neprijatelji sa formacijama ...................................................................... 36
II
4.2.3. Neprijatelji pomoću stabla ponašanja ....................................................... 37
5. Implementacija ................................................................................................... 40
5.1. Kreiranje projekta ........................................................................................... 40
5.2. Kretnje agenata ............................................................................................... 40
5.2.1. Agent Behaviour ....................................................................................... 40
5.2.2. Agent ........................................................................................................ 42
5.2.3. Kretanje igrača .......................................................................................... 44
5.2.4. Naganjanje i izbjegavanje ......................................................................... 45
5.2.5. Dostizanje na poziciju i napuštanje pozicije ............................................ 47
5.2.6. Suočavanje ................................................................................................ 49
5.2.7. Lutanje ...................................................................................................... 51
5.2.8. Suočavanje prema naprijed ....................................................................... 53
5.2.9. Izbjegavanje zidova .................................................................................. 53
5.2.10. Izbjegavanje agenata .............................................................................. 54
5.3. Donošenje odluka pomoću automata .............................................................. 56
5.3.1. Osnovna arhitektura .................................................................................. 56
5.3.2. Definiranje osnovnih uvjeta ..................................................................... 57
5.3.3. Implementacija stanja ............................................................................... 59
5.4. Donošenje odluka pomoću stabla ponašanja .................................................. 67
5.4.1. Implementacija osnovnih klasa ................................................................ 67
5.4.2. Implementacija kompozita ....................................................................... 69
5.4.3. Implementacija dekoratora ....................................................................... 71
5.4.4. Implementacija uvjeta .............................................................................. 74
5.4.5. Implementacija akcija ............................................................................... 77
5.5. Formacije agenata ........................................................................................... 83
5.5.1. Upravljanje formacijom ............................................................................ 83
5.5.2. Vrste formacija ......................................................................................... 86
5.6. Slaganje neprijatelja unutar Unity alata .......................................................... 91
5.6.1. Kreiranje osnovnih sučelja ....................................................................... 92
5.6.2. Kreiranje osnovne klase neprijatelja ........................................................ 93
5.6.3. Izgled osnovne strukture neprijatelja ........................................................ 95
5.6.4. Kreiranje neprijatelja „Healerbot“ ........................................................... 95
5.6.5. Kreiranje neprijatelja „Fighterbot“ .......................................................... 98
5.6.6. Kreiranje neprijatelja „Formation Fighterbot“ ........................................ 99
5.6.7. Kreiranje neprijatelja „Flybot Healer“ .................................................. 101
5.6.8. Kreiranje neprijatelja „Flybot Attacker“ ................................................ 103
5.7. Dodatne komponente .................................................................................... 105
III
6. Testiranje .......................................................................................................... 107
7. Zaključak .......................................................................................................... 113
8. Literatura .......................................................................................................... 114
9. Prilozi ............................................................................................................... 115
1
1. Uvod
U ovom radu upoznat ćemo se sa agentima u računalnim igrama, te o suvremenim
načinima implementacije. Tržište video igara je jedno od najvećih tržišta koje se u posljednjih
10-ak godina znatno proširilo. Prema riječima Gallaghera (Entertainment Software Assotiation,
2013), niti jedan sektor nije iskusio toliki rast poput računalne industrije i industrije video igara.
Isti dokument (Entertainment Software Assotiation, 2013) iz 2013 godine prikazuje podatak
koji navodi da 58% američke populacije igra video igre, te da prosječno američko kućanstvo
sadrži najmanje jednu platformu na kojoj je moguće pokrenuti igre. Za usporedbu iz istraživanja
za 2016 godine, u dokumentu (Entertainment Software Assotiation, 2016, p. 3) navode kako
63% američke populacije redovno igra video igre, čime je lako uočljiv porast od 5% u samo 3
godine, koji ujedno i opravdava porast i vrijednost samog tržišta video igara.
Razvoj video igara (engl. video game development) je proces razvoja i proizvodnje igre
kojeg izvodi programer (engl. game developer). U procesu razvoja igre obično sudjeluje veći
broj osoba, dok je u novije vrijeme čest slučaj razvoja u kojima sudjeluje samo jedna ili manji
broj osoba. Razvojni programi i alati u kojima se izrađuju mobilne igre često se ne razlikuju od
onih u kojima se izrađuju igre namijenjene ostalim platformama.
Prilikom implementacije igre u procesu razvoja igre, programeri se susreću sa raznim
dijelovima koji zasebno odvojeno nemaju funkcionalnu svrhu, već su dio veće cjeline koji
međusobno funkcioniraju i čine igru. Ti dijelovi uključuju razne multimedijske sadržaje poput
zvučnih efekata, glazbe, slike, 3d modele, te zatim sustav fizike, niz raznih sustava od kojih je
umjetna inteligencija (engl. AI) jedan od njih.
Područje umjetne inteligencije je područje gdje se najlakše i najbolje uočavaju agenti u
računalnim igrama. Upravo u tom području se postižu efekti neovisnih agenata koji u igri
predstavljaju neprijatelje ili suradnike koji su često „pametni“ i služe određenoj svrsi. U
nastavku rada ćemo vidjeti koliko su zapravo ti agenti u igrama pametni i zašto većina umjetne
inteligencije u igrama je samo prividna.
2
2. Agenti u računalnim igrama
Kao što je u uvodnom dijelu navedeno, upotreba agenata u računalnim igrama se
najčešće prepoznaje kod upotrebe umjetne inteligencije u igrama. Umjetna inteligencija u
igrama podliježe svim pravilima koji su uvjet kako bi određeni entitet u igri mogli zvati
agentom. Odnosno, korištenjem umjetne inteligencije u igrama kreiraju se likovi koji su
neovisni, samostalni (samostalno donose odluke), te reagiraju na određene podražaje.
U procesu razvoja igre, razvoj umjetne inteligencije i agenata spada u kategoriju
programiranja. Na slici 2.1 možemo vidjeti okvirni slijed procesa izrade igre koji započinje
analizom a završava testiranjem.
Slika 2-1: Proces nastajanja scenarija igre (Emalgam, 2010)
2.1. Agenti u igrama
Agenti su računalni sustavi koji su u stanju samostalno djelovati u skladu s ciljevima
korisnika ili vlasnika, pri čemu samostalno određuju načine ostvarenja cilja.
Za razliku od umjetne inteligencije u kojoj se žele oponašati situacije iz stvarnog svijeta,
agenti su tu da izvršavaju samo jednostavne operacije za koje su namijenjeni. Kod agenata nije
potrebno riješiti sve probleme umjetne inteligencije već samo potreban dio.
3
Autor (Millington & Funge, 2009, p. 11) navodi kako se agenti u igrama temelje na
kreiranju autonomnih likova koji imaju mogućnost primanja informacija iz podataka igre,
odlučivanja koju akciju donijeti temeljem te informacije, te na samom kraju i izvršiti odabranu
akciju. Taj pristup je obično poznat pod nazivom odozdo prema gore (engl. bottom-up).
Suprotnost tome su igre koje ne koriste pristup agenata, odnosno koriste pristup odozgo
prema dolje (engl. top- down), gdje jedan sustav sa vrha upravlja svim entitetima u igri. Primjer
takve igre je igra Grand Theft Auto 3, u kojem je promet i simulacija pješaka kreiran na takav
način (Millington & Funge, 2009, p. 11).
Ta dva pristupa se zapravo u većini igara kombiniraju. Pristup odozdo prema gore se ne
mora koristiti u mnogim igrama, ali u većini igara sa umjetnom inteligencijom se često koristi.
Pristup odozgo prema dolje se koristi u gotovo svakoj igri, bilo da se radi o osnovnom
upravljanju tijeka igre, ili o stvaranju neprijatelja, taj pristup se često koristi. U praktičnom
primjeru ovog rada, pristup odozgo prema dolje se koristi za stvaranje određenog broja
neprijatelja unutar vremenskog raspona. Nakon što je lik kreiran, on je potpuno neovisan o
komponenti koja ga je stvorila.
2.2. Umjetna inteligencija
Kao što navodi autor Steve Rabin (DeLoura, 2001), većina programera se drži
jednostavne tehnike umjetne inteligencije koja radi dobro. Naime, akademska zajednica koja
proučava „pravu“ umjetnu inteligenciju ima različit cilj od programera koji programiraju
umjetnu inteligenciju za igre. Cilj akademskog istraživanja je orijentiran na rezultat, način na
koji je problem riješen, dok cilj programera je izgled finalnog proizvoda.
Idealni primjer umjetne inteligencije je taj koji koristi agente, odnosno pristup odozdo
prema gore, koji mogu percipirati okolinu i samostalno donijeti odluke. U idućem poglavlju
opisane su tehnike na osnovu kojih se to najčešće postiže.
2.3. Primjeri igara sa umjetnom inteligencijom kroz povijest
Igre sa umjetnom inteligencijom počele su već od 70-tih godina prošlog stoljeća. U
nastavku su navedene neke od poznatijih igara koje su imale veći utjecaj razvijanju današnjih
algoritama umjetne inteligencije. Mnogi od tih algoritama korištenih kroz prošlost su dovoljno
dobri da se i dan danas aktivno koriste.
4
2.3.1. Početak umjetne inteligencije u igrama
Jedna od prvih igara koja je označavala početak razvoja umjetne inteligencije je igra
Pong. To je jedna od prvih igara koja je bila objavljena 1972. godine. U toj igri računalo i igrač
kontroliraju reket sprječavajući da im loptica dođe do ruba ekrana. Upravo takav način
jednostavnog upravljanja raketom gore-dolje na temelju pozicije loptice značilo je početak
razvoja umjetne inteligencije u igrama (Rabin, 2002, p. 3).
Pac-man igra iz 1976. godine koristi nešto složeniji oblik umjetne inteligencije. Za
umjetnu inteligenciju koriste se automati. Svaki od duhova u igri zapravo je individualan i
ponaša se različito od drugih. Autor igre navodi kako je to napravljeno da bi se izbjegla
monotonost igre (Madhav, 2014, p. 194).
2.3.2. Creatures
Creatures je igra iz 1996. godine u kojoj korisnik „leže“ određene životinje i uči ih kako
da se ponašaju. Te životinje mogu pričati, hraniti se i štititi. To je prva popularna aplikacija koja
koristi strojno učenje u interaktivnoj simulaciji. Koristi neuronske mreže kako bi životinje
mogle učiti (J. Champandard, 2007).
2.3.3. The Sims
The Sims igra je nastala 2000. godine. Radi se o igri koja prikazuje svakodnevne životne
aktivnosti obitelji unutar kuće i predgrađu. Igrač ima mogućnost kreiranja i izgradnje vlastite
kuće, uređivanje, postavljanje namještaja i opreme, te upravljanje likovima. Umjetna
inteligencija u ovoj igri funkcionira tako da se koristi pametna okolina. Okolina ukazuje što sve
nudi, dok lik prema svojim potrebama odlučuje i dolazi do traženih objekata. Tako npr. okolina
ukazuje gdje se nalazi hladnjak s hranom do kojeg lik dolazi kada postane gladan (Rabin, 2002,
p. 22).
5
Slika 2-2: The Sims igra (J. Champandard, 2007)
2.3.4. Black & White
Black & White igra je nastala 2001. godine te je jedan od najznačajnijih primjera
korištenja umjetne inteligencije u igrama. Radi se o igri u kojoj igrač vlada na otoku koji je
nastanjen sa raznim plemenima. Uključuje elemente simulacije umjetne inteligencije i
strategije. Tijekom igranja, fokusirana je interakcija sa velikim čudovištima koja mogu učiti iz
primjera, na osnovu svojih akcija dobivaju povratnu informaciju u obliku nagrade ili kazne.
Koriste arhitekturu inteligencije poznatu kao BDI model, model uvjerenja, želja i intencija.
Koriste i strojno učenje pomoću stabla odlučivanja i neuronskih mreža koje daju uspješan
rezultat.
6
Slika 2-3: Black & White igra (J. Champandard, 2007)
2.3.5. Halo
Halo igra je nastala 2001. godine, dok njen nastavak Halo 2 je nastao 2004. godine. Radi
se o pucačini iz prvog lica gdje se igrač bori protiv raznih vanzemaljaca. Neprijatelji mogu
koristiti zaklone jako pametno i pokrivati jedni druge. Kada timski vođa neprijatelja izgubi
život, ostatak tima se povlači. Posebno se ističe korištenje stabla ponašanja, pogotovo u drugom
nastavku, nakon čega su mnogi odlučili slijediti njihov primjer (J. Champandard, 2007).
Slika 2-4: Halo igra (J. Champandard, 2007)
7
2.3.6. F.E.A.R.
F.E.A.R. (First Encounter Assault Recon) je pucačina iz prvog lica u kojoj igrač
preživljava protiv nadnaravnih pojava i vojske kloniranih vojnika.
U igri se koristi planiranje kako bi generirali razna ponašanja. Korištena tehnologija i
danas služi kao referenca u mnogim studijima. Neprijatelji su u mogućnosti koristiti okolinu na
pametan način, pronalaze zaklone iza stolova, ruše police sa knjigama, otvaraju vrata, ulijeću
kroz prozor i sl. Vojnici su grupirani u posade te koriste grupne taktike, pokrivaju jedni druge
i sl. (J. Champandard, 2007).
Slika 2-5: F.E.A.R igra (J. Champandard, 2007)
8
3. Umjetna inteligencija u igrama
(Madhav, 2014) navodi, u tradicionalnoj računalnoj znanosti, mnoga istraživanja o
umjetnoj inteligenciji teže prema kompleksnoj formi umjetne inteligencije, uključujući
genetičke algoritme i neuronske mreže. Takvi kompleksni algoritmi imaju ograničenu upotrebu
u video igrama. Razlog tome je što kompleksni algoritmi iskorištavaju puno računalnih resursa
i zahtijevaju puno vremena.
U igrama, u svakoj sekundi, točnije u svakom frameu (jedna od više slika u sekundi) se
izvrši velik broj funkcija, poput ispisivanja grafike, razne kalkulacije i sl. Većina igara može
dozvoliti samo jedan manji dio vremena unutar jednog framea za umjetnu inteligenciju. Na taj
način korištenje kompleksnih algoritama se ne može uzeti u obzir. Drugi razlog je taj što su su
kompleksni algoritmi često nepotrebni jer problemi u igrama su jako dobro definirani i
određeni, te se željeno ponašanje može ostvariti pomoću znatno jednostavnijih metoda.
Kroz prošlost koristili su se razni oblici umjetne inteligencije. Najjednostavniji oblik
umjetne inteligencije je „hard-kodirani“ oblik. Idući korak koji je slijedio u umjetnoj
inteligenciji je nasumičnost. Bilo da se radi od „hard-kodiranom“ obliku inteligencije ili nekom
naprednijem, nasumičnost je ključna kod rješavanja problema determinističnosti.
Idući, i najčešći način koji se koristio kroz povijest je taj u kojem se AI ponašanja
definiraju kao kombinacije pravila automata (engl. state machines) sa nasumičnim varijacijama.
Ali različite igre zahtijevaju različite metode, tako igre poput šaha zahtijevaju stablo
odlučivanja za donošenje idućeg poteza. S tim da igre tog tipa imaju luksuz donošenja odluka
od par sekundi (a ne jedan dio framea), što se u igri predočava kao razmišljanje neprijatelja
(Hocine and Gouaich, 2011), i omogućava kreatorima da iskoriste nešto zahtjevniji algoritam.
Prema (Blanchet, 2012) najčešće AI metodologije koje se koriste za ostvarivanje
umjetne inteligencije su:
Stabla odlučivanja
Stabla ponašanja
Konačni automati
Hijerarhija naredbi
Pathfinding (A*)
Analiza terena
9
Flocking
Načini ostvarivanja umjetne inteligencije i metodologije svakog dana unaprjeđuju.
3.1. Model umjetne inteligencije u igrama
Kao što navodi (Ian Millington, 2009) umjetnu inteligenciju u igrama možemo
strukturirati na određene cjeline: kretnje (engl. movement), odlučivanje (engl. decision making)
i strategija. Prve dvije skupine se odnosne na osnovna svojstva samostalnog lika (engl.
character), dok strategija obuhvaća cjelinu veću od samog agenta i uzima i druge u obzir.
Slika 3-1: AI model za igre (Ian Milington, 2009)
Bitno je napomenuti da svaka igra nema potrebu za svim razinama umjetne inteligencije,
već neke igre zahtijevaju samo jednu razinu. Igre poput šaha zahtijevaju samo razinu strategije,
dok većina igara uopće ne zahtijevaju tu razinu.
3.2. Kretanje
Prema Slika 3-1, model AI u igrama, prva i početna kategorija koja se navodi je
kategorija kretnji likova, odnosno agenata u igri. Kretnje agenta u igri se mogu ostvariti na više
načina. Jedan od novih popularnijih načina implementacije te razine u igrama je pomoću
ponašanja upravljanjem (engl. Steering Behaviour) kojeg je osmislio Craig Reynolds. Craig
Reynolds je ujedno kreator tzv. flocking algoritma koji simulira ponašanje skupine određenih
10
životinja poput jata ptica, skupine riba, krdo stoke i sl. (Reynolds, 2011). Prednost korištenja
ovog algoritma je ta što ostvaruje neovisnost agenta i načina njegovog kretanja. Isti agent
funkcionira ispravno ukoliko mu postavimo ponašanje traženja, ponašanje lutanja ili neko
drugo ponašanje. Agent zadržava sva svoja svojstva poput brzine kretanja, brzine okretanja,
akceleracije, interaktivnost sa drugim elementima, jedina promjena je rezultat u kojem se agent
različito kreće.
Ponašanje upravljanjem je dinamički algoritam kretanja. Prvo ubrzava u pravom smjeru,
zatim kad dospije do tražene pozicije ubrzava na suprotnu stranu i tako kruži oko tražene
pozicije dok mu se ubrzanje ne smanji toliko da se pozicionira i ostane na traženoj poziciji (Ian
Millington, 2009).
Neki od poznatijih ponašanja upravljanjem su:
Naganjanje (engl. pursue)
Izbjegavanje (engl. evade)
Traženje (engl. seek)
Bježanje (engl. flee)
Dostizanje na poziciju (engl. arrive)
Napuštanje pozicije (engl. leave)
Lutanje (engl. wander)
Suočavanje (engl. face)
Izbjegavanje prepreka (engl. obstacle avoidance)
Izbjegavanje zidova (engl. wall avoidance)
Praćenje putanje (engl. path following)
Slika 3-2: Algoritam lutanja (wander) (Reynolds, 2011)
Neke od navedenih ponašanja prikazat ćemo i u samoj implementaciji u nastavku rada.
11
Pošto ponašanje agenata ovisi o jednoj varijabli, tzv. “steering” kojeg svako ponašanje
vraća i prosljeđuje agentu, koji zahvaljujući toj varijabli, izvršava određeni pokret. Takva vrsta
implementacije ujedno ograničava agenta na korištenje samo jednog ponašanja. Tu problem
nastaje ako postoji više ponašanja, na primjer želimo izbjegavati zidove i tražiti neprijatelja u
isto vrijeme. Kojem od tih ponašanja dati prednost, i kojeg prihvatiti, jer oba ponašanja će
agentu dati određenu vrijednost na osnovu koje će izvršiti pokret. Kako bi jednom agentu
dodijelili više ponašanja, potrebno je iskoristit neki od prigodnih algoritama za spajanje
ponašanja upravljanjem (engl. combining steering behaviour). U tu svrhu se najčešće koriste
algoritmi:
stapanjem po težini (engl. weight blending)
prioriteti
cijev ponašanja upravljanjem (engl. steering pipeline)
Ti algoritmi omogućuju postavljanje određene važnosti, tj. određuju prioritet za
ponašanja, te na osnovu toga agenti mogu koristiti više ponašanja istovremeno.
Naime, kada koristimo više ponašanja zajedno sa nekim od prethodno navedenim
algoritmima, postoji mogućnost da se naš agent neće ponašati onako kako očekujemo. Ukoliko
koristimo prioritete zajedno sa ponašanjem lutanja koji ima veći prioritet od ponašanja
izbjegavanja zidova, logično je zaključiti da će naš agent često pokušati prolaziti kroz zid. Stvar
se još više može zakomplicirati ukoliko koristimo još više ponašanja. Iz tog razloga je potrebno
dobro odabrati koji od tih algoritama koristimo i na koji način.
3.3. Automati
Konačni automati (engl. Final State Machines) su savršen model za reprezentaciju
umjetne inteligencije za ponašanje temeljeno na stanjima (engl. state-based behaviour).
Konačni automati se sastoje od stanja, prijelaza i uvjeta. Uvijek postoji jedno početno stanje iz
kojeg je moguće doći u neko drugo stanje ukoliko su uvjeti ispunjeni (Reynolds, 2011, p. 193).
Mnogi autori navode automate kao jedan od bitnijih modela za reprezentaciju umjetne
inteligencije, tako (McShaffry & Graham, 2013, p. 619) navode primjer prikazan na Slika 3-3
koji ilustrira automat za čuvara u igri. Čuvar u igri patrolira i ukoliko uoči da je igrač blizu onda
ga napada. Ukoliko igrač pobjegne u trenutku dok ga čuvar napada, čuvar će nastaviti
patrolirati. Ukoliko pak igrač napadne čuvara, čuvar se povlači i bježi.
12
Slika 3-3: Autiomat za čuvara (McShaffry & Graham, 2013, p. 618)
Slika 3-4 prikazuje osnovni primjer konačnih automata koji je kao što navodi (Madhav,
2014) kreiran za špijunsku igru. Agent, odnosno neprijatelj patrolira po mapi sve dok ne
pronađe igrača ili dok ne umre. Ukoliko ga igrač ubije, prelazi u stanje smrti.
Slika 3-4: Osnovni automat za stealth igru (Madhav, 2014, p. 193)
Ovaj osnovni primjer ima mnogo nedostataka. Što ako u stanju napadanja, igrač pobjegne od
neprijatelja, nije se moguće vratiti u prethodno stanje. Nešto kompleksniji i bolji automat je
moguće vidjeti na slici ispod.
13
Slika 3-5: Kompleksniji automat za stealth igru (Madhav, 2014, p. 194)
Na Slika 3-5 je vidljivo kako je stanje smrti (Death) prikazano nešto drugačije. Razlog tome je
što iz bilo kojeg stanja je moguće doći u stanje smrti. Način na koji je to moguće ostvariti u
objektno orijentiranom programskom jeziku je zapravo jednostavan, definiramo osnovno stanje
koja ima predefiniran prijelaz u stanje smrti, ostala stanja jednostavno nasljeđuju i proširuju
vlastitom implementacijom definirano stanje.
3.4. Hijerarhijski automati
Autor (Millington & Funge, 2009) navodi kako konačni automati mogu biti poboljšani
na više načina. Jedan od njih su hijerarhijski automati.
Jedan od poznatijih problema zbog kojeg se koristi ovaj mehanizam je poznat pod
nazivom alarmno ponašanje (engl. alarm behaviour). Naime, ukoliko u igri imamo agenta
robota sa svojim stanjima, koji patrolira po mapi i čisti otpad. Zamislimo situaciju u kojoj
robotu nestane baterije, trebao bi hitno ići na punjenje, i tek onda nastaviti gdje je stao, ili
ukoliko nastane požar pokraj mjesta gdje se puni, preživljavanje bi trebalo imati prioritet nad
punjenjem u tom trenutku.
Navedeni primjer bi se mogao napraviti unutar standardnih automata, s tim da bi
implementacija bila znatno kompliciranija i kreirano stanje se ne bi moglo iskoristit za druge
14
robote koji imaju slično ali drugačije ponašanje. Prethodni primjer možemo vidjeti na slici
ispod.
Slika 3-6: Alarmno ponašanje pomoću klasičnih automata (Millington & Funge, 2009, p. 319)
Implementacije stanja robota pomoću hijerarhijskih konačnih automata je znatno bolje
rješenje. Na Slika 3-7 moguće je vidjeti prikaz automata. Osnovni automat ostaje
nepromijenjen, ali je dio stanja pod nazivom „Clean up“ koji je povezan sa stanjem „Get power“
unutar kojeg robot puni baterije.
15
Slika 3-7: Hijerarhijski automati (Millington & Funge, 2009, p. 319)
Stanja u hijerarhijskim automatima funkcioniraju tako da stanja imaju prioritete.
Podržani su prijelazi između nivoa hijerarhije u kojima stanje sa većim prioritetom može
prekinuti izvođenje trenutnog automata, izvršiti potrebno, i vratiti se u prethodno stanje gdje je
stalo.
16
3.5. Stabla odlučivanja
Stabla odlučivanja (engl. Decision Tree) su jedan od jednostavnijih mehanizama za rad
sa problemima odlučivanja. Značajke stabla odlučivanja su brzina i jednostavnost
implementacije. Zahvaljujući tome, jedna su od korištenijih tehnika danas. Njihova upotreba se
intenzivno koristi za mehanizme vezane uz kontrolu likova, poput animacija (Millington &
Funge, 2009, p. 295).
Stablo odlučivanja je sastavljeno od niza povezanih odluka. Početna odluka se nalazi u
korijenu. Za svaku odluku, počevši od korijena, jedna od nadolazećih opcija je odabrana.
Slika 3-8: Algoritam lutanja (engl. wander) (Millington & Funge, 2009)
Svaka odluka unutar stabla odlučivanja je bazirana na osnovu znanja agenta. Pošto se
stabla odlučivanja koriste kao brzi i jednostavni mehanizam odlučivanja, agenti se često
referenciraju direktno na globalno stanje igre, umjesto posjedovanja reprezentacije vlastitog
znanja.
Svaki list u stablu odlučivanja je akcija koja je izvedena odmah u trenutku kada
algoritam dođe do te akcije.
17
3.5.1. Odluke
Odluke u stablu su jednostavne. Obično provjeravaju jednu vrijednost i ne posjeduju
provjeravanje poput I ili ILI uvjeta. Razlog zbog kojeg je stablo odlučivanja efikasno je zbog
toga što su odluke jako jednostavne i izvršavaju samo jedno testiranje unutar jedne odluke.
Kada je potrebno spajanja više odluka, potrebno je strukturirati odluke u samom stablu.
Na Slika 3-9 možemo vidjeti uvjet I, odnosno ako su A i B uvjeti istiniti onda izvrši
akciju 1, inače izvrši akciju 2.
Slika 3-9: Stablo odlučivanja sa I uvjetom ) (Millington & Funge, 2009)
Na Slika 3-10 možemo vidjeti uvjet koji predstavlja ILI uvjet, odnosno ako je A ILI B
uvjet istinit onda izvrši akciju 1, inače izvrši akciju 2.
Slika 3-10: Stablo odlučivanja sa ILI uvjetom ) (Millington & Funge, 2009)
Odluke u stablu odlučivanja ne moraju nužno posjedovati binarni uvjet. Uvjeti se mogu
implementirati tako da vraćaju više vrijednosti. Primjer takvog stabla je moguće vidjeti na Slika
3-11.
18
Slika 3-11: Plitko stablo odlučivanja sa 4 grane
Takav način implementacije donosi i neke kompleksnosti zbog čega se više preferiraju
binarne odluke, koje se mogu i dodatno optimizirati. Na Slika 3-12 vidimo jednako binarno
stablo, samo uz različit tip odluke.
Slika 3-12: Duboko binarno stablo odlučivanja
19
3.6. Stabla ponašanja
Stabla ponašanja (engl. Behaviour Tree) su postala popularan mehanizam za izradu
umjetne inteligencije likova. Halo 2, napravljena 2004. godine je prva veća igra koja za koju je
detaljno opisano korištenje stabla ponašanja. Zahvaljujući tome, mnoge igre su to slijedile.
Stablo ponašanja na neki način objedinjuje prethodno često korištene tehnike poput
hijerarhijskih automata, planiranja, izvršavanja akcija. Prednost ove tehnike je što objedinjuje
navedene tehnike na način koji je lagano razumjeti i iskoristiti za osobe koje ne programiraju.
Za razliku od automata, glavni dio stabla ponašanja je zadatak (engl. task). Zadatak
može biti jako jednostavan, poput izvršavanja animacije ili provjeravanje varijable. Zadaci se
sastavljaju unutar pod-stabla i predstavljaju kompleksniju akciju. Njihova struktura im daje
slobodu da ne zadaci međusobno nisu ovisi o tome na koji način su implementirani.
Zadaci se mogu podijeliti na više tipova zadataka. Postoje:
Akcije
Uvjeti
Kompoziti
Dekoratori
3.6.1. Uvjeti
Uvjeti testiraju određeno svojstvo u igri. Uvjet može testirati udaljenost traženog
objekta, testirati vidljivost traženog objekta, testirati vlastito stanje, i slično. Svaki od tih
zadataka implementiran je u vlastitoj klasi, kako bi bio iskoristiv. Svaki uvjet vraća uspješan
status ukoliko je uvjet ispunjen, u suprotnom vraća neuspjeh.
3.6.2. Akcije
Akcije mijenjaju stanje igre. Akcije mogu biti akcije za animaciju, za pomicanje agenta,
odnosno lika, za promjenu ponašanja, primjerice liku se može automatski puniti health ukoliko
odmara, zatim akcije za puštanje zvučnog zapisa, za interakciju sa dijalogom, te mnogi drugi.
Poput uvjeta, svaki od zadataka ima vlastitu implementaciju, i igre često imaju velik
broj akcija. Za razliku od uvjeta, većinu vremena, akcije će vratiti uspješan rezultat. U
suprotnom, treba razmisliti o korištenju uvjeta prije akcije.
20
Akcije i uvjeti su jako slični stanjima i prijelazima kod automata i temelje se na sličnoj
tehnici. Ta dva tipa zadataka se mogu kombinirati zajedno bez ovisnosti o ostatku stabla. Oba
tipa, uvjeti i akcije, nalaze se na listovima stabla.
Jedan čest oblik akcija je akcija čekanja. Navedena akcija jednostavno zadržava stanje
stabla u stanju izvršavanja, i nastavlja dalje tek kada je isteklo definirano vrijeme čekanja.
3.6.3. Kompoziti
Stablo se većinom grana pomoću kompozita. Kompoziti koriste i sadrže kolekciju
zadaka. Njihovo ponašanje i rezultat ovisi ponašanju njihove djece, odnosno zadataka koje
sadrže.
Popularni oblici kompozita su sekvenca i selekcija. Oba tipa kompozita pokreću
izvođenje svojih zadataka u redoslijedu. Sekvenca funkcionira tako da nastavlja izvedbu idućeg
zadatka ukoliko je prethodni zadatak vratio uspješan rezultat, inače prekida s radom i vraća
neuspješan rezultat. Selekcija funkcionira na suprotan način, prolazi kroz svu djecu, odnosno
zadatke i vraća uspješan rezultat ako je barem jedan od zadataka uspješan, inače vraća
neuspješan rezultat.
Na Slika 3-13 može se vidjeti jednostavan primjer sekvence. Stablo ponašanja će prvo
pozvati korijenski zadatak, odnosno sekvencu, koja prvo provjerava dali je neprijatelj vidljiv,
te ukoliko je uvjet uspješan, agent će se okrenuti i pobjeći.
Slika 3-13: Primjer sekvence u stablu ponašanja (Millington & Funge, 2009, p. 336)
Na Slika 3-14 stablo ponašanja će pozvati korijenski zadatak selekciju. Selekcija će prvo
pozvati napad na igrača, i ukoliko je uspješan, selekcija je gotova, u suprotnom ide na idući
zadatak.
21
Slika 3-14: Primjer selekcije u stablu ponašanja (Millington & Funge, 2009, p. 336)
Osim navedenih kompozita, postoji kompozit poznat pod nazivom „Parallel“ a odnosi
se na paralelno izvođenje zadataka. Paralelni kompozit je sličan sekvenci. Izvodi set djece sve
dok jedan od njih ne vrati neuspješan rezultat. Razlika od sekvence je ta što set djece izvodi
paralelno, odnosno istovremeno.
3.6.4. Dekoratori
Naziv dekorator je uzet iz uzoraka dizajna kod objektno orijentiranog programiranja. U
kontekstu stabla odlučivanja, dekoratori su zadaci koji imaju samo jedno dijete kao zadatak.
Možemo ih zamisliti kao kompozite sa samo jednim zadatkom.
Razlika dekoratora i kompozita je ta što kompoziti na neki način modificiraju način
izvođenja djeteta. Jedan oblik dekoratora je u kojem odlučuju dali dopustiti izvođenje djeteta
ili ne, takav oblik poznat je pod nazivom „filteri“. Ukoliko ne dopuste izvršavanje djeteta,
vraćaju neuspjeh. Drugi oblik dekoratora je taj gdje se limitira broj izvođenja djeteta, poznat
pod nazivom „limit“. Zatim dekorator može izvršavati dijete u while petlji sve dok dijete ne
vrati uspješan rezultat, taj dekorator je poznat pod nazivom „until fail“. Također jedan od
poznatijih dekoratora je dekorator koji vraća suprotnu vrijednost od vrijednosti koje vrati dijete.
Pa tako ukoliko se dijete uspješno izvrši, dekorator će vratiti neuspjeh, takav dekorator je poznat
pod nazivom „inverter“.
3.6.5. Primjer stabla ponašanja
Jednostavni primjer stabla ponašanja je vidljiv na slici ispod. Kreirano stablo ponašanje
kombinira sekvencu i selekciju. Prvo se izvodi sekvenca koja ima samo jedno dijete, a to je
selekcija. Selekcija će prvo pozvati prvo dijete, uvjet u kojem provjerava dali su vrata otvorena.
22
Ukoliko su vrata otvorena, stablo vraća uspjeh i završava s radom. Ukoliko vrata nisu otvorena,
selekcija ide na iduće dijete, sekvencu. Unutar sekvence lik dolazi do vrata i otvara vrata.
Slika 3-15: Primjer stablu ponašanja (Millington & Funge, 2009, p. 339)
Stabla ponašanja se mogu spremiti na takav način da njihova pod stabla se iskoriste više
puta. Primjer jednog od često korištenih pod stabala je prikazan na Slika 3-16. Pod stablo će se
izvršavati sve dok selekcija vraća uspjeh. Unutar sekvence prvo se provjerava vidljivost
neprijatelja, te ukoliko je neprijatelj vidljiv, suočava se sa njim. Ukoliko pak neprijatelj nije
vidljiv, neprijatelj ide na posljednju poznatu poziciju na kojoj je bio neprijatelj. Ovo pod stablo
ponašanja je učestalo za špijunske igre.
23
Slika 3-16: Primjer pod stabla u stablu ponašanja (Millington & Funge, 2009, p. 368)
24
3.7. Navigacija
Kako bi se likovi u igri uspješno mogli snalaziti na nekoj mapi, u nekom prostoru,
potrebno je koristiti neki od algoritama traženja putanje (engl. Pathfinding). Važno je znati da
se ti algoritmi trebaju koristiti samo ukoliko je zbilja potrebna navigacija jer inače uzalud troše
puno resursa. U nekim igrama je dovoljno koristiti algoritme kretanja uz kombinaciju sa
ponašanjem izbjegavanja zidova i prepreka.
Najveći problem koji nastaje korištenjem direktnih algoritama kretanja je taj što likovi
često ne mogu savladati kompleksnije prepreke, pa kao rezultat zapnu za određeni zid, ili pak
pokušavaju ići nemogućnom putanjom do cilja. Algoritmi traženja putanje u potpunosti
rješavaju taj problem.
Slika 3-17: Graf putanje za igru Rat Race (McShaffry & Graham, 2013, p. 637)
Svijet je pojednostavljen pomoću grafa sa čvorovima, ili pak sa mrežom sa rubovima
kroz koju prolazi algoritam traženja putanje između dvije točke tog grafa. Graf ili mreža
predstavljaju prohodni teren. Čvor na grafu predstavlja točku do koje agent mora doći. Kod
većine grafova agent može putovati slobodno između bilo koja dva čvora na grafu koji su
povezani lukom (McShaffry & Graham, 2013, p. 637).
25
Također, svjetovi se mogu reprezentirati na više načina. Neki od načina su pomoću
rešetke, odnosno koordinatne mreže (engl. Grid). Ovakav oblik strukture je jako jednostavno
implementirati i vizualizirati. Drugi način predstavljanja svijeta je pomoću Dirichletovih
domena, ili poznatim još po nazivom Voroniievi poligoni u dvodimenzionalnom prostoru, koji
dijeli svijet u regije. Svijet možemo predstaviti i pomoću točaka vidljivosti.
Slika 3-18: Reprezentacija svijeta pomoću rešetke (Palacios, 2016, p. 54)
Slika 3-19: Točke vidljivosti u nivou igre (Rabin, 2002, p. 164)
26
3.7.1. Algoritmi traženja putanje
Neki od poznatih algoritama traženja putanje su Dijkstrin algoritam traženja nakraće
putanje, DFS (Deep-First-Search) algoritam, A* algoritam traženja najbolje putanje. Algoritmi
traženja putanje se obično baziraju na određenom grafu koji predstavlja prostor u igri, i sastoji
se od čvorova i težina.
Slika 3-20: Težinski graf (Millington & Funge, 2009, p. 200)
Dijkstrin algoritam je jedan od algoritama koji se može koristiti za pronalazak najkraćeg
puta unutar igre. Originalno algoritam nije zamišljen za korištenje unutar igara, stoga ima i veći
broj nedostataka. Algoritam uzima samo jedan od niza mogućih putanja kao najbolji put, te
ostale putanje odbacuje. Zbog tog načina funkcioniranja, većinom se koristi kao dodatni
algoritam a ne glavni. Glava svrha mu je zapravo taktička analiza terena jer je znatno
jednostavniji od glavnih algoritama za traženje putanje.
Glavni algoritam traženja putanje je A* (izgovara sa na engleskom A star). A* je
algoritam koji je jednostavno implementirati, jako je učinkovit te postoji velik niz optimizacija
koje se mogu primijeniti. Većina ostalih algoritama traženja putanje je zapravo varijacija A*
algoritma. Za razliku od Dijkstrovog algoritma, A* je dizajniran za traženje putanje od točke
do točke, a ne za rješavanje najkraće putanje u problemu teorije grafova.
A* algoritam funkcionira tako da analizira svaki čvor grafa i postavlja tri vrijednosti.
Prva vrijednost se odnosi na ukupnu cijenu do tog čvora koristeći trenutnu putanju. Ta
vrijednost se naziva cilj (engl. goal) i označava se sa g. Druga vrijednost je preostala cijena od
27
trenutnog čvora do cilja (g), i naziva se heurističkom vrijednošću te se označava se sa h. Treća
vrijednost je preostala cijena od početka putanje do cilja, što je zapravo vrijednost g + h. Treća
vrijednost se označava sa f, i naziva se pogodnost (engl. fitness). Cilj korištenja tih triju
vrijednosti je da vode evidenciju uspjeha trenutnog napretka.
Kod početka proces A* algoritma, prvo se uzima čvor najbliži lokaciji lika.
28
3.8. Tehnike učenja
Tehnike učenja je područje često poznato i po nazivu strojno učenje (engl. Machine
Learning). Kroz povijest, strojno učenje je bilo problematično zbog resursa. Većina ranih
pokušaja strojnog učenja su rezultirali neigrivim igrama, ili pak sa lošom inteligencijom.
Strojno učenje je uspješno primijenjeno u igrama ali u ograničenim pod domenama poput
odabranih putanja kod trkaćih igara, također koriste se često kao tehnike za postizanje
određenog balansa u igri.
Najčešće se tehnike učenja koriste kako bi se agenti mogli prilagoditi svakom igraču,
učenje njihovih trikova i tehnika, tako da im predstavljaju izazov. Često agenti uče iz okoline
te to iskorištavaju na najbolji način (Millington & Funge, 2009, p. 579).
3.8.1. Osnove učenja
Postoji širok spektar tehnika korištenih za učenje, od jednostavnih do kompleksnih
poput neuronskih mreža. Tehnike učenja se mogu klasificirati u dvije skupine koje se odnose
na to kada je učenje nastalo i koji efekt izaziva u ponašanju.
Postoji online i offline učenje. Online učenje omogućuje da se agent prilagodi dinamički
na stil igranja omogućujući izazovniju igru. Nažalost dolazi sa puno problema, nemogućnosti
testiranja, pošto igra nikada nije ista. Češće se koristi offline učenje koje nastaje između
pojedinih razina unutar igre, ili pak u samom studiju dok igra još nije plasirana na tržište.
Učenje unutarnjeg ponašanjem (engl. Intra Behavior Learning) je najjednostavniji način
učenja koji mijenja mali dio ponašanja agenta, odnosno likova u igri. Ne mijenjaju cijelu
kvalitetu ponašanja već je na određeni način podešavaju. Jako su jednostavni za kontroliranje i
testiranje. Mogu se koristiti za prilagođavanja gađanja mete sa podešavanjem sile, za učenje
najbolje rute na određenom nivou, za učenje mjesta koja služe za zaklon, ili pak za učenje
najboljeg načina naganjanja i bježanja od igrača.
Učenje među ponašanjem (engl. Inter Behavior Learning) je učenje gdje likovi uče samo
ponašanje. Ukoliko lik ima potrebu naučiti najbolji način za ubijanje neprijatelja, na primjer,
može mu postaviti zamku iza prigodnog ugla. Likovi uče od nule kako djelovati unutar igre
kako bi predstavljali izazov čak i za najbolje igrače. Takva vrsta umjetne inteligencije je skoro
čista fantazija, navodi (Millington & Funge, 2009, p. 581). Kreiranje niza ponašanja, osnovnog
29
sustava kretanja, alata za donošenje odluka, te odluke na visokoj razini su znatno jednostavnije
i brže za implementirati, koje zatim mogu biti podešene pomoću učenja unutarnjeg ponašanja.
3.8.2. Učenje pomoću stabla odlučivanja
U poglavlju 3.5 navedena su stabla odlučivanja koji se sastoji od niza odluka koji
definiraju akcije temeljene na određenim podacima. Stablo odlučivanja efikasno može biti
naučeno, odnosno konstruirano dinamički od seta podataka i akcija, zatim se koristi na
normalan način kako bi likovi donosili tehnike tijekom igranja.
Postoji više algoritama kako stablo odlučivanja može učiti koje se koriste za
klasifikaciju, predviđanje i statističku analizu. Algoritmi koji se koriste u igrama su bazirani na
Quinlanovom ID3 algoritmu.
ID3 (Inductive Decision tree algorithm 3, Iterative Dichotomizer 3) je jednostavan za
implementirati i relativno efikasan kao algoritam učenja. Kod implementacije koristi dvije vrste
podataka, jedan za same čvorove i odluke, drugi za primjere učenja. Funkcionira tako da prvo
kreira jedan list, kojem dodjeljuje set primjera. Zatim dijeli taj list tako da primjere podijeli u
dvije grupe, ovisno o odabranim atributima tako da čine najefikasnije stablo. Proces je
rekurzivan i radi sve dok stablo nije kreirano. Kreira se sve dok kreirano stablo nije u
mogućnosti donijeti određene akcije, u tom trenutku dodatno grananje nije potrebno
(Millington & Funge, 2009, p. 613–614).
3.8.3. Ostale tehnike učenja
Među ostalim tehnikama učenja često se koristi tehnika predviđanja akcija pomoću N-
Gram prediktor algoritma. Predviđanje akcija je dobar način poboljšavanja izazova, od
nasumične selekcije do selekcija temeljenom na prethodnim djelima. Algoritam sadrži set
vjerojatnosti za donošenje odluke (koja je obično pokret), zajedno sa kombinacijom izbora iz
prethodnih pokreta (Palacios, 2016, p. 208).
Naivni Bayesovi klasifikatori (engl. Naïve Bayes classifiers) su tehnike predviđanja
koje se koriste za klasifikaciju, tj. dodjeljivanje oznaka raznim instancama problema. Koristi se
kod seta vjerojatnosti, gdje je Bayesov teorem neovisan o samim vrijednostima koje analiziran.
Jedan od ključnih prednosti je njegova skalabilnost (Millington & Funge, 2009, p. 608).
Umjetne neuronske mreže, ili kratko neuronske mreže su biološki inspiriran algoritam.
Imaju široku primjenu u aplikacijama. U igrama se koriste u području učenja, u nešto rjeđoj
30
upotrebi zato što su kompleksni i zahtjevni. Kao što navodi autor (Millington & Funge, 2009,
p. 676) često projekti koji koriste neuronske mreže, završe na kraju bez njih, iako njihova
primjena svakako postoji. Mogu se primijeniti na različite načine, posebno za tehnike
klasifikacije, što im je ujedno i primarna snaga.
31
4. Plan implementacije
Implementaciju umjetne inteligencije izvršiti će se Unity alatu. Prvo su prikazane
implementacije samih klasa i njihov opis u Unity alatu, te su ukomponirane u sami projekt na
temelju opisa igre. Projekt, odnosno igra u kojoj ćemo prikazati implementaciju umjetne
inteligencije je igra pod nazivom Botornot. Prva, početna verzija Botornot igre je započeta
2015. godine na Infogamer natjecanju u Zagrebu.
Programski dio igre će biti prikazan i opisan, s naglaskom na agente i umjetnu
inteligenciju, dok ostali dijelovi koji nisu tema ovog rada biti će izostavljeni. Razlog odabira
projekta Botornot je postojanost niza različitih resursa koji su slobodni za korištenje, među
kojima su 3D modeli, zvučni efekti, sučelje, te stil i pravila igranja.
4.1. Unity
Unity je višeplatformski game engine, odnosno sustav za kreiranje i razvoj video igara
razvijen od tvrtke Unity Techologies. Razvoj na alatu započeli su Davit Helgason, Nicolas
Francis te Joachim Ante 2001. godine, da bi prva verzija izašla 2005 godine. Unity je već od
2008 godine pružao podršku za mobilne uređaje, odnosno za Appleov iPhone uredaj, te 2010.
godine u verziji 3.0 za Android uređaje.
Unity poput većine game enginea, sastoji se od različitih pomagala koja ubrzavaju i
znatno pomažu u razvoju igre. Trenutno podržava sve popularne platforme, u kojima se ubrajaju
računala, mobilni uređaji, igrače konzole, te web preglednici.
Razlog biranja tog alata je njegova jednostavnost te dostupnost svima. Unity je
besplatan svim individualnima, te poduzećima koje ima godišnju dobit manju od 100 tisuća
američkih dolara, u suprotnom, trebaju platiti licencu. Na slici 4.1, moguće je vidjeti sučelje
Unity alata.
Unity se dijeli na dva osnovna dijela. Na projekt i njegove komponente, i na scenu i
komponente scene. Scena u Unitiju je obično jedan nivo koji se izrađuje, i njegove dijelove je
moguće vidjeti u hijerarhiji ili u pogledu scene. Inspektor služi da prikaže od kojih se
komponenti i skripti određeni objekt sastoji. U inspektoru ´će biti prikazane većina naših klasa,
kojima ´ćemo mijenjat javne parametre. Klase koje su vidljive u Inspektor prozoru moraju
naslijediti klasu MonoBehaviour, te time postaju komponenta GameObject-a (objekt u igri).
32
Uspoređujući to sa MVC modelom, možemo reći da MonoBehaviour pripada u View
komponenti tog modela.
Slika 4-1: Izgled Unity sučelja (Unity Technologies, 2016)
33
4.2. Opis igre
Igra Botornot je igra u kojoj je glavni lik virus koji pokušava zaraziti računalo, ali
antivirusi u računalu mu to onemogućavaju. Radnja same igre je u 3d prostoru, gdje je virus
prikazan sa 3d modelom koji posjeduje određene sposobnosti kojima ubija neprijatelje,
odnosno antiviruse. Antivirusi su također prikazani sa 3d modelom i štite određene komponente
na prostoru u kojem se nalaze.
Slika 4-2: Komponente unutar igre
Način na koji se neprijatelji stvaraju na mapi je predefiniran. Neprijatelji se mogu
stvoriti iz 4 različite pozicije, od kojeg su dvije namijenjene za neprijatelje koji liječe, a
preostale dvije za neprijatelje koji napadaju igrača. Neprijatelji se stvaraju u valovima. U
svakom valu unutar određenog intervala kreiraju se neprijatelji, tek kada su svi neprijatelji
poraženi, kreće idući val.
Kako bi igru učinili zanimljivijom i izazovnijom, ubacit ćemo nešto naprednije
neprijatelje koji su zapravo agenti. Neprijatelje ćemo nazvati HealerBot i FighterBot koji rade
točno ono kao što im samo ime ukazuje. Uz njih Flybot Healer koji također može liječiti
komponente, i FlyBot Attacker koji napada igrača.
Ukoliko razmotrimo tipove neprijatelja koji liječe komponente, može se uočiti
problematika sa kojom se susrećemo. Pošto postoje 4 komponente unutar scene, neprijatelj
mora odlučiti koju komponentu prvu liječiti.
34
Zatim, neprijatelji koji napadaju igrača mogu napadati unutar određene formacije, ili
pak mogu napadati igrača na način „hit and run“, gdje čim napadnu igrača pobjegnu.
Neprijatelj također može kružiti po mapi ukoliko mu je životna energija, odnosno „health“
nizak i rjeđe napadati igrača. Ukoliko je neprijatelj dovoljno pametan, može pokušati odvući
pozornost na sebe dok drugi neprijatelji dovrše liječenje komponente.
Postoji neograničen niz mogućnosti koje je moguće isplanirati za naše neprijatelje i igru. U
tablici ispod su navedeni osnovni tipovi neprijatelja i njihove funkcije.
Neprijatelj Glavna svrha Algoritam
HealerBot Lječenje komponente Automati
FighterBot Napadanje igrača Automati
Formation FighterBot Napadanje igrača Automati uz formacije
FlyBot Healer Lječenje komponente Stablo ponašanja
FlyBotAttacker Napadanje igrača Stablo ponašanja
Tablica 1. Vrsta neprijatelja uz svrhu i algoritam implementacije
4.2.1. Neprijatelji pomoću automata
U tablici (vidi Tablica 1) možemo vidjeti da imamo dva tipa neprijatelja koji koriste
automate, točnije neprijatelji Healerbot i Fighterbot.
Healerbot neprijatelj kao što je već navedeno, liječi komponente. Tu je očito stanje
lječenja, odnosno „State Healing“. Zatim, osim toga, prije nego što liječi komponentu, potrebno
ju je zapravo odabrati i doći do nje. Način biranja komponente je implementiran na način takav
da se računa postotak za svaku komponentu, zatim se generira nasumičan broj unutar ukupnog
raspona i odabere se komponenta koja je unutar nasumično odabranog raspona (rasponi se za
svaku komponentu kumulativno dodjeljuju). Postotak biranja komponente računa se na osnovu
udaljenosti agenta od komponente, i na osnovu izliječenosti komponente. Iduće stanje unutar
kojeg koristimo navedeni algoritam i putujemo do komponente je stanje dostizanja do
komponente, odnosno „State Arrive To Component“ . Te stanje smrti, odnosno „State Death“.
35
Slika 4-3: Automat za HealBot-a
Na prethodnoj slici vidimo da je uvjet za liječenje komponente ispunjen tek kada je
neprijatelj na poziciji za liječenje. Ponovno traženje i dolazak na drugu komponentu izvršava
se nakon što istekne vrijeme potrebno za liječenje.
Drugi neprijatelj, Fighterbot napada igrača, gdje je uočljivo stanje napada, odnosno
„State Attack“. Zbog jednostavnosti te implementacije, nije potrebno implementirati dodatno
stanje u kojem igrač dolazi do igrača, no moguće je. Osim tog stanja neprijatelj također ima
stanje smrti, odnosno „State Death“. Ovaj neprijatelj ima jako jednostavnu primjenu, razlog je
taj što unutar stanja napada koristi drugu komponentu koja javlja samom stanju ukoliko je
neprijatelj unutar kolizije sa igračem, te u tom slučaju igraču se oduzima snaga. Stanje napada
cijelo vrijeme ima uključeno ponašanje naganjanja, tako da cijelo vrijeme prati i naganja igrača.
Slika 4-4: Automat za FighterBot-a
36
4.2.2. Neprijatelji sa formacijama
Formacije su jedno jako zanimljivo područje kod video igara i imaju široku primjenu.
U našoj igri implementirat ćemo jednostavnu kružnu formaciju. Napadače, odnosno
Fighterbote ćemo ubaciti unutar formacije tako da svi zajedno formiraju krug. Neprijatelji
unutar formacije stižu do igrača, okružuju ga i napadaju, gdje im se ujedno smanjuje radijus
kruga kojeg čine. Na taj način igraču zadaju više udaraca od jednom i znatno mu smanjuju
snagu.
Navedena implementacija zahtjeva višu razinu stanja, odnosno automata. Osim
neprijatelja, sama formacija ima svoje stanje. Formacija prvo treba provjeriti dali je minimalni
broj agenata prisutan kako bi mogla funkcionirati, što prikazujemo stanjem formiranja
formacije, odnosno „State forming formation“. Zatim stanje napadanja, gdje dolazimo do
igrača, i napadamo igrača ukoliko je igrač u središtu kruga, stanje pod nazivom „State
Circular Formation Attack“. Te stanje obrane, gdje se agenti udaljuju od igrača (povećavaju
radijus) na određenoj udaljenosti prije nego što napadnu opet. To stanje se naziva „State
Circular Formation Defense“. Na slici ispod moguće je vidjeti stanja formacije.
Slika 4-5: Automat za kružnu formaciju
Zatim neprijatelji koji su unutar formacije imaju slična stanja kao i obični Fighterbot
neprijatelj, prikazani na slici ispod.
37
Slika 4-6: Automat za FighterBot-a unutar formacije
4.2.3. Neprijatelji pomoću stabla ponašanja
U tablici (vidi Tablica 1) navedena su dva tipa neprijatelja koji koriste stablo ponašanja.
Flybot Healer koji je zadužen za liječenje komponenti, te Flybot Attacker koji je zadužen za
napadanje igrača. Razlog uzimanja stabla ponašanje je taj što pomoću njega jako jednostavno
slažemo neprijatelja po želji. Jednom kada imamo skup zadataka i uvjeta, jednostavno je složiti
stablo koje predstavlja ponašanje agenta.
Flybot Healer je neprijatelj kojem je primarna svrha liječiti komponente. Za razliku od
običnih neprijatelja koji liječe komponente, Flybot Healer provjerava svoju udaljenost od
igrača, te ukoliko je igrač blizu, bježi na određenu udaljenost te tek onda traži komponente koje
je potrebno liječiti. Stablo ponašanja za ovog neprijatelja je vidljivo na slici ispod.
http://editor.behavior3.com/#/editor
38
Slika 4-7: Stablo ponašanja za Flybot Healer-a
Flybot Attacker je neprijatelj koji ima glavnu svrhu napasti igrača. Za razliku od
prethodnih napadača, Flybot Attacker igrača napada na način takav da čim uspješno zada udarac
igraču, odmah se odmakne od igrača. Stablo ponašanja nasumično odlučuje i bira između
napadanja, bježanja i lutanja. Bježanje će se uspješno izvesti ukoliko igrač ima jako veliku
životnu energiju ili ukoliko sam neprijatelj ima nisku životnu energiju. Na slici ispod je moguće
vidjeti stablo ponašanja za navedenog neprijatelja. Moguće je uočiti da su nasumične selekcije
i nasumične sekvence označene sa prefiksom „?“. Za slučaj kada nasumična sekvenca odabere
smjer izbjegavanja (treće pod stablo), prvo će se izvesti sekvenca koja će biti uspješna samo
ukoliko je jedan od uvjeta istinit. Prvi uvjet provjerava vlastitu energiju dok drugi provjerava
energiju neprijatelja. Ukoliko oba uvjeta nisu zadovoljena, odnosno ukoliko igrač ima malu
životnu energiju, a neprijatelj visoku, nema razloga bježanju, te se pod stablo obustavlja. Tu
39
također vidimo da je zadatak „Stop Evade“ drugo dijete selektora koje će se u svakom slučaju
izvest, na taj način osiguramo da neprijatelj završi treći ogranak stabla bez da bježi od
neprijatelja.
Slika 4-8: Stablo ponašanja za Flybot Attacker-a
40
5. Implementacija
U ovom poglavlju prikazana je implementacija prema prethodno navedenom planu.
Korišteni programski jezik je C#, i UnityEngine biblioteka u kojoj se nalaze osnovne klase i
metode za rad u Unity sustavu. Većina klasa nasljeđuje klasu MonoBehaviour iz UnityEngine
biblioteke koja označava da je naša klasa zapravo komponenta u Unitiju.
5.1. Kreiranje projekta
Na samom početku, u projekt smo ubacili sve potrebne resurse, modele pomoću kojih
ćemo graditi agente.
5.2. Kretnje agenata
Kako bi agenta pokrenuli, prvo ćemo implementirati njegove kretnje. Za našeg agenta,
potrebne su dvije osnovne klase:
AgentBehaviur
Agent
Te pomoćna klasa Steering.
Steering klasa nam služi za prijenos informacija o kretnji koju agent treba poduzeti. Sastoji se
od linearnog smjera kretanja, te od rotacije.
1. using UnityEngine;
2. using System.Collections;
3. public class Steering
4. {
5. public float angular;
6. public Vector3 linear;
7. public Steering() {
8. angular = 0.0 f;
9. linear = new Vector3();
10. }
11. }
5.2.1. Agent Behaviour
Agent Behaviour klasa je klasa koju je osnova za sva ponašanja koja su implementirana
u nastavku.
41
Većina naših ponašanja koristi određeni objekt o kojem ovise, pa tako ponašanje 'traži'
zahtjeva objekt kojeg moramo tražiti, dok ponašanje 'bježanje' zahtjeva objekt od kojeg moramo
pobjeći. Zbog toga imamo javni objekt pod nazivom target. Objekt tipa Agent nam služi kao
referenca na agenta koji koristi to ponašanje. Na osovi tog objekta, ponašanje može pročitati
parametre agenta, ograničiti maksimalnu brzinu te najvažnije, poslati agentu objekt tipa
Steering koji sadrži smjer kretanja i orijentaciju koju agent treba pratiti.
1. using UnityEngine;
2. using System.Collections;
3. public class AgentBehaviour: MonoBehaviour
4. {
5. public GameObject target;
6. protected Agent agent;
7. public virtual void Awake() {
8. agent = gameObject.GetComponent < Agent > ();
9. }
10. public virtual void Update() {
11. agent.SetSteering(GetSteering());
12. }
13. public virtual Steering GetSteering() {
14. return new Steering();
15. }
16. }
U implementaciji koristimo tri virtualne metode: Awake, Update i GetSteering.
Awake i Update metode su metode koje poziva Unity, odnosno MonoBehavior klasa. Awake
metoda se izvršava samo jednom na samom početku te u njoj vršimo dohvaćanje komponente
Agent, Update metoda se izvršava unutar svakog framea, odnosno preko 30 puta u sekundi
gdje agentu prosljeđujemo Steering objekt. GetSteering je metoda koja koja za ostala
ponašanja ima najveći značaj jer unutar te metode će biti implementirano ponašanje.
U drugom poglavlju u kojem su opisani algoritmi kretanja agenta, navedeni su
problemi i ograničenje korištenja samo jednog ponašanja. U tu svrhu, ažurirat ćemo Update
metodu sa metodom ispod.
1. public float weight = 1.0 f;
2. // ostatak koda …
3.
4. public virtual void Update() {
5. if (agent.useBlendWeight) {
6. agent.SetSteering(GetSteering(), weight);
42
7. }
8. else {
9. agent.SetSteering(GetSteering());
10. }
11. }
Može se uočiti da je u ovoj implementaciji iskorišten uzorak dizajna pod nazivom
Template Method.
5.2.2. Agent
Agent klasa je glavna komponenta koja iskorištava ponašanja i pokreće agenta. U klasi
su implementirana svojstva koja uključuju agentove sposobnosti poput brzine i akceleracije,
koja se definiraju u Unitijevom sučelju, te varijable koje pohranjuju agentovu rotaciju,
orijentaciju, vektor sile kretanja, te objekt Steering.
1. using UnityEngine;
2. using System.Collections;
3. public class Agent: MonoBehaviour
4. {
5. public float maxSpeed;
6. public float maxAccel;
7. public float orientation;
8. public float maxRotation;
9. public float maxAngularAccel;
10. public float rotation;
11. public Vector3 velocity;
12. protected Steering steering;
13. void Start() {
14. velocity = Vector3.zero;
15. steering = new Steering();
16. }
17. public void SetSteering(Steering steering) {
18. this.steering = steering;
19. }
20. }
Glavni dio implementacije prikazan je u nastavku. Unutar Update metode vrši se
pomak agenta prema izračunatoj sili kretanja i rotira se agent prema izračunatoj orijentaciji.
1. public virtual void Update() {
2. Vector3 displacement = velocity * Time.deltaTime;
43
3. orientation += rotation * Time.deltaTime;
// limitiranje vrijednosti orjentacijeu na skup (0 – 360)
4. if (orientation < 0.0 f) orientation += 360.0 f;
5. else if (orientation > 360.0 f) orientation -= 360.0 f;
6. transform.Translate(displacement, Space.World);
7. transform.rotation = new Quaternion();
8. transform.Rotate(Vector3.up, orientation);
9. }
Kako bi iskoristili silu kretanja i orijentaciju, potrebno ih je izračunati na osnovu
proslijeđenog objekta Steering. Izračun se vrši u metodi LateUpdate koja se izvršava također
jednako kao i Update metoda, samo u drugom redoslijedu, odnosno izvršava se odmah nakon
Update metode.
1. public virtual void LateUpdate() {
2. velocity += steering.linear * Time.deltaTime;
3. rotation += steering.angular * Time.deltaTime;
4. if (velocity.magnitude > maxSpeed) {
5. velocity.Normalize();
6. velocity = velocity * maxSpeed;
7. }
8. if (steering.angular == 0.0 f) {
9. rotation = 0.0 f;
10. }
11. if (steering.linear.sqrMagnitude == 0.0 f) {
12. velocity = Vector3.zero;
13. }
14. steering = new Steering();
15. }
Glavni dio implementacije agenta je gotov. No kao što je navedeno u implementaciji
AgentBehavior, problem je ukoliko želimo koristiti više ponašanja istovremeno. Kako bi
agent omogućio više ponašanja od jednom, potrebno je definirati novu metodu za postavljanje
kretnji. U našoj implementaciji, koristimo težine ponašanja kako bi iskoristili više ponašanja i
dali veću prednost onim ponašanjima koja smatramo važnijim.
1. public bool useBlendWeight;
2.
3. // ostatak koda …
4.
5. public void SetSteering(Steering steering, float weight) {
44
6. this.steering.linear += (weight * steering.linear);
7. this.steering.angular += (weight * steering.angular);
8. }
5.2.3. Kretanje igrača
Možemo reći da je glavni lik agent koji je pod utjecajem igrača koji igra igru. Stoga
ćemo za kontrolu glavnog lika kreirati klasu AgentPlayer. Navedena klasa nasljeđuje klasičnog
neprijatelja, samo unutar metode „Update“, dohvaća ulazne informacije sa tipkovnice i na
osnovu toga računa smjer kretanja i pomiče igrača. Rotacija se zasniva na smjeru kretanja miša,
računa se objekt tipa „Quaterion“ koji predstavlja rotaciju, na osnovu pozicije na podu u koju
pokazuje miš.
1. public class AgentPlayer: Agent {
2. public LayerMask MaskForTurning;
3. Rigidbody rb;
4. protected override void Start() {
5. base.Start();
6. rb = GetComponent < Rigidbody > ();
7. }
8. public override void Update() {
9. velocity.x = Input.GetAxis("Horizontal");
10. velocity.z = Input.GetAxis("Vertical");
11. velocity *= maxSpeed;
12. Vector3 translation = velocity * Time.deltaTime;
13. transform.Translate(translation, Space.World);
14. Quaternion quatOrient = GetTurningQuaterion(150 f, MaskForTurning);
15. rb.MoveRotation(quatOrient);
16. orientation = transform.rotation.eulerAngles.y;
17. }
18. public override void LateUpdate() {
19. SetAnimationSpeed(velocity.magnitude);
20. return;
21. }
22. public Quaternion GetTurningQuaterion(float camRayLength, LayerMask floorMask) {
23. Quaternion newRotation = Quaternion.identity;
24. Ray camRay = Camera.main.ScreenPointToRay(Input.mousePosition);
25. RaycastHit floorHit;
26. if (Physics.Raycast(camRay, out floorHit, camRayLength, floorMask)) {
27. Vector3 playerToMouse = floorHit.point - transform.position;
28. playerToMouse.y = 0 f;
29. newRotation = Quaternion.LookRotation(playerToMouse);
45
30. }
31. return newRotation;
32. }
33. }
5.2.4. Naganjanje i izbjegavanje
Naganjanje i izbjegavanja su jedan od često korištenih ponašanja u mnogim igrama, pa
tako u i u poznatoj Pacman igri se mogu lako uočiti. Navedena ponašanja imaju objekt kojeg
naganjaju ili proganjaju, te u obzir uzimaju i smjer njihovog trenutnog kretanja.
Kako bi napravili naganjanje, tj. 'pursue' i izbjegavanje 'evade', prvo ćemo napraviti
traženje 'seek' i bježanje 'flee'.
Klasa Seek implementira svoje ponašanje u virtualnoj metodi GetSteering u kojoj na
osnovu traženog, tj. target objekta računa smjer kretanja koji ovisi o akceleraciji agenta.
1. public class Seek: AgentBehaviour {
2. public override Steering GetSteering() {
3. Steering steering = new Steering();
4. steering.linear = target.transform.position - transform.position;
5. steering.linear.Normalize();
6. steering.linear = steering.linear * agent.maxAccel;
7. return steering;
8. }
9. }
Klasa Flee radi na identičan način, samo rezultat smjera kretanja je suprotan, računa
razliku vektora trenutne pozicije i pozicije traženog objekta, te kao rezultat dobiva smjer
suprotan smjeru traženog objekta.
1. public class Flee: AgentBehaviour {
2. public override Steering GetSteering() {
3. Steering steering = new Steering();
4. steering.linear = transform.position - target.transform.position;
5. steering.linear.Normalize();
6. steering.linear = steering.linear * agent.maxAccel;
7. return steering;
8. }
9. }
Nakon što imamo osnovu za ponašanja naganjanjem i izbjegavanje možemo kreirati
klasu Pursue. Unutar klase se nalazi varijabla koju možemo podesiti a to je maxPrediction, na
46
osnovu te varijable definiramo maksimalu udaljenost u kojoj agenti mogu predvidjeti kretanje
traženog objekta.
1. public class Pursue: Seek {
2. public float maxPrediction;
3. private GameObject targetAux;
4. private Agent targetAgent;
5. }
Varijable targetAux i targetAgent pohranjuju izvorni objekt kojeg naganjamo i
njegovu instancu Agent. Originalni objekt target zamjenjujemo novim, praznim objektom.
1. public override void Awake() {
2. base.Awake();
3. targetAgent = target.GetComponent < Agent > ();
4. targetAux = target;
5. target = new GameObject();
6. }
Unutar OnDestroy metode objekt target, koji je novo kreirani prazni objekt uništavamo
iz razloga što se koristi samo u ovom ponašanju i može se slobodno izbrisati kada se izbriše
trenutno ponašanje. OnDestroy metoda je metoda koja je automatski poziva od Unitija kada
naznačimo brisanje objekta, pa tako kada brišemo ponašanje sa našeg agenta, automatski će se
izbrisati i prazni target objekt.
1. void OnDestroy() {
2. Destroy(target);
3. }
Svrha praznog target objekta je napokon vidljiva u GetSteering metodi. Prvo se računa
smjer kretanja traženog objekta, zatim udaljenost, brzina, te na osnovu toga definiramo
varijablu prediction. Naš prazni objekt postavljamo na istu poziciju na kojoj se nalazi pravi
traženi objekt, s tim da mu pomjerimo poziciju u smjeru kretanja objekta s obzirom na
prediction. Zatim vraćamo rezultat iz osnovne klase Seek koja vraća smjer kretanja prema target
objektu, koji je u ovom slučaju pomjereni prazni objekt.
1. public override Steering GetSteering() {
2. Vector3 direction = targetAux.transform.position - transform.position;
3. float distance = direction.magnitude;
4. float speed = agent.velocity.magnitude;
5. float prediction;
6. if (speed <= distance / maxPrediction) prediction = maxPrediction;
47
7. else prediction = distance / speed;
8. target.transform.position = targetAux.transform.position;
9. target.transform.position += targetAgent.velocity * prediction;
10. return base.GetSteering();
11. }
Za ponašanje izbjegavanjem, kreirat ćemo Evade klasu unutar koje je sve identično kao
i u Pursue klasi, osim klase koju nasljeđuje.
1. public class Evade: Flee { 2. 3. // isto kao u klasi Pursue 4. }
5.2.5. Dostizanje na poziciju i napuštanje pozicije
Dostizanje na poziciju i napuštanje pozicije su implementirani redom u klasama, Arrive
i Leave.
Unutar Arrive klase koristimo više javnih varijabli. Varijabla targetRadius predstavlja
ciljani radijus od središta traženog objekta. Varijabla slowRadius predstavlja radijus unutar
kojeg agent usporava, te je vrijednost ove varijable veća od prethodne.
1. public class Arrive: AgentBehaviour {
2. public float targetRadius;
3. public float slowRadius;
4. public float timeToTarget = 0.1 f;
5. }
Slično naganjanju, dostizanje na poziciju također računa smjer prema traženom objektu.
Na početku se računa brzina s obzirom na udaljenost od objekta i na varijable slowRadius i
targetRadius. Zatim u nastavku računa potrebnu silu kretanja gdje se uzima u obzir akceleracija
i trenutna agentova sila kretanja.
1. public override Steering GetSteering() {
2. Steering steering = new Steering();
3. Vector3 direction = target.transform.position - transform.position;
4. float distance = direction.magnitude;
5. float targetSpeed;
6. if (distance < targetRadius) return steering;
7. if (distance > slowRadius) targetSpeed = agent.maxSpeed;
8. else targetSpeed = agent.maxSpeed * distance / slowRadius;
9. Vector3 desiredVelocity = direction;
48
10. desiredVelocity.Normalize();
11. desiredVelocity *= targetSpeed;
12. steering.linear = desiredVelocity - agent.velocity;
13. steering.linear /= timeToTarget;
14. if (steering.linear.magnitude > agent.maxAccel) {
15. steering.linear.Normalize();
16. steering.linear *= agent.maxAccel;
17. }
18. return steering;
19. }
Implementacija Leave klase je slična Arrive klasi, te umjesto targetRadius i slowRadius
imamo espaceRadius i dangerRadius.
6. public class Arrive: AgentBehaviour {
7. public float escapeRadius;
8. public float dangerRadius;
9. public float timeToTarget = 0.1 f;
10. }
Metoda GetSteering je jako slična kao kod klase Arrive. Razlika je samo u prvom dijelu
koda gdje se računa brzina kretanja na osnovu prethodno definiranih radijusa, te u definiranom
smjeru kretanja. Drugi dio koda nakon linije 9 je identičan.
1. public override Steering GetSteering() {
2. Steering steering = new Steering();
3. Vector3 direction = transform.position - target.transform.position;
4. float distance = direction.magnitude;
5. if (distance > dangerRadius) return steering;
6. float reduce;
7. if (distance < escapeRadius) reduce = 0 f;
8. else reduce = distance / dangerRadius * agent.maxSpeed;
9. float targetSpeed = agent.maxSpeed - reduce;
10. Vector3 desiredVelocity = direction;
11. desiredVelocity.Normalize();
12. desiredVelocity *= targetSpeed;
13. steering.linear = desiredVelocity - agent.velocity;
14. steering.linear /= timeToTarget;
15. if (steering.linear.magnitude > agent.maxAccel) {
16. steering.linear.Normalize();
17. steering.linear *= agent.maxAccel;
18. }
19. return steering;
49
20. }
Na Slika 5-1 možemo vidjeti vizualni prikaz navedenih radijusa i ponašanje dostizanja
na poziciju i napuštanje pozicije.
Slika 5-1: Dostizanje na poziciju i napuštanje pozicije (Palacios, 2016, p. 11)
5.2.6. Suočavanje
Ponašanje suočavanje, odnosno Face je ponašanje u kojem našeg agenta rotiramo u
smjeru traženog objekta. Prije nego što implementiramo klasu Face unutar AgentBehaviour
klase dodajemo novu metodu MapToRage. Metoda jednostavno pomaže za pronalazak smjera
rotacije.
1. public float MapToRange(float rotation) {
2. rotation %= 360.0 f;
3. if (Mathf.Abs(rotation) > 180.0 f) {
4. if (rotation < 0.0 f) rotation += 360.0 f;
5. else rotation -= 360.0 f;
6. }
7. return rotation;
8. }
Također prije implementacije suočavanja, implementirat ćemo osnovno ponašanje
usklađivanja, odnosno klasa Allign. Klasa koristi jednake principe kao i Arrive samo za rotaciju.
50
Klasa na osnovu trenutne orijentacije traženog agenta računa razliku i željenu brzinu dostizanja
rotacije kako bi se uskladila s orijentacijom traženog objekta.
1. public class Align: AgentBehaviour {
2. public float targetRadius;
3. public float slowRadius;
4. public float timeToTarget = 0.1 f;
5. public override Steering GetSteering() {
6. Steering steering = new Steering();
7. float targetOrientation = target.GetComponent < Agent > ().orientation;
8. float rotation = targetOrientation - agent.orientation;
9. rotation = MapToRange(rotation);
10. float rotationSize = Mathf.Abs(rotation);
11. if (rotationSize < targetRadius) return steering;
12. float targetRotation;
13. if (rotationSize > slowRadius) targetRotation = agent.maxRotation;
14. else targetRotation = agent.maxRotation * rotationSize / slowRadius;
15. targetRotation *= rotation / rotationSize;
16. steering.angular = targetRotation - agent.rotation;
17. steering.angular /= timeToTarget;
18. float angularAccel = Mathf.Abs(steering.angular);
19. if (angularAccel > agent.maxAngularAccel) {
20. steering.angular /= angularAccel;
21. steering.angular *= agent.maxAngularAccel;
22. }
23. return steering;
24. }
25. }
Napokon možemo impelentirati klasu suočavanja, tj. klasu Face. Klasa nasljeđuje
prethodno kreiranu klasu Allign i definira dodatnu varijablu targetAux. Poput ponašanja
naganjanja, targerAux varijabla pohranjuje pravi traženi objekt, dok target varijabla kreira
prazni objekt kojeg u GetSteering metodi prosljeđuje klasi Allign.
1. public class Face: Align {
2. protected GameObject targetAux;
3. public override void Awake() {
4. base.Awake();
5. targetAux = target;
6. target = new GameObject();
7. target.AddComponent < Agent > ();
8. }
9. void OnDestroy() {
51
10. Destroy(target);
11. }
12. }
U implementacija GetSteering metode, pomjeramo orijentaciju našeg praznog objekta
tako da je usmjerena u smjeru u kojem se nalazi naš traženi objekt, kako bi allign klasa vratila
usklađenu rotaciju.
1. public override Steering GetSteering() {
2. Vector3 direction = targetAux.transform.position - transform.position;
3. if (direction.magnitude > 0.0 f) {
4. float targetOrientation = Mathf.Atan2(direction.x, direction.z);
5. targetOrientation *= Mathf.Rad2Deg;
6. target.GetComponent < Agent > ().orientation = targetOrientation;
7. }
8. return base.GetSteering();
9. }
5.2.7. Lutanje
Lutanje je tehnika koja je savršena kada želimo postići nasumično kretanje. Prije
početka implementacije, unutar AgentBehaviour klase implementiramo dodatnu metodu
GetOriAsVec koja pretvara vrijednost orijentacije u vektor.
1. public Vector3 GetOriAsVec(float orientation) {
2. Vector3 vector = Vector3.zero;
3. vector.x = Mathf.Sin(orientation * Mathf.Deg2Rad) * 1.0 f;
4. vector.z = Mathf.Cos(orientation * Mathf.Deg2Rad) * 1.0 f;
5. return vector.normalized;
6. }
Klasa Wander nasljeđuje prethodno definiranu klasu Face. Ponašanje lutanjem
funkcionira na osnovu rotacije. Parametri offset, radius, rate definiraju vrijednosti na osnovu
kojih se nasumično definira smjer kretanja. Na slici je moguće vidjeti značenje tih varijabli.
1. public class Wander: Face {
2. public float offset;
3. public float radius;
4. public float rate;
5. public override void Awake() {
6. target = new GameObject();
7. target.transform.position = transform.position;
52
8. base.Awake();
9. }
10. }
Metoda GetSteering prvo definira nasumičnu orijentaciju na osnovu koje računa traženu
rotaciju s obzirom na trenutnu orijentaciju agenta.
1. public override Steering GetSteering() {
2. Steering steering = new Steering();
3. float wanderOrientation = Random.Range(-1.0 f, 1.0 f) * rate;
4. float targetOrientation = wanderOrientation + agent.orientation;
5. Vector3 orientationVec = OriToVec(agent.orientation);
6. Vector3 targetPosition = (offset * orientationVec) + transform.position;
7. targetPosition = targetPosition + (OriToVec(targetOrientation) * radius);
8. targetAux.transform.position = targetPosition;
9. steering = base.GetSteering();
10. steering.linear = targetAux.transform.position - transform.position;
11. steering.linear.Normalize();
12. steering.linear *= agent.maxAccel;
13. return steering;
14. }
Slika 5-2: Način funkcioniranja algoritma lutanja (Palacios, 2016, p. 16)
53
5.2.8. Suočavanje prema naprijed
Korištenjem prethodnih ponašanja, poput naganjanja, dostizanja na poziciju, nisu
obuhvaćali rotiranje, naš agent bi „klizio“ prema traženom objektu bez da se okrene u smjeru
kretanja. Stoga, suočavanje, odnosno rotiranje prema smjeru kretanja je jedno od važnijih
ponašanja ukoliko je bitna orijentacija našeg agenta.
Implementaciju vršimo u klasi FaceForward. Izračun orijentacije vršimo pomoću sile
smjera kretanja.
1. public class FaceForward: Align {
2. private Agent targetAgent;
3. public override void Awake() {
4. base.Awake();
5. }
6. void OnEnable() {
7. target = new GameObject();
8. targetAgent = target.AddComponent < Agent > ();
9. }
10. void OnDestroy() {
11. Destroy(target);
12. }
13. public override Steering GetSteering() {
14. Vector3 velocity = agent.velocity;
15. if (velocity.magnitude <= 0.0001 f) return new Steering();
16. targetAgent.orientation = Mathf.Atan2(velocity.x, velocity.z) * Mathf.Rad2Deg;
17. return base.GetSteering();
18. }
19. }
5.2.9. Izbjegavanje zidova
Tehnika izbjegavanja zidova funkcionira na osnovu sigurnosne zone i vektora normale.
Za provjeravanje zida, koristi se metoda Raycast iz Unity alata koja na osnovu vektora
provjerava ukoliko se nalazi prepreka sa kolizijama.
Varijable avoidDistance definira vrijednost udaljenosti od zida, lookAhead predstavlja
vrijednost dužine vektora koji provjerava postojanost zida. Unutar GetSteering metode nalazi
se implementacija zaobilaska zida. Ukoliko postoji prepreka, na osnovu vektora normale
dobivamo smjer suprotan od zida na temelju kojeg postavljamo poziciju praznog objekta. U
54
tom slučaju, osnovna klasa Seek računa i vraća vrijednost tipa Steering na osnovu prethodno
pozicioniranog objekta target.
1. public class AvoidWall: Seek {
2. public float avoidDistance;
3. public float lookAhead;
4. public override void Awake() {
5. base.Awake();
6. target = new GameObject();
7. }
8. public override Steering GetSteering() {
9. Steering steering = new Steering();
10. Vector3 position = transform.position;
11. Vector3 rayVector = agent.velocity.normalized * lookAhead;
12. Vector3 direction = rayVector;
13. RaycastHit hit
14. if (Physics.Raycast(position, direction, out hit, lookAhead)) {
15. position = hit.point + hit.normal * avoidDistance;
16. target.transform.position = position;
17. steering = base.GetSteering();
18. }
19. return steering
20. }
21. }
5.2.10. Izbjegavanje agenata
Izbjegavanje agenata je ponašanje koje omogućuje zaobilazak agenata koji su u blizini.
Način na koji dohvaćamo agente je pomoću GameManager klase, koja je objašnjena u poglavlju
1. Algoritam prolazi kroz sve agenta, računa njihove udaljenost i sile kretanja. Na osnovu toga
zatim računa vrijeme do kolizije kako bi pronašao agenta koji će prvi biti u koliziji sa trenutnim.
Ukoliko pronađe takvog agenta zaobilazi ga pomoću vektora smjera, odnosno vraća Steering
objekt sa željenim smjerom kretanja koji je suprotan od smjera u kojem se nalazi drugi agent.
1. public class AvoidAgent: AgentBehaviour {
2. public float CollisionRadius = 0.4 f;
3. public override Steering GetSteering() {
4. Steering steering = new Steering();
5. float shortestTime = Mathf.Infinity;
6. GameObject firstTarget = null;
55
7. float firstMinSeparation = 0.0 f;
8. float firstDistance = 0.0 f;
9. Vector3 firstRelativePos = Vector3.zero;
10. Vector3 firstRelativeVel = Vector3.zero;
11. foreach(GameObject t in GameManager.Instance.Enemies) {
12. if (t == null) continue;
13. Vector3 relativePos;
14. Agent targetAgent = t.GetComponent < Agent > ();
15. relativePos = t.transform.position - transform.position;
16. Vector3 relativeVel = targetAgent.velocity - agent.velocity;
17. float relativeSpeed = relativeVel.magnitude;
18. float timeToCollision = Vector3.Dot(relativePos, relativeVel);
19. timeToCollision /= relativeSpeed * relativeSpeed * -1;
20. float distance = relativePos.magnitude;
21. float minSeparation = distance - relativeSpeed * timeToCollision;
22. if (minSeparation > 2 * CollisionRadius) continue;
23. if (timeToCollision > 0.0 f && timeToCollision < shortestTime) {
24. shortestTime = timeToCollision;
25. firstTarget = t;
26. firstMinSeparation = minSeparation;
27. firstRelativePos = relativePos;
28. firstRelativeVel = relativeVel;
29. }
30. }
31. if (firstTarget == null) return steering;
32. if (firstMinSeparation <= 0.0 f || firstDistance < 2 * CollisionRadius)
firstRelativePos = firstTarget.transform.position;
33. else firstRelativePos += firstRelativeVel * shortestTime;
34. firstRelativePos.Normalize();
35. steering.linear = -firstRelativePos * agent.maxAccel;
36. return steering;
37. }
38. }
56
5.3. Donošenje odluka pomoću automata
Za donošenje odluka i kreiranje stanja u našoj igri, implementirat ćemo konačnih
automate. Prvo ćemo definirati osnovnu arhitekturu i osnovne klase kako bi poslije mogli
kreirati konkretna stanja.
5.3.1. Osnovna arhitektura
Automati se sastoje od uvjeta, prijelaza i stanja. Naša osnovna klasa za uvjete sadrži
virtualnu metodu Test koja vraća rezultat.
1. public class Condition {
2. public virtual bool Test() {
3. return false;
4. }
5. }
Tranzicija između dva stanja predstavljamo objektom koji sadrži željeno stanje, i uvjet
koji mora biti ispunjen kako bi se traženo stanje ostvarilo.
1. public class Transition {
2. public Condition condition;
3. public State target;
4. }
Zatim definiramo klasu State. Klasa State je zapravo komponenta u Unitiju, te kao takva
biti će prikazana unutar Unity sustava. Stanje može posjedovati različit broj tranzicija prema
drugom stanju koje pohranjujemo u transitions listi. Implementacija se sastoji od niza virtualnih
metoda koje trebaju biti implementirane prilikom izrade konkretnog stanja. Metoda LateUpdate
testira ispunjenost uvjeta svake od tranzicija, te ukoliko je uvjet ispunjen, trenutno stanje se
deaktivira, i željeno uključuje.
1. public class State: MonoBehaviour {
2. public List < Transition > transitions;
3. public virtual void Awake() {
4. transitions = new List < Transition > ();
5. }
6. public virtual void OnEnable() {
7. // TO-DO develop state's initialization here
8. }
9. public virtual void OnDisable() {
10. // TO-DO develop state's finalization here
57
11. }
12. public virtual void Update() {
13. // TO-DO develop behaviour here
14. }
15. public void LateUpdate() {
16. foreach(Transition t in transitions) {
17. if (t.condition.Test()) {
18. t.target.enabled = true;
19. this.enabled = false;
20. return;
21. }
22. }
23. }
24. }
5.3.2. Definiranje osnovnih uvjeta
Nakon kreirane arhitekture, možemo kreirati osnovne uvjete koji su najčešće korišteni.
Najjednostavniji uvjet je uvjet koji direktno vraća vrijednost tipa bool.
1. public class ConditionBool: Condition {
2. public bool testBool;
3. public override bool Test() {
4. return testBool;
5. }
6. }
Zatim jedan od čestih uvjeta je uvjet provjeravanja vrijednosti. ConditionFloat klasa
provjerava dali je testna vrijednost unutar željenog raspona.
1. public class ConditionFloat: Condition {
2. public float valueMin;
3. public float valueMax;
4. public float valueTest;
5. public override bool Test() {
6. if (valueMax >= valueTest && valueTest >= valueMin) return true;
7. return false;
8. }
9. }
Idući uvjet je uvjet testira vrijednost dvaju uvjeta i vraća istinitost ukoliko su oba istinita.
1. public class ConditionAnd: Condition {
2. public Condition conditionA;
58
3. public Condition conditionB;
4. public override bool Test() {
5. if (conditionA.Test() && conditionB.Test()) return true;
6. return false;
7. }
8. }
Nešto kompleksniji uvjet je uvjet u kojem provjeravamo dali je objekt dovoljno udaljen.
ConditionTargetFar je klasa koja računa udaljenost i provjerava dali je objekt udaljeniji od
maksimalne udaljenosti.
1. public class ConditionTargetFar: Condition {
2. public GameObject origin;
3. public GameObject target;
4. public float maxDistance;
5. public override bool Test() {
6. if (origin == null || target == null) return false;
7. Vector3 originPos = origin.transform.position;
8. Vector3 targetPos = target.transform.position;
9. if (Vector3.Distance(originPos, targetPos) > maxDistance) return true;
10. return false;
11. }
12. }
Idući uvjet provjerava upravo suprotno, ukoliko je traženi objekt unutar određene
udaljenosti.
1. public class ConditionTargetClose: Condition {
2. public GameObject origin;
3. public GameObject target;
4. public float minDistance;
5. public override bool Test() {
6. if (origin == null || target == null) return false;
7. Vector3 originPos = origin.transform.position;
8. Vector3 targetPos = target.transform.position;
9. if (Vector3.Distance(originPos, targetPos) <= minDistance) return true;
10. return false;
11. }
12. }
Zatim idući uvjet provjerava vidljivost traženog objekta.
59
1. public class ConditionTargetVisible: Condition {
2. public GameObject origin;
3. public GameObject target;
4. public override bool Test() {
5. bool passTest = false;
6. if (origin != null && target != null) {
7. Vector3 originPos = origin.transform.position + Vector3.up;
8. Vector3 targetPos = target.transform.position + Vector3.up;
9. Vector3 direction = targetPos - originPos;
10. RaycastHit hit;
11. if (Physics.Raycast(originPos, direction, out hit)) {
12. if (hit.collider.gameObject.Equals(target)) passTest = true;
13. else passTest = false;
14. }
15. }
16. return passTest;
17. }
18. }
5.3.3. Implementacija stanja
Stanja su definirana u poglavlju 0 gdje se navode neprijatelji temeljeni na automatima i
neprijatelji sa formacijama.
Prvo definiramo osnovni predložak za stanja koja mogu prijeći u stanje smrti.
StateDefault je klasa koja definira traženi predložak. Pomoću instance tipa StateHolder, dolazi
do komponente EnemyEntity što je ujedno osnovna klasa bilo kojeg neprijatelja u igri. U toj
klasi postoji događaj pod nazivom OnEmptyHealth na koji se klasa pretplaćuje. Na taj način
štedimo performanse izbjegavajući učestalo provjeravanje snage kako bi mogli izvršiti
tranziciju u stanje smrti. Također, u istoj klasi postavljamo unutar metode OnEnable aktivno
stanje za StateHolder komponentu.
1. public class StateDefault: State {
2. public StateHolder Owner;
3. public StateDie StateDie;
4. private ConditionBool _condition;
5. public override void Awake() {
6. base.Awake();
7. _condition = new ConditionBool();
8. _condition.test = false;
9. if (StateDie) {
60
10. Owner.GetComponent <EnemyEntity> ().OnEmptyHealth.AddListener(OnEmptyHealth);
11. Transition transition = new Transition();
12. transition.condition = _condition;
13. transition.target = StateDie;
14. transitions.Add(transition);
15. }
16. }
17. void OnEmptyHealth() {
18. _condition.test = true;
19. }
20. public override void OnEnable() {
21. base.OnEnable();
22. Owner.ActiveState = this;
23. }
24. }
Zatim možemo implementirati i stanje smrti. Navedeno stanje je jednostavno za
implementirati ukoliko se glavna logika događa u klasi neprijatelja, kao što je u ovom slučaju
klasa EnemyEntity. Stanje smrti jednostavno se pozove kada je snaga neprijatelja jednaka nuli,
gdje stanje poziva metodu pod nazivom „Die“ koju posjeduje svaki od entiteta neprijatelja.
Zahvaljujući toj arhitekturi stanje je jednostavno za implementirati.
1. public class StateDie: StateDefault {
2. public override void Awake() {
3. base.Awake();
4. }
5. public override void OnEnable() {
6. base.OnEnable();
7. if (Owner.GetComponent < IEntity > () != null)
Owner.GetComponent < IEntity > ().Die();
8. }
9. public override void OnDisable() {
10. base.OnDisable();
11. }
12. }
Nešto kompleksnije stanje je stanje Attack. To je stanje koje koristi neprijatelj
FighterBot. Unutar stanja se definiraju osnovna svojstva napada, poput jačine i brzine napada.
Zatim koristi se objekt DamageDealer tipa OnColliderEvents koji izvještava klasu o kolizijama.
Kada se traženi objekt (igrač) nađe u koliziji sa trenutnim agentom, pozvat će se prvo metoda
OnDamageTriggerStart, te zatim OnDamageTriggerEnter gdje zapravo igraču oduzimamo
61
energiju. U metodi OnDamageTriggerExit isključujemo animaciju za napad. Prilikom
uključivanja samog stanja, uključujemo ponašanje kretanjem, koje u ovom slučaju pomoću
unity alata postavljamo na ponašanje naganjanjem. Kod isljučivanja stanja, ponašanje
isključujemo. Postoji i dodatno ponašanje, definirano nazivom AdditionalBehavior. koje se
može koristiti.
1. public class StateAttack: StateDefault {
2. [Tooltip("Usually Face (for formation enemy) or Pursue")]
public AgentBehaviour ActiveBehaviour;
3. [Tooltip("Usually FaceForward of similar behavior used with Arrive/Pursue")]
public AgentBehaviour AdditionalBehavior;
4. [Header("Attack Properties")]
public float AttackDamage = 2 f;
5. public float AttackFrequency = 0.5 f;
6. public OnColliderEvents DamageDealer;
7. public GameObject _target;
8. public override void Awake() {
9. base.Awake();
10. if (!DamageDealer) {
11. DamageDealer = Owner.GetComponentInChildren < OnColliderEvents > ();
12. }
13. }
14. public override void OnEnable() {
15. base.OnEnable();
16. _target = GameManager.Instance.Player;
17. ActiveBehaviour.target = _target;
18. ActiveBehaviour.enabled = true;
19. if (AdditionalBehavior) AdditionalBehavior.enabled = true;
20. if (DamageDealer) {
21. DamageDealer.TriggerStayFrequency = AttackFrequency;
22. DamageDealer.OnTriggerEnterEvent.AddListener(OnDamageTriggerStart);
23. DamageDealer.OnTriggerStayEvent.AddListener(OnDamageTriggerEnter);
24. DamageDealer.OnTriggerExitEvent.AddListener(OnDamageTriggerExit);
25. }
26. }
27. public override void OnDisable() {
28. base.OnDisable();
29. ActiveBehaviour.enabled = false;
30. if (AdditionalBehavior) AdditionalBehavior.enabled = false;
31. if (DamageDealer) {
32. DamageDealer.OnTriggerEnterEvent.RemoveListener(OnDamageTriggerStart);
33. DamageDealer.OnTriggerStayEvent.RemoveListener(OnDamageTriggerEnter);
62
34. DamageDealer.OnTriggerExitEvent.RemoveListener(OnDamageTriggerExit);
35. }
36. }
37. void OnDamageTriggerStart() {
38. Owner.GetComponent < EntityAnimations > ().
SetAnimation(EntityAnimations.EntityAnimType.Attack, true);
39. }
40. void OnDamageTriggerEnter() {
41. DamagableEntity playerDmg = _target.GetComponent < DamagableEntity > ();
42. if (playerDmg) {
43. playerDmg.ApplyDamage(AttackDamage);
44. }
45. }
46. void OnDamageTriggerExit() {
47. Owner.GetComponent < EntityAnimations > ().
SetAnimation(EntityAnimations.EntityAnimType.Attack, false);
48. }
49. }
Kod neprijatelja HealerBot, koristimo ponašanje dostizanja na poziciju. Također
kompleksniji oblik stanja gdje prvo pomoću ComponentPicker klase tražimo prigodne
komponente za liječenje. Traženje se izvršava svakih 5 sekundi tako da ukoliko u međuvremenu
druga komponenta zahtjeva veću potrebu za liječenje, onda se bira ta nova komponenta. Stanje
je aktivno sve dok igrač ne zakorači u prostor blizu komponente gdje može započeti stanje
liječenja. U tom trenutku, se poziva metoda OnHealEnter koja postavlja trenutnu komponentu
stanju liječenja, i postavlja uvjet za liječenje. Poput prethodnog stanja, ovo stanje također
uključuje dva ponašanja koja su točno definirana, a to su dostizanje na poziciju u suočavanje
prema naprijed, tako da agent može doći do komponente sa pravilnom rotacijom.
1. public class StateArriveToComponent: StateDefault {
2. [Header("Seek Component Parameters")]
public OnColliderEvents HealDealer;
3. public ComponentPicker ComponentPicker;
4. public StateHeal NextState;
5. [Header("ReadOnly")]
public DamagableEntity TargetComponent;
6. private ConditionBool _conditionToHeal;
7. private FaceForward _faceForwardBehaviour;
8. public Arrive _arriveBehaviour;
9. private WaitForSeconds _wait = new WaitForSeconds(5 f);
10. public override void Awake() {
63
11. base.Awake();
12. if (!HealDealer)
13. HealDealer = Owner.GetComponentInChildren < OnColliderEvents > ();
14. _arriveBehaviour = Owner.GetComponent < Arrive > ();
15. if (!_arriveBehaviour)
_arriveBehaviour = Owner.gameObject.AddComponent < Arrive > ();
16. _faceForwardBehaviour = Owner.GetComponent < FaceForward > ();
17. if (!_faceForwardBehaviour)
_faceForwardBehaviour = Owner.gameObject.AddComponent < FaceForward > ();
18. _conditionToHeal = new ConditionBool();
19. _conditionToHeal.test = false;
20. if (NextState) {
21. Transition transition = new Transition();
22. transition.condition = _conditionToHeal;
23. transition.target = NextState;
24. transitions.Add(transition);
25. }
26. }
27. public override void OnEnable() {
28. base.OnEnable();
29. _conditionToHeal.test = false;
30. _faceForwardBehaviour.enabled = true;
31. StartCoroutine(PerformSearch());
32. if (HealDealer)
33. HealDealer.OnTriggerEnterTransformEvent.AddListener(OnHealEnter);
34. }
35. public override void OnDisable() {
36. base.OnDisable();
37. _arriveBehaviour.enabled = false;
38. _faceForwardBehaviour.enabled = false;
39. if (HealDealer)
40. HealDealer.OnTriggerEnterTransformEvent.RemoveListener(OnHealEnter);
41. }
42. void SearchAndSetTargetComponent() {
43. TargetComponent= ComponentPicker.GetBestComponent(GameManager.Instance.Components);
44. ComponentPicker.SetCurrentComponent(TargetComponent);
45. if (TargetComponent != null) {
46. _arriveBehaviour.target = TargetComponent.gameObject;
47. _arriveBehaviour.enabled = true;
48. }
49. }
50. public IEnumerator PerformSearch() {
51. SearchAndSetTargetComponent();
64
52. yield return _wait;
53. if (!_conditionToHeal.test)
54. yield return PerformSearch();
55. }
56. void OnHealEnter(Transform transf) {
57. DamagableEntity component = transf.GetComponentInParent < DamagableEntity > ();
58. if (component && component == TargetComponent) {
59. if (NextState)
60. NextState.CurrentComponent = TargetComponent;
61. _conditionToHeal.test = true;
62. }
63. }
64. }
Zatim iduće stanje je stanje liječenja. Isječak klase je moguće vidjeti ispod, zbog
veličine koda, metoda Awake je skraćena, njezina implementacija je skoro identična kao kod
prethodnih stanja. Na početku, stanje liječenja poziva metodu OnHealEnter gdje postavlja
animaciju liječenja, te na kraju stanja poziva metodu OnHealExit koja isključuje animaciju
liječenja. Sve dok je agent unutar prostora za liječenje, pozivat će se metoda OnHeal gdje se
vrši povećavanje energije komponente. Stanje završava u trenutku kada metoda IsHealingDone
vrati istinit rezultat.
1. public class StateHeal: StateDefault {
2. [Header("Heal Properties")]
public float HealDamage = 2 f;
3. public float HealFrequency = 0.5 f;
4. public float MinHealTimeBeforeSearch = 4 f;
5. public float MaxHealTimeBeforeSearch = 8 f;
6. public OnColliderEvents HealDealer;
7. public State NextState;
8. [Header("ReadOnly")]
public DamagableEntity CurrentComponent;
9. protected ConditionBool _conditionState;
10. private float _lastWrittenTime = 0 f;
11. private bool _isHealing = false;
12. public AgentBehaviour _faceBehaviour;
13. public override void Awake() {
14. base.Awake();
15. // INICIJALIZACIJA I DOHVAT KOMPONENTI
16. }
17. public override void OnEnable() {
18. base.OnEnable();
65
19. _lastWrittenTime = Time.time;
20. _conditionState.test = false;
21. if (CurrentComponent) {
22. _faceBehaviour.target = CurrentComponent.gameObject;
23. _faceBehaviour.enabled = true;
24. if (HealDealer) {
25. HealDealer.TriggerStayFrequency = HealFrequency;
26. HealDealer.OnTriggerStayEvent.AddListener(OnHeal);
27. OnHealEnter();
28. }
29. } else
30. Debug.LogError("No Component Found");
31. }
32. public override void OnDisable() {
33. base.OnDisable();
34. _faceBehaviour.enabled = false;
35. if (HealDealer) {
36. HealDealer.OnTriggerStayEvent.RemoveListener(OnHeal);
37. OnHealExit();
38. }
39. }
40. public override void Update() {
41. base.Update();
42. _conditionState.test = IsHealingDone();
43. }
44. void OnHealEnter() {
45. Owner.GetComponent < EntityAnimations > ().
SetAnimation(EntityAnimations.EntityAnimType.Heal, true);
46. _isHealing = true;
47. }
48. void OnHeal() {
49. if (!CurrentComponent) return;
50. CurrentComponent.ApplyDamage(HealDamage * -1);
51. _isHealing = true;
52. }
53. void OnHealExit() {
54. Owner.GetComponent < EntityAnimations > ().SetAnimation(EntityAnimations.EntityAnim
Type.Heal, false);
55. _isHealing = false;
56. }
57. bool IsHealingDone() {
58. bool healingDone = false;
66
59. if (CurrentComponent != null && CurrentComponent.Health.IsFullHealth())
healingDone = true;
60. if (!_isHealing) healingDone = true;
61. if (Time.time - _lastWrittenTime < MinHealTimeBeforeSearch) healingDone = false;
62. else if(Time.time-_lastWrittenTime > MaxHealTimeBeforeSearch) healingDone = true;
63. return healingDone;
64. }
65. }
67
5.4. Donošenje odluka pomoću stabla ponašanja
Implementacija stabla ponašanja je slična implementaciji prethodnih algoritama. Prvo
se implementiraju klase koje se trebaju nadalje proširiti sa vlastitom implementacijom. Način
na koji implementiramo stablo ponašanja se temelji na korutinama (engl. Courutines) Korutine
su metode koje omogućuju pauziranje izvođenja funkcije na određeni vremenski raspon.
Upravo to svojstvo je idealno za stablo ponašanja, jer stablo je strukturirano na rekurzivan
način, gdje određeni tipovi zadataka pozivaju zadatke djece koji mogu potrajati, te zahvaljujući
korutinama, idući zadatak će se pokrenuti tek nakon što trenutni završi sa radom.
Prvo ćemo kreirati osnovne klase: Task, Action, Condition, Decorator.
5.4.1. Implementacija osnovnih klasa
U stablu ponašanja najosnovnija klasa koju prvo implementiramo je klasa Task. Unutar
navedene klase se nalazi osnovna logika stabla ponašanja koju nadalje koriste sve klase.
Zadatak se sastoji od niza djece koji su zapravo zadaci pohranjeni u listi pod nazivom
children. Zadatak mora imati rezultat, koji predstavljamo pomoću tipa bool te pohranjuje
informaciju o završenosti zadatka. Naime implementacije stabla ponašanja mogu biti
implementirane na druge načine. Jedan od čestih oblika ne vraća bool tip podataka, već vraća
tip koji ima vrijednosti „Runing“, „Success“, „Fail“. Zbog korištenja tzv. Courutines nemamo
potrebu korištenja tog načina zbog toga što stablo ponašanje neće nastaviti dalje dok zadatak
koji se izvršava nije gotov.
1. public class Task: MonoBehaviour {
2. public Blackboard Blackboard;
3. public List < Task > children;
4. protected bool result = false;
5. protected bool isFinished = false;
6. protected WaitForFixedUpdate _waitForFixedUpdate = new WaitForFixedUpdate();
7. public virtual void SetResult(bool r) {
8. result = r;
9. isFinished = true;
10. }
11. public virtual IEnumerator Run(Blackboard blackboard) {
12. SetResult(true);
13. yield
14. return _waitForFixedUpdate;
15. }
68
16. public virtual IEnumerator RunTask() {
17. yield
18. return StartCoroutine(Run(Blackboard));
19. }
20. public bool GetResult() {
21. return result;
22. }
23. public bool IsFinished() {
24. return isFinished;
25. }
26. }
Osnovna klasa za uvjete implementira virtualnu metodu Run. Unutar te metode uvjeti
provjeravaju dali su određena svojstva zadovoljena ili ne, te na osnovu toga postavljaju rezultat.
1. public class ConditionBT: Task {
2. public override IEnumerator Run() {
3. isFinished = false;
4. bool r = false;
5. // implement your behaviour here
6. // define result (r) whether true or false
7. SetResult(r);
8. yield break;
9. }
10. }
Akcije u stablu su implementirane na sličan način kao i uvjeti. Glavna razlika akcija je
ta što akcije uvijek vraćaju uspješan rezultat, te nema potrebe za postavljanjem rezultata.
Ukoliko imamo iznimku gdje zadatak može vratiti rezultat, jednostavno postavimo rezultat kao
i kod uvjeta i prekinemo izvođenje metode pomoću „yield break“.
1. public class ActionBT: Task {
2. public override IEnumerator Run() {
3. isFinished = false;
4. // implement your behaviour here
5. return base.Run();
6. }
7. }
Osnovna klasa za dekoratore je jako jednostavna. Dekorator posjeduje samo jedno
dijete.
1. public class Decorator: Task {
69
2. public Task child;
3. }
5.4.2. Implementacija kompozita
Implementacija kompozita, se odnosi na implementaciju sekvence i selektora.
Implementacija selektora je prikazana ispod. Prolazeći kroz listu zadataka, selektor pomoću
naredbe „yield return“ pokreće izvođenje zadatka djeteta. Ostatak koda (nakon linije 8) će se
izvršiti tek nakon što je dijete izvršilo potreban zadatak, zahvaljujući korutinama. U slučaju da
dijete vrati uspješan rezultat, selekcija završava.
1. public class Selector: Task {
2. public override void SetResult(bool r) {
3. if (r == true) isFinished = true;
4. }
5. public override IEnumerator RunTask() {
6. foreach(Task t in children) {
7. t.Blackboard = Blackboard;
8. yield return StartCoroutine(t.RunTask());
9. if (t.GetResult()) {
10. SetResult(true);
11. yield break;
12. }
13. }
14. }
15. }
Za razliku od selektora, sekvenca provjerava dali je zadatak djeteta neuspješan, te
ukoliko je neuspješan, prekida izvršavanje i vraća neuspjeh.
1. public class Sequence: Task {
2. public override void SetResult(bool r) {
3. if (r == true) isFinished = true;
4. }
5. public override IEnumerator RunTask() {
6. foreach(Task t in children) {
7. t.Blackboard = Blackboard;
8. yield return StartCoroutine(t.RunTask());
9. if (!t.GetResult()) {
10. result = false;
11. SetResult(false);
12. yield break;
70
13. }
14. }
15. SetResult(true);
16. }
17. }
Osim navedene dvije implementacije, kreirat ćemo i nedeterminističku sekvencu i
selekor. Jedina razlika u implementaciji što prije prolaska kroz listu djece, lista se izmiješa.
1. public class NonDeterministicSelector: Task {
2. public override void SetResult(bool r) {
3. if (r == true) isFinished = true;
4. }
5. public override IEnumerator RunTask() {
6. yield return ShuffleTasks();
7. // Isječak isti kao kod implementacije sekvence
8. }
9. IEnumerator ShuffleTasks() {
10. children.Shuffle < Task > ();
11. yield return _waitForFixedUpdate;
12. }
13. }
Izmjena u implementaciji nedeterminističke sekvence.
1. public class NonDeterministicSequence: Task {
2. public override void SetResult(bool r) {
3. if (r == true) isFinished = true;
4. }
5. public override IEnumerator RunTask() {
6. yield return ShuffleTasks();
7. // Isječak isti kao kod implementacije sekvence
8. }
9. IEnumerator ShuffleTasks() {
10. children.Shuffle < Task > ();
11. yield return _waitForFixedUpdate;
12. }
13. }
Metoda Shuffle na osnovu koje se miješaju zadaci je prikazana u isječku ispod.
1. public static void Shuffle < T > (this IList < T > list) {
2. int n = list.Count;
3. while (n > 1) {
4. n--;
71
5. int k = ThreadSafeRandom.ThisThreadsRandom.Next(n + 1);
6. T value = list[k];
7. list[k] = list[n];
8. list[n] = value;
9. }
10. }
5.4.3. Implementacija dekoratora
Prvi dekorator koji ćemo implementirati je dekorator BlackboardManager. Navedeni
dekorator se odnosi na prijenos podataka, odnosno naznačava da dijete dekoratora će imati novu
instancu podataka. Prijenos podataka kroz stablo ponašanja je jedan od uobičajenih problema
koji se može riješiti na više načina. U ovom slučaju, koristimo tzv. Blackboard objekt koji se
sastoji od niza parova key, value gdje su pohranjeni podaci. Isti skup podataka se koristi u
cijelom stablu. Taj pristup je dobar ali postoji problem, ukoliko sa ključem „target“ naznačimo
referencu na glavnog igrača, onda mijenjanjem tog svojstva mogu nastati razni problemi. Iz tog
razloga koristimo BlackboardManager koji djetetu instancira novu instancu podataka koja neće
mijenjati postojeću. Na taj način, ostatak stabla će koristiti svoju referencu za ključ target, dok
će manji dio stabla promijeniti referencu i koristiti isti ključ.
Implementacije podataka, tj. Blackboard je vidljiva ispod. Kako bi mogli unutar Unity
alata definirati vrijednosti i ključeve, implementacija ograničava vrijednost podataka na:
GameObject, int, float, string tip podataka.
1. public class Blackboard {
2. public Blackboard Parent;
3. public class Data {
4. public string Name;
5. public GameObject Target;
6. public int intValue;
7. public float floatValue;
8. public string stringValue;
9. public Data(string name, GameObject target) {
10. Name = name;
11. Target = target;
12. }
13. public Data(string name, int intValue) {
14. Name = name;
15. this.intValue = intValue;
72
16. }
17. public Data(string name, float floatValue) {
18. Name = name;
19. this.floatValue = floatValue;
20. }
21. public Data(string name, string stringValue) {
22. Name = name;
23. this.stringValue = stringValue;
24. }
25. }
26. public List < Data > data = new List < Data > ();
27. public void Add(Data d) {
28. data.Add(d);
29. }
30. public void Add(string name, Data d) {
31. bool dataInserted = false;
32. for (int i = 0; i < data.Count; i++) {
33. if (data[i].Name.Equals(name)) {
34. data[i] = d;
35. dataInserted = true;
36. break;
37. }
38. }
39. if (!dataInserted) Add(d);
40. }
41. public Data Get(string name) {
42. Data currentData = null;
43. foreach(Data d in data) if (d.Name.Equals(name)) currentData = d;
44. if (currentData == null && Parent != null) currentData = Parent.Get(name);
45. return currentData;
46. }
47. }
Klasa BlackboardHolder je klasa koja nasljeđuje MonoBehaviour, te u svojoj
implementaciji sadrži samo instancu Blackboard objekta.
Implementacija samog dekoratora BlackboardManager je prilično jednostavna.
Definira se nova instanca podataka tipa Blackboard, kao roditelj se postavi trenutna instanca
podataka te se djetetu proslijede podaci i pokreće se zadatak djeteta.
1. public class BlackboardManager: Decorator {
2. public override IEnumerator Run(Blackboard blackboard) {
3. Blackboard new_bb = new Blackboard();
73
4. new_bb.Parent = blackboard;
5. child.Blackboard = new_bb;
6. yield return child.RunTask();
7. new_bb = null;
8. SetResult(child.GetResult());
9. yield return _waitForFixedUpdate;
10. }
11. }
Idući dekorat je Inverter. Kao što je prethodno navedeno, služi kako bi vratio suprotan
rezultat. Inverter jednostavno pokrene zadatak djeteta, te sebi postavi rezultat suprotan rezultatu
djeteta.
1. public class Inverter: Decorator {
2. public override IEnumerator Run(Blackboard blackboard) {
3. child.Blackboard = blackboard;
4. yield return child.RunTask();
5. SetResult(!child.GetResult());
6. yield return _waitForFixedUpdate;
7. }
8. }
Jedan od popularnih dekorata je Until Fail. Funkcionira tako da izvršava dijete sve dok
dijete ne vrati negativan rezultat. U tom slučaju prekida izvođenje djeteta i postavlja vrijednost
na neuspjeh. Kombinacijom Until Fail i Inverter dekoratora dobivamo dekorator Until Success
kojeg iz tog razloga nije potrebno posebno kreirati.
1. public class UntilFail: Decorator {
2. public override IEnumerator Run(Blackboard blackboard) {
3. child.Blackboard = blackboard;
4. isFinished = false;
5. while (true) {
6. yield return child.RunTask();
7. if (!child.GetResult()) break;
8. else result = true;
9. }
10. SetResult(false);
11. yield return _waitForFixedUpdate;
12. }
13. }
74
5.4.4. Implementacija uvjeta
Prvi uvjet kojeg implementiramo je uvjet u kojem provjeravamo dali komponente
trebaju liječenje. Koristi se jednaka komponenta kao i kod implementacije automata, odnosno
stanja liječenja, ComponentPicker. Na osnovu te komponente provjeravamo jednostavno dali
su sve komponente izlječenje, i ukoliko uvjet nije istinit, vraćamo uspješan rezultat.
1. public class ComponentsNeedHeal: Task {
2. public ComponentPicker Picker;
3. public override IEnumerator Run(Blackboard blackboard) {
4. isFinished = false;
5. bool r = false;
6. if (!Picker.AreComponentsAtMaxHealth(GameManager.Instance.Components)) {
7. r = true;
8. }
9. SetResult(r);
10. yield return _waitForFixedUpdate;
11. }
12. }
Zatim uvjet u kojem provjeravamo energiju traženog objekta. Ovaj uvjet se koristi
najviše za provjeru energije igrača. Dohvaća se igračeva komponenta koja implementira sučelje
IDamagable te na osnovu toga provjeravamo ukoliko se igračeva energija nalazi unutar
traženog raspona.
1. public class HealthCondition: Task {
2. public float MinHealth;
3. public float MaxHealth;
4. public HealthProperties currentHealth = null;
5. public override IEnumerator Run(Blackboard blackboard) {
6. isFinished = false;
7. bool r = false;
8. GameObject target = blackboard.Get("target").Target;
9. if (currentHealth == null)
currentHealth = target.GetComponent < IDamagable > ().GetHealth();
10. if (MinHealth <= currentHealth.CurrentHealth
&& MaxHealth >= currentHealth.CurrentHealth) {
11. r = true;
12. }
13. SetResult(r);
14. yield return _waitForFixedUpdate;
15. }
16. }
75
Idući uvjet je uvjet provjeravanja vlastite energije koji je identičan prethodnom uz
glavnu razliku što sa naše skupine podataka uzimamo ključ pod nazivom „owner“. Naime, ova
varijanta se mogla izbjeći korištenjem dekoratora BlackboardManager s tim da bi to uvjetovalo
kreiranjem novog zadatka koji postavlja objekt kojem provjeravamo energiju.
1. public class OwnerHealthCondition: Task {
2. public float MinHealth;
3. public float MaxHealth;
4. public HealthProperties currentHealth = null;
5. public override IEnumerator Run(Blackboard blackboard) {
6. isFinished = false;
7. bool r = false;
8. GameObject target = blackboard.Get("owner").Target;
9. if (currentHealth == null)
currentHealth = target.GetComponent < IDamagable > ().GetHealth();
10. if (MinHealth <= currentHealth.CurrentHealth
&& MaxHealth >= currentHealth.CurrentHealth) {
11. r = true;
12. }
13. SetResult(r);
14. yield return _waitForFixedUpdate;
15. }
16. }
Jedan od osnovnih uvjeta je provjeravanje udaljenosti. Uvjet vraća uspjeh ukoliko je
udaljenost manaja od maksimalne udaljenosti. Korištenjem invertera može se dobiti suprotan
rezultat. Uvjet računa udaljenost između traženog objekta, i vlasnika stabla, te vraća istinitost
ukoliko je ta udaljenost manja ili jednaka maksimalnoj.
1. public class ProximityCondition: Task {
2. public Transform Target;
3. public float MaxDistance;
4. public float CurrentDistance = 0;
5. public override IEnumerator Run(Blackboard blackboard) {
6. isFinished = false;
7. bool r = false;
8. Target = blackboard.Get("target").Target.transform;
9. GameObject owner = blackboard.Get("owner").Target;
10. if (Target != null) {
11. float distance = Vector3.Distance(Target.position, owner.transform.position);
12. CurrentDistance = distance;
76
13. if (distance <= MaxDistance) r = true;
14. }
15. SetResult(r);
16. yield return _waitForFixedUpdate;
17. }
18. }
Posljednji uvjet koji je implementiran je provjeravanje ukoliko je traženi objekt unutar
kolizije. Uvjet se zasniva na OnColliderEvents komponenti koja je i prethodno korištena kod
automata. Prvo se pretplaćujemo na događaje OnTriggerEnter te OnTriggerExit i na osnovu
toga u listu objekata dodajemo objekte koji su u koliziji i izbacujemo objekte koji više nisu u
koliziji. Kada se pozove zadatak, odnosno Run metoda ovog uvjeta, jednostavno se provjeri
dali je traženi objekt unutar liste objekata koji su u koliziji.
1. public class InTriggerCollider: Task {
2. public OnColliderEvents CollisionChecker;
3. public List < GameObject > _inCollisionObjects = new List < GameObject > ();
4. void Start() {
5. CollisionChecker.OnTriggerEnterTransformEvent.AddListener(TrigerEntered);
6. CollisionChecker.OnTriggerExitTransformEvent.AddListener(TrigerExited);
7. }
8. void TrigerEntered(Transform obj) {
9. if (obj.parent != null) _inCollisionObjects.Add(obj.parent.gameObject);
10. else _inCollisionObjects.Add(obj.gameObject);
11. }
12. void TrigerExited(Transform obj) {
13. if (obj.parent != null) _inCollisionObjects.Remove(obj.parent.gameObject);
14. else _inCollisionObjects.Remove(obj.gameObject);
15. }
16. public override IEnumerator Run(Blackboard blackboard) {
17. isFinished = false;
18. bool r = false;
19. if (_inCollisionObjects.Contains(blackboard.Get("target").Target)) {
20. r = true;
21. }
22. SetResult(r);
23. yield return _waitForFixedUpdate;
24. }
25. }
77
5.4.5. Implementacija akcija
Lista implementiranih akcija je nešto veća, postoje akcije: DoAttack,
DoEnableBehavior, DoEvade, DoHealing, DoPlayAnimation, DoPursue, DoStopEvade,
DoStopPursue, DoWait, MoveTo, StopMoveTo, SelectComponent. Pošto je popis zadataka
nešto veći, prikazat ćemo samo neke od njih.
Prva akcija koju implementiramo je akcija koja se jako često koristi, a to je akcija
čekanja. Akcija čekanja jednostavno pauzira zadatak na određeno vrijeme. Zahvaljujući
korištenju korutina i arhitekturi zadataka, cijelo stablo ponašanja će čekati na taj zadatak i
nastavit će tek kada vrijeme istekne.
1. public class DoWait: ActionBT {
2. public float WaitTime = 1 f;
3. protected WaitForSeconds _waitTime;
4. void Awake() {
5. _waitTime = new WaitForSeconds(WaitTime);
6. }
7. public override IEnumerator Run(Blackboard blackboard) {
8. isFinished = false;
9. yield return _waitTime;
10. isFinished = true;
11. yield return base.Run(blackboard);
12. }
13. }
Akcija MoveTo je jedna od čestih akcija u kojoj vlasniku stabla dodajemo ponašanje
dolaska na poziciju kojem dodjeljujemo traženi objekt kao ciljanu poziciju.
1. public class MoveTo: ActionBT {
2. public GameObject Target;
3. public override IEnumerator Run(Blackboard blackboard) {
4. isFinished = false;
5. GameObject target = blackboard.Get("target").Target;
6. GameObject owner = blackboard.Get("owner").Target;
7. Arrive arrive = owner.GetComponent < Arrive > ();
8. if (!arrive) arrive = owner.AddComponent < Arrive > ();
9. arrive.target = target;
10. arrive.enabled = true;
11. isFinished = true;
12. return base.Run(blackboard);
13. }
78
14. }
Zatim akcija StopMoveTo dohvaća komponentu dolaska na poziciju i deaktivira ju.
1. public class StopMoveTo: ActionBT {
2. public GameObject Target;
3. public override IEnumerator Run(Blackboard blackboard) {
4. isFinished = false;
5. GameObject owner = blackboard.Get("owner").Target;
6. Arrive arrive = owner.GetComponent < Arrive > ();
7. if (arrive) {
8. arrive.enabled = false;
9. }
10. isFinished = true;
11. return base.Run(blackboard);
12. }
13. }
Zadatak napadanja definira osnovna svojstva koja predstavljaju jačinu napada i brzinu
napadanja. Dohvaća sučelje IDamagable sa traženog objekta i oduzima objektu energiju.
1. public class DoAttack: ActionBT {
2. public GameObject Target;
3. private IDamagable _targetDamagable;
4. public float Damage;
5. public float AttackFrequency;
6. protected WaitForSeconds _waitTime;
7. void Awake() {
8. _waitTime = new WaitForSeconds(AttackFrequency);
9. }
10. public override IEnumerator Run(Blackboard blackboard) {
11. isFinished = false;
12. Target = blackboard.Get("target").Target;
13. _targetDamagable = Target.GetComponent < IDamagable > ();
14. if (_targetDamagable != null) {
15. _targetDamagable.ApplyDamage(Damage);
16. yield return _waitTime;
17. SetResult(true);
18. } else {
19. SetResult(false);
20. }
21. isFinished = true;
22. yield break;
23. }
79
24. }
Akcija liječenja je suprotna od napadanja, traženom objektu dodjeljuje pozitivnu štetu,
tako da se energija zapravo puni. Za razliku od napadanja liječenja se izvodi određeni
vremenski raspon, pošto neprijatelji koji lijeće komponente trebaju stati i liječiti, a tek nakon
isteka vremena, provjeravaju novu komponentu koju je potrebno liječiti.
1. public class DoHealing: ActionBT {
2. public DamagableEntity Target;
3. public float HealAmmount;
4. public float HealFrequency;
5. public float HealTimeBeforeCheck = 4 f;
6. private float _lastWrittenTime;
7. protected WaitForSeconds _waitTime;
8. void Awake() {
9. _waitTime = new WaitForSeconds(HealFrequency);
10. }
11. public override IEnumerator Run(Blackboard blackboard) {
12. isFinished = false;
13. Target = blackboard.Get("target").Target.GetComponent < DamagableEntity > ();
14. yield return Heal();
15. isFinished = true;
16. yield return base.Run(blackboard);
17. }
18. IEnumerator Heal() {
19. _lastWrittenTime = Time.time;
20. bool run = true;
21. while (run) {
22. Target.ApplyDamage(-1 * HealAmmount);
23. yield return _waitTime;
24. if (Time.time - _lastWrittenTime > HealTimeBeforeCheck) {
25. run = false;
26. }
27. }
28. yield break;
29. }
30. }
Akcija biranja komponente je zadatak koji dohvaća komponentu pomoću
ComponentPicker klase. Zatim unutar postojeće strukture podataka, dodaje i ažurira ključ
„target“ sa instancom objekta koji predstavlja odabranu komponentu. Naime može se uočiti da
je ključ target koji koristimo u mnogo akcija promijenjen, stoga ova akcija često prethodi
80
korištenjem dekoratora BlackboardManager koji kreira novu strukturu podataka za određeno
dijete, na taj način unutar pod stabla ključ target predstavlja komponentu računala, dok u
drugom pod stablu isti taj ključ predstavlja objekt igrača.
1. public class SelectComponent: ActionBT {
2. public ComponentPicker Picker;
3. public DamagableEntity Component;
4. public override IEnumerator Run(Blackboard blackboard) {
5. isFinished = false;
6. Component = Picker.GetBestComponent(GameManager.Instance.Components);
7. blackboard.Add("target", new Blackboard.Data("target", Component.gameObject));
8. isFinished = true;
9. return base.Run(blackboard);
10. }
11. }
Akcija naganjanja je jako slična akciji dostizanja na poziciju. Objektu dodajemo
ponašanje naganjanjem i postavimo traženi objekt.
1. public class DoPursue: ActionBT {
2. public GameObject Target;
3. public float MaxPrediction = 2 f;
4. public override IEnumerator Run(Blackboard blackboard) {
5. isFinished = false;
6. Target = blackboard.Get("target").Target;
7. GameObject owner = blackboard.Get("owner").Target;
8. Pursue pursue = owner.GetComponent < Pursue > ();
9. if (!pursue) pursue = owner.AddComponent < Pursue > ();
10. pursue.maxPrediction = MaxPrediction;
11. pursue.weight = 2;
12. pursue.enabled = true;
13. pursue.Setup(Target);
14. isFinished = true;
15. return base.Run(blackboard);
16. }
17. }
Zadatak izbjegavanja je identičan prethodnom, u ovom slučaju dodajemo komponentu
izbjegavanja.
1. public class DoEvade: ActionBT {
2. public GameObject Target;
3. public float MaxPrediction = 2 f;
4. private Evade _evade;
81
5. protected WaitForSeconds _waitTime;
6. public override IEnumerator Run(Blackboard blackboard) {
7. isFinished = false;
8. Target = blackboard.Get("target").Target;
9. GameObject owner = blackboard.Get("owner").Target;
10. _evade = owner.GetComponent < Evade > ();
11. if (!_evade) _evade = owner.AddComponent < Evade > ();
12. _evade.enabled = true;
13. _evade.maxPrediction = MaxPrediction;
14. _evade.Setup(Target);
15. yield
16. return base.Run(blackboard);
17. }
18. }
Akcija DoPlayAnimation je zapravo jednostavno rješenje korištenja postojeće
komponente EntityAnimations na modularan način. Akcija provjerava o kojem tipu animacije
se radi, te postavlja vrijednost koja može biti tipa bool ili se pak radi o okidaču.
1. public class DoPlayAnimation: ActionBT {
2. public EntityAnimations Animations;
3. public EntityAnimations.EntityAnimType AnimationType;
4. public bool UseBoolValue = false;
5. public bool UseTriggerValue = false;
6. public float floatValue = 0;
7. public bool boolValue = false;
8. public override IEnumerator Run(Blackboard blackboard) {
9. isFinished = false;
10. Animations = blackboard.Get("owner").Target.GetComponent < EntityAnimations > ();
11. if (Animations) {
12. if (AnimationType == EntityAnimations.EntityAnimType.Speed) {
13. Animations.SetAnimationMovement(floatValue);
14. } else if (AnimationType == EntityAnimations.EntityAnimType.Attack
|| AnimationType == EntityAnimations.EntityAnimType.Heal
||(AnimationType == EntityAnimations.EntityAnimType.Die && UseBoolValue)){
15. Animations.SetAnimation(AnimationType, boolValue);
16. } else if (AnimationType == EntityAnimations.EntityAnimType.Die
&& UseTriggerValue) {
17. Animations.SetAnimationTrigger(AnimationType);
18. }
19. }
20. isFinished = true;
82
21. return base.Run(blackboard);
22. }
23. }
Zadatak DoStopEvade, DoStopPursue imaju gotovo identičnu implementaciju kao
StopMoveTo, dok zadatak DoEnableBehaviour ima implementaciju sličnu kao DoPursue sa
varijablom Behavior koju jednostavno uključuje, bez postavljanja traženog objekta.
83
5.5. Formacije agenata
Implementacija formacija se zasniva na dvije osnovne klase, klasu FormationManager
koja kontrolira upravlja logikom formacije bez obzira na tip formacije i klasu FormaionPattern
koja definira oblik formacije.
5.5.1. Upravljanje formacijom
Klasa koja upravlja logikom formacije je klasa FormationManager. Njena
implementacija obuhvaća odgovornosti: dodavanje lika unutar formacije, brisanje lika iz
formacije, ažuriranje pozicije unutar formacije. Kako ova komponenta funkcionirala, potrebno
ju je koristiti zajedno sa nekim od tipova formacije, odnosno klase tipa FormationPattern.
Također klasa sadrži i pohranjene pozicije formacije unutar varijable slotAssignments.
Prije implementacije formacije implementiramo strukturu pohrane pozicija. Struktura
se sastoji od indeksa pozicije i reference na objekt lika.
1. public class SlotAssignment {
2. public int slotIndex;
3. public GameObject character;
4. public SlotAssignment() {
5. slotIndex = -1;
6. character = null;
7. }
8. }
Zatim glavna implementacija se sastoji od metoda AddCharacter u kojoj dodjeljujemo
novog lika formaciji, te RemoveCharacter koja radi suprotno. Metoda UpdateSlots je metoda
unutar koje ažuriraju pozicije svakog od objekata tako da prate oblik formacije. Metoda se
poziva iz neke klase iz više razine, poput klase koja predstavlja sami objekt formacije, ili se pak
može koristiti u nekom od stanja formacije.
1. public class FormationManager: MonoBehaviour {
2. public FormationPattern pattern;
3. private List < SlotAssignment > slotAssignments;
4. private Location driftOffset;
5. void Awake() {
6. slotAssignments = new List < SlotAssignment > ();
7. }
8. public bool AddCharacter(GameObject character) {
9. int occupiedSlots = slotAssignments.Count;
84
10. if (!pattern.SupportsSlots(occupiedSlots + 1)) return false;
11. SlotAssignment sa = new SlotAssignment();
12. sa.character = character;
13. slotAssignments.Add(sa);
14. UpdateSlotAssignmentsWithCosts();
15. return true;
16. }
17. public void RemoveCharacter(GameObject agent) {
18. int index = slotAssignments.FindIndex(x => x.character.Equals(agent));
19. slotAssignments.RemoveAt(index);
20. UpdateSlotAssignmentsWithCosts();
21. }
22. public void UpdateSlots() {
23. GameObject leader = pattern.Leader;
24. Vector3 anchor = leader.transform.position;
25. Vector3 slotPos;
26. Quaternion rotation;
27. rotation = leader.transform.rotation;
28. foreach(SlotAssignment sa in slotAssignments) {
29. Vector3 relPos;
30. slotPos = pattern.GetSlotLocation(sa.slotIndex);
31. relPos = anchor + leader.transform.TransformDirection(slotPos);
32. Location charDrift = new Location(relPos, rotation);
33. FormationEntity character = sa.character.GetComponent < FormationEntity > ();
34. character.SetTarget(charDrift);
35. }
36. }
37. }
Obje metode, za brisanje i dodavanje lika, zasnivaju se na metodi
UpdateSlotAssignmentsWithCosts. Naime, jedan od najboljih prednosti formacija je
pozicioniranje različitih igrača na određene pozicije. Npr. ukoliko želimo napraviti formaciju u
obliku linije, na samom početku formacije možemo postaviti likove koji imaju veću energiju i
koji su više otporni na udarce. Kako bi to postigli, implementiramo algoritam cijene pozicija.
Svaka pozicija u formaciji ima određenu cijenu. Cijena se može definirati na različite načine i
ovisi isključivo o samom tipu formacije.
Metoda UpdateSlotAssignmentsWithCosts implementira algoritam cijene tako da
pozicije popunja na specifičan način. Prvo se popuni lista likova i njihovih cijena za svaku
poziciju u formaciji. Zatim se liste pozicija za svakog lika sortiraju, tako da je ona pozicija sa
85
najmanjom cijenom na vrhu. Nakon toga se vrši dodjela likova samoj poziciji na način da, lika
dodjeljujemo prvoj slobodnoj poziciji prema prethodno sortiranoj listi.
Na taj način u formaciju ubacujemo agente sa najboljom mogućom raspodijelim s
obzirom na definirane cijene pojedine pozicije.
1. public int SlotsSize() {
2. return slotAssignments.Count;
3. }
4. public class CostAndSlot {
5. public int cost;
6. public int slot;
7. }
8. public class CharacterAndSlots {
9. public FormationEntity character;
10. public float assignmentEase;
11. public List < CostAndSlot > costAndSlots;
12. }
13. public List < CharacterAndSlots > charactersData;
14. public void UpdateSlotAssignmentsWithCosts() {
15. charactersData = new List < CharacterAndSlots > ();
16. // Compile the character data
17. for (int i = 0; i < slotAssignments.Count; i++) {
18. CharacterAndSlots datum = new CharacterAndSlots();
19. datum.costAndSlots = new List < CostAndSlot > ();
20. datum.character = slotAssignments[i].character.GetComponent < FormationEntity > ();
21. // Add each valid slot to it
22. for (int slot = 0; slot < pattern.NumOfSlots; slot++) {
23. // Get the cost of the slot
24. int cost = 0;
25. cost = pattern.GetSlotCost(slot, datum.character);
26. // Make sure the slot is valid
27. if (cost >= 1000) continue;
28. // Store the slot information
29. CostAndSlot slotDatum = new CostAndSlot();
30. slotDatum.slot = slot;
31. slotDatum.cost = cost;
32. datum.costAndSlots.Add(slotDatum);
33. datum.assignmentEase = 1 / (1 + cost);
34. }
35. charactersData.Add(datum);
86
36. }
37. // Keep track of which slots we have filled
38. bool[] filledSlots = new bool[pattern.NumOfSlots];
39. for (int i = 0; i < filledSlots.Length; i++) filledSlots[i] = false;
40. slotAssignments.Clear();
41. if (charactersData.Count > 1) charactersData.Sort(delegate(CharacterAndSlots c1, Charac
terAndSlots c2) {
42. return c1.assignmentEase.CompareTo(c2.assignmentEase);
43. });
44. foreach(CharacterAndSlots characterDatum in charactersData) {
45. characterDatum.costAndSlots.Sort(delegate(CostAndSlot c1, CostAndSlot c2) {
46. return c1.cost.CompareTo(c2.cost);
47. });
48. bool slotFilled = false;
49. foreach(CostAndSlot slot in characterDatum.costAndSlots) {
50. if (!filledSlots[slot.slot]) {
51. SlotAssignment assigment = new SlotAssignment();
52. assigment.character = characterDatum.character.gameObject;
53. assigment.slotIndex = slot.slot;
54. slotAssignments.Add(assigment);
55. filledSlots[slot.slot] = true;
56. slotFilled = true;
57. break;
58. }
59. }
60. }
61. driftOffset = pattern.GetDriftOffset(slotAssignments);
62. }
5.5.2. Vrste formacija
Osnovna klasa za tipove formacija je klasa FormationPattern. Prvi dio klase definira
broj pozicija, postavlja virtualne metode koje treba naslijediti konkretna formacija. Unutar
GetSlotLocation se zapravo nalazi implementacija koja definira oblik formacije.
1. public class FormationPattern: MonoBehaviour {
2. public int NumOfSlots = 3;
3. public GameObject Leader;
4. void Start() {
5. if (Leader == null) Leader = transform.gameObject;
6. }
7. public virtual Vector3 GetSlotLocation(int slotIndex) {
87
8. return Vector3.zero;
9. }
10. public bool SupportsSlots(int slotCount) {
11. return slotCount <= NumOfSlots;
12. }
13. public virtual Location GetDriftOffset(List < SlotAssignment > slotAssignments) {
14. Location location = new Location();
15. location.position = Leader.transform.position;
16. location.rotation = Leader.transform.rotation;
17. return location;
18. }
19. }
U drugom dijelu klase nalazi se implementacija cijena pozicija. Postoji niz instanci klase
CharacterTypeAndCost gdje definiramo za svaku vrstu lika cijenu za poziciju. Naime to je
jedna od jednostavnijih varijanti iskorištavanja cijena pozicija. Likove na pozicije smještamo s
obzirom na njihov tip. Druge mogućnosti mogu obuhvaćati prilagodbu cijene s obzirom na
poziciju lika, s obzirom na energiju, ili pak s obzirom na neki drugi faktor. U ovom slučaju
sasvim je dovoljna raspodjela pozicija prema tipu entiteta neprijatelja.
Implementacija se sastoji uglavnom od glavne metode u kojoj se dohvaća cijena
pozicije. Metoda Reset se odnosi na funkcionalnost same Unity komponente, te se izvršava
prilikom dodavanja komponente na neki objekt. U metodi Reset kreiramo listu svih tipova
neprijatelja i pozicije sa zadanim vrijednostima koje se mogu popuniti.
1. // Slot Cost
2. public class CharacterTypeAndCost {
3. public FormationEntity.FormationEntityType CharacterType;
4. public int[] Costs;
5. }
6. public CharacterTypeAndCost[] CharactersWithSlotCosts;
7. public int CostLimit;
8. void Reset() {
9. FormationEntity.FormationEntityType[] types = (FormationEntity.FormationEntityType[])
Enum.GetValues(typeof(FormationEntity.FormationEntityType));
10. CharactersWithSlotCosts = new CharacterTypeAndCost[types.Length];
11. for (int i = 0; i < types.Length; i++) {
12. CharactersWithSlotCosts[i] = new CharacterTypeAndCost();
13. CharactersWithSlotCosts[i].CharacterType = types[i];
14. CharactersWithSlotCosts[i].Costs = new int[NumOfSlots];
15. for (int j = 0; j < NumOfSlots; j++) {
88
16. CharactersWithSlotCosts[i].Costs[j] = 0;
17. }
18. }
19. }
20. public virtual int GetSlotCost(int slot, FormationEntity character) {
21. int cost = int.MaxValue;
22. foreach(CharacterTypeAndCost characterWithCost in CharactersWithSlotCosts) {
23. if (characterWithCost.CharacterType.Equals(character.Type)) {
24. if (characterWithCost.Costs.Length > slot)
cost = characterWithCost.Costs[slot];
25. break;
26. }
27. }
28. return cost;
29. }
30. // If Even One Slot is avaiable for this
31. public bool IsCharacterOverLimit(FormationEntity character) {
32. bool isOverLimit = true;
33. foreach(CharacterTypeAndCost characterWithCost in CharactersWithSlotCosts) {
34. if (characterWithCost.CharacterType.Equals(character.Type)) {
35. for (int i = 0; i < characterWithCost.Costs.Length; i++) {
36. if (characterWithCost.Costs[i] < CostLimit) {
37. isOverLimit = false;
38. break;
39. }
40. }
41. break;
42. }
43. }
44. return isOverLimit;
45. }
Na slici ispod je moguće vidjeti Unity komponentu u kojoj je moguće definirati cijenu
određenog neprijatelja za svaku poziciju, elementi Element 0 do Element 5 su zapravo oznake
za pozicije.
89
Slika 5-3: Komponenta kružne formacije
Nakon što imamo kostur, potrebno je samo implementirati oblik formacije. U ovom
radu, dovoljan nam je kružni tip formacije. Implementacija implementira virtualne metode
GetSlotLocation te GetDriftOffset.
1. public class DefensiveCirclePattern: FormationPattern {
2. public float CharacterRadius;
3. public override Vector3 GetSlotLocation(int slotIndex) {
4. float angleAroundCircle = 0;
5. angleAroundCircle = ((float) slotIndex / NumOfSlots) * Mathf.PI * 2;
6. float radius = CharacterRadius / Mathf.Sin(Mathf.PI / NumOfSlots);
7. Location location = new Location();
8. location.position.x = radius * Mathf.Cos(angleAroundCircle);
9. location.position.z = radius * Mathf.Sin(angleAroundCircle);
10. return location.position;
11. }
12. public override Location GetDriftOffset(List < SlotAssignment > slotAssignments) {
13. Location center = new Location();
14. for (int i = 0; i < slotAssignments.Count; i++) {
15. Vector3 location = GetSlotLocation(i);
16. center.position += location;
17. }
18. float numOfSlots = slotAssignments.Count;
19. center.position /= numOfSlots;
20. return center;
90
21. }
22. }
Jedna od dodatnih klasa koja nam služi kao pomoćna klasa za upravljanjem svim
formacijama je klasa FormationsManager (množina). Kako neprijatelje kreiramo dinamički,
potrebno je dinamički i dohvatiti formaciju za agenta. Stoga nam klasa FormationsManager
služi za definiranje tipova formacija. Zbog veličine koda, kod ispod pokazuje pseudo javnih
metoda i varijabli koje se koriste kako bi ukazale na osnovne funkcije te klase koje je
jednostavno implementirati. DefinedPatterns se sastoji od niza mogućih formacija, dok
varijabla Formations pohranjuje aktivne formacije u sceni, i redom dodjeljuje neprijatelje u
prvu slobodnu formaciju.
1. public class FormationsManager: Singleton < FormationsManager > {
2. public PatternProperties[] DefinedPatterns;
3. public List < FormationsProperties > Formations;
4. FormationManager GetFormation(PatternType formationPatternType,
FormationEntity character);
5. FormationManager GetClosestFormationManager(FormationEntity character);
6. void AddToFormation(FormationManager formation, FormationEntity character);
7. void RemoveFromFormation(FormationManager formation, FormationEntity character);
8. }
91
5.6. Slaganje neprijatelja unutar Unity alata
Kako bi složili funkcionalne neprijatelje, prvo moramo implementirati niz klasa. Klase
od kojih se sastoje neprijatelji obuhvaćaju korištenje životne energije, zatim korištenje događaja
vezanih za stanje životne energije, metode za smanjivanje energije. Također ukoliko se radi o
neprijatelju koji se dodaje u formacije, potrebno je implementirati dohvaćanje formacije,
dodavanje u formaciju. Zatim komponente računala također moraju koristiti vlastitu
implementaciju.
Klase koje su povezane za entitete u igri su:
EnemyEntity
FormationEntity
DamagableEntity
BTFlybotEntity
IDamagable
IEntity
IFormationEntity
Na slici ispod je prikazan UML dijagram navedenih klasa.
92
Slika 5-4: UML dijagram klasa neprijatelja
5.6.1. Kreiranje osnovnih sučelja
Nakon što posjedujemo sve potrebne klase za umjetnu inteligenciju, kreirat ćemo i
osnovne klase koje predstavljaju svojstva neprijatelja.
Svaki neprijatelj će implementirati sučelje IEntity. IEntity sučelje se sastoji od samo
jedne metode, metoda Die.
1. public interface IEntity {
2. void Die();
3. }
Zatim svaki neprijatelj implementira sučelje IDamagable. S tim definiramo određena
svojstva neprijatelja koja obuhvaćaju korištenje HealthProperties klase za životnu energiju,
zatim metode ApplyDamage koji utječe na promjenu te energije, i OnHealthChanged metoda
koja se poziva prilikom promjene energije. HealthProperties je klasa koja se sastoji od
maksimalne energije i trenutne energije.
1. public interface IDamagable {
2. /// <summary>
3. /// Method to Apply Damage On Entity
4. /// </summary>
5. /// <param name="damage"></param>
6. void ApplyDamage(float damage);
7. /// <summary>
8. /// Method called when entity health is changed
9. /// </summary>
10. void OnHealthChanged();
11. HealthProperties GetHealth();
12. }
Iduće sučelje koje implementiramo je sučelje za neprijatelje koji podržavaju formacije.
Sučelje se sastoji od metoda u kojima dohvaćamo formaciju za neprijatelja, zatim metoda u
kojoj dodajemo formaciji neprijatelja, zatim jedna od bitnijih metoda SetTarget koju poziva
formacija te postavlja novu lokaciju, te UpdateLocationObject metoda koja primjenjuje željenu
lokaciju na nekom praznom objektu koji služi kao traženi objekt za neprijatelje koji su u
formaciji.
93
1. public interface IFormationEntity {
2. FormationManager GetFormation();
3. void AddToFormation();
4. void RemoveFromFormation();
5. void UpdateLocationObject();
6. void SetTarget(Location location);
7. float DistanceFromLocation();
8. }
5.6.2. Kreiranje osnovne klase neprijatelja
Nakon kreiranja sučelja, potrebno ih je implementirati u konkretne klase. Prema
dijagramu klasa (vidi Slika 5-4) vrši se implementacija klasa. Zbog količine koda prikazat ćemo
implementaciju klase koja obuhvaća najviše sučelja a to je klasa EnemyEntity. Ostale klase su
jako slične, dok je klasa DamagableEntity gotovo identična.
Kreiranje samih neprijatelja u Unity alatu podrazumijeva korištenje već prethodno
definiranih animacija, modela, grafičkog sučelja i ostalih komponenti koje nisu vezani za
umjetnu inteligenciju.
EnemyEntity klasa sadrži već prethodno poznate varijable, a to je varijabla Health
vezana za životnu energiju i varijabla CanDie koja definira dali neprijatelj može umrijeti. Na
samom početku, u Start metodi koja se poziva samo jednom nakon inicijalizacije objekta te
prije uzastopnog pozivanja metode Update, vrši se dodavanje trenutnog objekta u statičku listu
neprijatelja koja je pohranjena u GameManager klasi. Ta ista lista se ažurira i neprijatelj se
briše tijekom uništenja u OnDestroy metodi, također metoda koja se automatski poziva.
EnemyEntity implementira već spomenuta sučelja koja obuhvaćaju metode Die, AppyDamage,
GetHealth, te OnHealthChanged.
1. public class EnemyEntity: MonoBehaviour, IDamagable, IEntity {
2. public HealthProperties Health;
3. public bool CanDie = true;
4. public float DestroyDelay = 2 f;
5. public CustomUnityEvent < float, float > OnHealthChange = new CustomUnityEvent < float,
float > ();
6. public HealthPercentangeEvent OnHealthChangePercentange = new HealthPercentangeEvent();
7. public CustomUnityEvent OnFullHealth = new CustomUnityEvent();
8. public CustomUnityEvent OnEmptyHealth = new CustomUnityEvent();
9. protected virtual void Awake() {
10. Health.Initialize();
94
11. }
12. protected virtual void Start() {
13. GameManager.AddEnemy(gameObject);
14. OnHealthChangePercentange.Invoke(Health.CurrentHealth / Health.MaxHealth);
15. }
16. public void ApplyDamage(float damage) {
17. Health.ApplyDamage(damage);
18. if (OnHealthChange != null) {
19. OnHealthChanged();
20. OnHealthChange.Invoke(Health.CurrentHealth, Health.MaxHealth);
21. OnHealthChangePercentange.Invoke(Health.CurrentHealth / Health.MaxHealth);
22. }
23. if (Health.IsFullHealth() && OnFullHealth != null) OnFullHealth.Invoke();
24. if (!Health.IsAlive() && OnEmptyHealth != null) OnEmptyHealth.Invoke();
25. }
26. public void Die() {
27. StartCoroutine(OnDie());
28. }
29. protected virtual IEnumerator OnDie() {
30. if (CanDie) {
31. GameManager.OnEnemyDied();
32. this.GetComponent < Collider > ().enabled = false;
33. if (this.GetComponent < EntityAnimations > ()) {
34. this.GetComponent < EntityAnimations > ().SetAnimation(EntityAnimations.Ent
ityAnimType.Die, true);
35. }
36. yield return new WaitForSeconds(DestroyDelay);
37. Destroy(gameObject);
38. }
39. yield break;
40. }
41. void OnDestroy() {
42. GameManager.RemoveEnemy(gameObject);
43. }
44. public virtual void OnHealthChanged() {}
45. public HealthProperties GetHealth() {
46. return Health;
47. }
48. }
95
5.6.3. Izgled osnovne strukture neprijatelja
Neprijatelji su strukturirani pomoću objekta igre, tzv. „GameObject“. Objekti u Unity
alatu se sastoje od niza komponenti, jedna od komponenti koje svaki GameObject posjeduje je
komponenta Transform koja predstavlja veličinu, lokaciju i rotaciju u prostoru.
Glavni objekt (roditelj) sadrži glavne komponente koje predstavljaju neprijatelja. Taj
objekt sadrži niz djece koji predstavljaju povezane objekte kako bi glavni objekt ispravno
funkcionirao. Pošto se radi o 3D modelu, uvijek će imati dvoje objekata od kojih jedan
predstavlja izgled (mesh), obično sa jednakim nazivom kao i naziv neprijatelja, a drugi
strukturu modela, često sa nazivom Armature. Ostali objekti koje sadrži glavni objekt može biti
objekt stanja automata, objekt stabla ponašanja, objekt u kojem je pohranjen vizualni izgled
životne energije (HealthCanvas), objekt sa prikazom naziva stanja (CanvasTop), te objekt koji
sadrži detektor napada ili liječenja (Attack).
Struktura neprijatelja Healerbot i Flybot je prikazana u hijerarhijskom pogledu u Unity
alatu na slici ispod.
Slika 5-5: Struktura neprijatelja Flybot Attacker i HealerBot
5.6.4. Kreiranje neprijatelja „Healerbot“
Nakon implementacije EnemyEntity klase možemo kreirati prvi tip neprijatelja, a to je
HealerBot neprijatelji koji lijeće komponente. Izgled same komponente u Unitiju je prikazan
na slici ispod. Kod pokretanja projekta trenutna životna energija se automatski postavi na
maksimalnu.
96
Slika 5-6: Komponenta Enemy Entity na Healerbot neprijatelju
Animator komponenta je komponenta iz Unity sustava pomoću koje su definirane
animacije modela. Osim te komponente, postavljene su komponente Rigidbody za fiziku te
CapsuleCollider (prikazano zelenom bojom na Slika 5-7) za kolizije.
Vizualni izgled namještenog neprijatelja je moguće vidjeti na slici ispod.
97
Slika 5-7: Neprijatelj Healerbot
Stanja Healerbot neprijatelja su definirana i prikazana na Slika 5-8. Vidi se kako je
uključeno početno stanje State Arrive To Component. ComponentPicker je komponenta koja je
već prethodno spomenuta više puta, te je zaslužna za biranje komponente u igri za liječenje.
UIState komponenta je zaslužna za upravljanje vizualnim prikazom stanja automata.
98
Slika 5-8: Stanja Healerbot neprijatelja
5.6.5. Kreiranje neprijatelja „Fighterbot“
Neprijatelj Fighterbot je kreiran na identičan način kao što je kreiran i prethodni
neprijatelj Healerbot, izgled neprijatelja je vidljiv na Slika 5-9.
99
Slika 5-9: Neprijatelj Fighterbot
Na Slika 5-10 vidimo implementirana stanja samog agenta koja su opisana u poglavlju 4.2.1.
Slika 5-10: Stanja Fighterbot neprijatelja
5.6.6. Kreiranje neprijatelja „Formation Fighterbot“
Formacije se sastoje od same formacije koja sadrži više neprijatelja, u ovom slučaju do
6 neprijatelja. Izgled objekta formacije prikazan je na Slika 5-11. Cijene pozicija su postavljene
za FighterBot neprijatelja na 0. Komponenta Character Formation Control poziva metodu
formacije UpdateSlots na osnovu koje se ažuriraju lokacije neprijatelja unutar formacije, koju
pomoću ponašanja dolaska na poziciju prate tu lokaciju.
100
Slika 5-11: Kružna formacija za Fighterbot neprijatelje
Formacija posjeduje i vlastita stanja, prethodno spomenuta u poglavlju 4.2.2. Stanja su
prikazana na Slika 5-12.
Slika 5-12: Stanja kružne formacije
101
Stanja samog neprijatelja Fighterbot unutar formacije prikazana su na Slika 5-13.
Slika 5-13: Stanja neprijatelja Fighterbot unutar kružne formacije
5.6.7. Kreiranje neprijatelja „Flybot Healer“
Flybot Healer neprijatelj je izrađen pomoću stabla ponašanja. Hijerarhija strukture je
prikazana na slici ispod.
Slika 5-14: Struktura i izgled Flybot Healer-a
Slika 5-15 prikazuje komponente na glavnom objektu neprijatelja. Entitet koji označava
neprijatelja je komponenta BTFlyBot koja sadrži referencu na BehaviourTree komponentu koja
se također nalazi na istom objektu. BehaviourTree komponenta sadrži referencu
BlackboardHolder komponente u kojoj se mogu direktno unijeti početni podaci kojima
pristupamo putem ključa.
102
Slika 5-15: Komponente vezane za stablo ponašanja
Slika 5-16 prikazuje korijen stabla ponašanja, na Slika 5-14 objekt je označen plavom
bojom. Korijen stabla je zapravo selektor koji se sastoji od troje djece. Prvo dijete je RunAway
dijete.
Slika 5-16: Korijen stabla ponašanja za Flybot Healer neprijatelja
RunAway je prikazano na Slika 5-17. Sastoji se od sekvence koja ima 4 različita djeteta.
Prvo se provjerava dali je udaljenost traženog objekta ispod 36 m, te ukoliko je, sekvenca
nastavlja na zadatak izbjegavanja, koji se isključuje nakon zadatka čekanja.
103
Slika 5-17: Zadaci pod stabla „RunAway“
Ostatak komponenti je jednostavno složen prema Slika 4-7.
5.6.8. Kreiranje neprijatelja „Flybot Attacker“
Flybot Attacker neprijatelj je također izrađen pomoću stabla ponašanja. Sastoji se od
jednakih komponenti kao i Flybot Healer. Jedina razlika je u strukturi stabla ponašanja i
njegovim zadacima što je vidljivo na Slika 5-19. Detalji stabla ponašanja prikazani su u
poglavlju 4.2.3 te na Slika 4-8.
Slika 5-18: Izgled Flybot Attacker neprijatelja
104
Slika 5-19: Struktura i korijen stabla ponašanja za neprijatelja Flybot Attacker
105
5.7. Dodatne komponente
Od dodatnih komponenti koje se koriste u projektu su:
Game Manager
Wave Spawner
Registered Events Manager
Transition To
Component Picker
On Collider Events
Shooting Ability
Bullet
UIState
CameraZoom
Entity Animations
State Holder
Komponenta Game Manager je singleton klasa koja pohranjuje glavne parametre igre i
na nju se referenciraju neka od ponašanja. Tako u navedenoj komponenti se nalazi lista svih
neprijatelja, zatim referenca na objekt igrača i lista komponenti.
Wave Spawner je komponenta zaslužna za stvaranje valova neprijatelja, unutar
komponente se može definirati broj valova, zatim broj neprijatelja u određenom valu i njihove
početne pozicije.
Registered Events Manager je singleton klasa koja ima sličnu ulogu kao i
GameManager. U njoj su pohranjeni glavni događaji poput OnMatchStart, na koje se mogu
preplatiti određeni objekti i izvršiti pozive prigodnih metoda.
On Collider Events je klasa koju koristimo na raznim mjestima. Pohranjuje više
različitih događaja među kojima su događaji OnTriggerEnter i OnTriggerExit koji se odnose
na to da je objekt koji posjeduje tu klasu, ušao unutar tzv. collidera. Collider je komponenta
koja omogućuje objektu u igri da poprimi fizičke osobine, odnosno kolizije.
ShootingAbility se koristi na objektu glavnog igrača, i omogućuje pucanje. Komponenta
omogućuje dodjelu objekta koji predstavlja metak, zatim poziciju od koje metak izlazi, efekt
pucanja, brzina metka, količina metaka, te brzina pucanja.
106
Bullet se koristi na objektu koji predstavlja metak. Klasa provjerava dali je metak ušao
u koliziju sa drugim objektom, te ukoliko je taj objekt jedan od neprijatelja, oduzima mu snagu.
UIState je komponenta koja se koristi na agentima sa stanjima. Omogućuje korištenje
grafičkog sučelja za stanje.
StateHolder je komponenta koja predstavlja objekt sa stanjima koji su implementirani
pomoću automata. Preko tog objekta druge komponente mogu jednostavno pristupiti
aktivnom stanju i jednostavno ga uključiti ili isključiti. Tu funkcionalnost iskorištavaju
formacije kako bi kontrolirale agente koji se nalaze unutar formacije.
CameraZoom je komponenta koja omogućuje zumiranje kamere.
Entity Animations omogućuje jednostavan način aktivacije određene animacije,
pozivom jedne od tri metode. Postoji metoda koja postavlja animaciju brzine hodanja, zatim
metoda koja postavlja vrijednost istinitosti za animacije liječenja, napadanja, te za animaciju
smrti.
107
6. Testiranje
U svrhe testiranja, kreirana je igra sa prethodnim implementacijama koja obuhvaća 7
različitih valova napada. Kontrole igre su jednostavne, kao što je već spomenuto, pomoću miša
se igrač okreće u željenom smjeru, dok pomoću strelica na tipkovnici ili tipki W,A,S,D se igrač
pokreće. U tablici ispod nalazi se popis neprijatelja unutar svakog vala.
Broj vala Neprijatelj Broj neprijatelja Vremenski raspon stvaranja
1 Healerbot 4 1.6
2 Healerbot
6 1 Fighterbot
3 Formation Fighterbot 6 1.2
4 Formation Fighterbot
8 1 Healerbot
5 Flybot Healer 8 1.8
6 Flybot Attacker 7 1.8
7 Flybot Healer
12 1.8 Flybot Attacker
Tablica 2: Neprijatelji kroz valove u igri
U igri su implementirani i gumbovi za ponavljanje trenutne scene, zatim gumb za izlaz,
te gumb za skrivanje i otkrivanje vizualnog prikaza stanja neprijatelja koji su kreirani pomoću
automata. Također u na samom vrhu oko sredine ekrana, postoji prikaz ukupnog i poraženog
broja neprijatelja, te brojač vala. U svrhe testiranja umjetne inteligencije, igrač ne može
umrijeti, te igra nema stanje pobjede i gubitka. Početna scena igre prikazana je na slici ispod.
108
Slika 6-1: Početna scena igre
Nakon što igrač izađe iz svog prostora, vrata se zatvaraju i počinje stvaranje neprijatelja.
Na slici ispod je moguće vidjeti prve neprijatelje, odnosno Healerbot neprijatelje sa svojim
stanjima. U samom početku stvaraju se, i započinju sa stanjem „Arrive to component“ gdje
dostižu na određenu komponentu i započinju stanje liječenja. U ovom slučaju, sve komponente
imaju punu energiju, te im nije potrebno liječenje, ali unatoč tome Healerbot je prisiljen otići
jednoj od komponenti i pokušati je liječiti.
Slika 6-2: Početak stvaranja neprijatelja
109
Na slici ispod je prikazano i stanje smrti Healerbot neprijatelja, nakon čega neprijatelj
nestaje sa mape.
Slika 6-3: Stanje smrti Healerbot neprijatelja
Nakon što su poraženi svi neprijatelji, započinje idući val neprijatelja u kojem se pojavljuje
neprijatelj Fighterbot.
Slika 6-4: Neprijatelj Fighterbot
U trećem valu se pojavljuju neprijatelji sa formacijama. Na slici ispod moguće je vidjeti izgled
neprijatelja kada proganjaju glavnog igrača.
110
Slika 6-5: Kružna formacija Fighterbot neprijatelja
Formacija ide do neprijatelja i zajednički napada tako da smanjiva radijus kada se igrač nalazi
u središtu formacije.
Slika 6-6: Opkoljavanje i napadanje glavnog igrača sa isljučenim prikazom stanja
Peti val donosi neprijatelja Flybot Healera koji osim liječenja komponente, pokušava
se udaljiti maksimalno od igrača. Na Slika 6-7 moguće je vidjeti ponašanje neprijatelja, neki
bježe od igrača, dok neki liječe komponente.
111
Slika 6-7: Ponašanje Flybot Healer neprijatelja
Posljednji neprijatelj koji se pojavljuje u šestom valu je Flybot Attacker koji napada
igrača i netom nakon toga odmah bježi. Za razliku od Fighterbot neprijatelja, znatno je brži.
Na Slika 6-8 moguće je vidjeti kako u samom početku svi Flybot Attacker neprijatelji
su počeli hvatati igrača kako bi ga napali. Slika 6-9 pak pokazuje stanje nakon toga, gdje neki
od neprijatelja pokušavaju pobjeći, tako da ih igrač ne može jednostavno uhvatiti.
Slika 6-8: Flybot Attacker neprijatelji pokušavaju uhvatiti igrača
112
Slika 6-9: Ponašanje Flybot Attacker neprijatelja
113
7. Zaključak
U ovom radu upoznali smo se sa umjetnom inteligencijom u video igrama. Agenti u
video igrama se očituju najviše u području umjetne inteligencije gdje se ponašanje određenih
entiteta u igri radi prema pristupu odozdo prema gore, odnosno po pristupu gdje entiteti
samostalno dohvaćaju podatke iz okoline bez da njima upravlja druga klasa.
Video igre su industrija koja se brzo razvija i unaprjeđuje, te umjetna inteligencija kao
dio igre igra veliku ulogu. Povećanje resursa igrama omogućuje korištenje raznih novih ili
kompleksnijih tehnika među kojima su neuronske mreže jedna od njih. Iako se tehnike
razvijaju, osnove kod izrade umjetne inteligencije nisu znatno promijenjene u posljednjih 15
godina. Čak u samom početku igre poput Pacmana su koristile automate za stanja. Igra Halo 2
2004. godine je pokazala primjenu stabla ponašanja, od tada su mnogi programeri počeli
slijediti njihov primjer. Zahvaljujući tome, trenutno, stablo ponašanja je postalo dovoljno
popularno da ga koristi velik broj igara koje posjeduju umjetnu inteligenciju.
Na samom primjeru implementacije igre može se uočiti kompleksnost izrade umjetne
inteligencije za igre. Tehnika pomoću automata je jednostavnija za implementirati u samom
početku, ali ukoliko želimo nešto različito ponašanje, obično zahtjeva programiranje novog
stanja. Stabla ponašanja su nešto kompleksnija za implementirati zbog toga što koriste velik niz
akcija i uvjeta. Jednom kada posjedujemo sve uvjete i akcije, zadatak slaganja inteligencije
može ispuniti i dizajner ili neka druga osoba koja ne mora znati programirati, pogotovo ukoliko
programer pripremi grafičko sučelje za kreiranje stabla.
Praktični dio ovog rada ukazuje na važnost umjetne inteligencije u igrama. Loša umjetna
inteligencija se lako primijeti i igra znatno gubi na vrijednosti. Dobra umjetna inteligencija
pridonosi izazovnijom igrom i nudi puno veću zabavu, stoga je jako bitno dobro isplanirati i
proučiti to područje prije same implementacije umjetne inteligencije ukoliko je ona potrebna u
igri.
114
8. Literatura
[1] Chapple, C. (2014, April). The top 16 game engines for 2014.
[2] DeLoura, M. (2001). Game Programming Gems 2. Game Programming Gems 2, 220–227.
[3] Entertainment Software Assotiation. (2013). Essential Facts About the Computer and Video
Game Industry 2013.
[4] Entertainment Software Assotiation. (2016). Essential Facts About the Computer and Video
Game Industry. Preuzeto 20.08.2016. sa http://essentialfacts.theesa.com/Essential-Facts-
2016.pdf
[5] J. Champandard, A. (2007). Top 10 Most Influential AI Games.
[6] Madhav, S. (2014). Game Programming Algorithms and Techniques. Addison Wesley.
[7] McShaffry, M., & Graham, D. (2013). Game Coding Complete. (M. Justak, Ed.) (Fourth
Edi). Course Technology PTR.
[8] Millington, I., & Funge, J. (2009). Artificial Intelligence for Games, Second Edition.
Representations. http://doi.org/10.1017/S0263574700004070
[9] Palacios, J. (2016). Unity 5.x Game AI Programming.
[10] Rabin, S. (2002). AI Game Programming Wisdom. (S. Rabin, Ed.).
[11] Reynolds, C. W. (2011). Steering Behaviors For Autonomous Characters. Preuzeto
22.08.2016. sa http://www.cs.uu.nl/docs/vakken/mcrs/papers/8.pdf
[12] Unity Technologies. (2016). Unity. Preuzeto sa http://unity3d.com/
115
9. Prilozi
U ovom poglavlju nalaze se poveznice vezane za praktični dio rada. Izvorni kod igre
kreiran je pomoću Unity 5.3.6 alata. Projekt je potrebno izvesti iz „rar“ datoteke, koji se otvori
pomoću Unity alata gdje se bira korijenska mapa. Zatim, u Unitiju je potrebno otvoriti scenu
pod nazivom „Gameplay“.
[1] Izvorni kod igre dostupan na:
https://dl.dropboxusercontent.com/u/29527409/AI/BotornotAI_source.rar
[2] Web verzija igre dostupna na:
https://dl.dropboxusercontent.com/u/29527409/AI/Prototype/index.html
[3] Windows verzija igre dostupna na:
https://dl.dropboxusercontent.com/u/29527409/AI/BotornotAI.rar