Evolutionsprogrammering för simulerat flockbeteende Som ett verktyg inom animering. PETTER LUNDAHL Kandidatexamensarbete för CSC Handledare: Vahid Mosavat Examinator: Karl Meinke och Örjan Ekeberg April 2014 iii Abstract Humans have always shown an interest in the problem solving capabilities of nature. Two fields in computer science that strongly represents that interest is flocking simulation and evolutionary computing. In this article the possibility to combine these two fields to enhance the flocking simulation is explored. The evolutionary algorithms did not invoke any new behaviors, but was very useful in calibrating the flocking behavior, which makes them a useful tool in animation. iv Sammanfattning Människan har alltid varit intresserad av naturen, och hur naturen har löst de problem som uppstått under vägen. Två fält där människan tydligt har blivit inspirerad av naturen är vid flocksimuleringar och evolutionär programmering. I den här artikeln undersöks hur man kan använda evolutionära algoritmer för att förbättra och förenkla flocksimuleringar. De evolutionära algoritmerna gav inte upphov till några nya beteenden för flocksimuleringarna men de förenklade avsevärt kalibreringen för att få önskvärda flockbeteenden. Innehåll Innehåll v 1 Inledning 1.1 Frågeställning . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.2 Avgränsningar . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 1 1 2 Bakgrund 2.1 Boids . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.2 Evolutionsprogrammering . . . . . . . . . . . . . . . . . . . . . . . . 3 3 4 3 Metod 3.1 Flocksimulering . . . . . . . . . . . . . . . 3.1.1 Grannar . . . . . . . . . . . . . . . 3.1.2 Centrering . . . . . . . . . . . . . . 3.1.3 Kollisionsundvikning . . . . . . . . 3.1.4 Hastighetsmatchning . . . . . . . . 3.1.5 Regler specifika för animering . . . 3.2 Evolutionsalgoritmer . . . . . . . . . . . . 3.2.1 Fitnessfunktion . . . . . . . . . . . 3.2.2 Variationsoperation och Urval . . . 3.3 De variabler som flocken evolverades kring . . . . . . . . . . 7 7 8 9 10 11 11 12 12 13 13 4 Resultat och diskussion 4.1 Resultat . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.2 Diskussion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15 15 18 Litteraturförteckning 19 5 Appendix .1 Källkod för flocksimulering . . . . . . . . . . . . . . . . . . . . . . . .2 Källkod för evolution . . . . . . . . . . . . . . . . . . . . . . . . . . . 21 v . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21 30 Kapitel 1 Inledning 1.1 Frågeställning Är det möjligt att förbättra Reynolds klassiska flocksimulering med hjälp av evolutionär programmering? Där en förbättring betyder att flocken upplevs som mer naturlig. Det skulle exempelvis kunna vara att individerna i flocken börjar interagera med varandra på ett nytt sätt, men också att flocken som helhet upplevs som mer naturlig. 1.2 Avgränsningar Den här rapporten berör bara Reynolds Boids[1] och enkla evolutionsalgoritmer. Den förändring som förväntas uppstå är bara tänkt att beröra animation, inte forskning på flockbeteende. För att förenkla applikationen sker flocksimuleringen i 2D istället för 3D. Eftersom jag arbetar själv på den här rapporten och eftersom vi har ont om tid så har jag valt att inte använda mig av användarstudier, utan har istället utgått från mina egna upplevelser av andvändbarhet och flockens utseende. 1 Kapitel 2 Bakgrund Det är idag vanligt att man tar inspiration ifrån naturen när man försöker simulera eller beräkna vissa problem med hjälp av datorer.[2] Två tydliga sådana inspirationer är simulerat flockbeteende och genetiska algoritmer. Simulerat flockbeteende används ofta när stora filmscener ska animeras för att animatören ska slippa animera varje individ för sig själv. Dels för att det är extremt tidskrävande, men också för att det lätt uppstår fel, t.ex. individer som rör sig igenom varandra eller inte reagerar dynamiskt på sin omgivning. Ett tidigt exempel är från Lejonkungen[3] 1994 där en stor hjord med gnuer inte animerades utan simulerades. Ytterligare ett exempel på storskaligheten man kan få till med hjälp av simulerat flockbeteende, som hade varit nästintill omöjligt utan, är de stora krigsscenerna ur Sagan om ringentrilogin där individernas beteende under stora slagen simulerades.[3] Ytterligare ett sätt att simulera naturen på är att simulera evolutionen med hjälp av genetiska algoritmer, man vill då använda sig av evolutionens metodik med reproduktion, mutation och survival of the fittest för att få fram så optimala parametrar som möjligt för sitt program.[2] Att kombinera dessa två sätt att efterlikna naturen för att få flocksimuleringen att upplevas mer naturlig fokuset för det här arbetet. 2.1 Boids Boids är ett program som Craig Reynolds beskrev i artikeln Flocks, Herds, and Schools: A Distributed Behavioral Model för SIGGRAPH 1987[1], där han beskriver ett sätt att med hjälp av några enkla regler för ett partikelsystem simulera flockbeteende. Jag kommer fortsättningsvis i texten att kalla de simulerade aktörer/partiklarna som ingår i flocksimuleringar för boids. Programmet Reynolds utvecklade försöker i så stor utsträckning som möjligt efterlikna naturen där varje flockmedlem, som i naturen till exempel är fågel eller fisk, har en egen intelligens och tar beslut ut efter vad den ser och upplever. Reynolds observerade att beteendet hos de individer som ingår i en flock kan beskrivas med tre enkla regler[4], undvika kollisioner, matcha grannars hastighet och försöka centrera flocken. För att räkna ut hastighet och riktning för varje boid så adderas helt enkelt de bidragande faktorerna från 3 4 KAPITEL 2. BAKGRUND varje regel till dess nuvarande hastighets- och riktningsvektor. Reglerna är rätt självklara, kollisioner kan enkelt undvikas genom att varje boid tar hänsyn till var dess grannar befinner sig och undviker att röra sig mot dem, hastighetsmatchning kan göras genom att en boid tar genomsnittet av dess grannars hastighetsvektorer och försöker matcha sin hastighetsvektor mot den och centrering av flocken görs genom att alla boids rör sig mot mitten av flocken. En ytterligare observation som Reynolds gjorde var att det i naturen inte verkar finnas någon övre gräns på hur stora och komplexa flockar kan bli.[1] Till exempel har sillstim på över miljoner individer observerats, och de beter sig fortfarande på samma sätt som mindre stim. Det tyder på att individerna inte har någon uppfattning om flocken utan bara om sina närmsta grannar i flocken, annars skulle beräkningskapacitet som krävdes av varje individ öka i och med att flocken växte.[1] För att efterlikna det här beteendet gav Reynolds varje boid ett synfält och lät den bara påverkas av de grannar som låg inom synfältet. Att varje boid bara påverkas av dess närmsta grannar ger också upphov till ett väldigt naturligt beteende när till exempel statiska hinder introduceras, det gör det möjligt för flocken att delas upp i flera mindre flockar som inte påverkar varandra förrän de kommer tillräckligt nära för att se varandra och då smälta samman. Ett beteende som också observerats i naturen. 2.2 Evolutionsprogrammering Evolutionsprogrammering är en gren av datalogi som försöker utnyttja Darwin evolutionsteori för att lösa diverse tekniska och datalogiska problem. Detta görs med en enkel modell av evolution som baseras på naturligt urval. De sex viktigaste delarna för en evolutionär algoritm är enligt boken Introduktion to Evolutionary Computing[2] av Eiben och Smith, representation, fitnessfunktion, population, föräldraurvals mekanik, variationsoperatorer och urvalsprocess. Representation står för hur man väljer att beskriva de problemlösningsinstanser som man sedan ska utföra det naturliga urvalet på. Inom evolutionsprogrammering finns det två vanliga datastrukturer att representera dessa objekt på, antingen som listor eller som träd. Valet av datastruktur leder till olika algoritmer för framförallt parning, mutation, etc. Fitnessfunktionen är den algoritm som beräknar hur välanpassade en viss individ, dvs lösningsinstans, är till att lösa problemet man arbetar med. Detta görs genom att man ställer upp diverse regler och tester för att få ett fitnessvärde för den givna individen.[2] Populationens roll är att hålla reda på alla individer som just nu finns tillgängliga för vidare evolution. Det är inte individerna som ändras vid evolutionen utan vilka individer som ingår i den nuvarande populationen. Det är från populationen man väljer vilka individer som ska användas vid till exempel parning. Ofta är populationen väldigt enkel att implementera, en lista av de individer som ingår är ofta fullt tillräckligt.[2] 2.2. EVOLUTIONSPROGRAMMERING 5 Urvalsmekaniken som används för att välja ut de individer som får bygga upp nästa generation är i nästan alla fall probabilistisk när det kommer till evolutionsprogrammering. Vilket betyder att de individer som har ett högt fitnessvärde är de individer som med störst sannolikhet kommer att användas som fäder för att skapa nästa generation, men alla individer har en nollskild sannolikhet att bli fäder. Anledningen till att man låter alla individer ha en chans att bli fäder och inte bara de som just nu är bäst lämpade är för att man då riskerar att fastna i lokala maximum.[2] Variationsoperatorer är de algoritmer som används för att givet någon/några fäder skapa en ny individ. Dessa algoritmer delas in i två grupper, mutation och rekombination. Mutation tar en endast en fader och skapar en ny individ genom att slumpmässigt ändra på några eller alla av dess värden. Rekombination är det som i djur-och växtvärden kallas fortplantning, då väljer man ut två (eller flera) individer och skapar en ny individ genom att kombinera ihop deras olika värden.[2] Urvalsprocessen är den process som väljer ut vilka individer som ska överleva till nästa generation, den är ofta väldigt lik urvalsmekaniken, man rankar individerna efter deras fitnessvärde, men är oftast deterministisk och inte stokastisk. Man kan till exempel välja ut den övre halvan av de individer som finns tillgängliga efter variationssteget.[2] Reglerna för att initialisera och avsluta en evolutionär algoritm hålls ofta enkla[2], den första populationen kan till exempel skapas genom att man helt enkelt slumpar fram alla individer. Och den evolutionära algoritmen kan avslutat efter ett visst antal generationer, eller då ett visst fitnessvärde har uppnåtts. Kapitel 3 Metod 3.1 Flocksimulering För att göra min flocksimulering så enkel och ren som möjligt la jag mig väldigt nära Reynolds implementering men i 2D istället för i 3D. Populationen bestod av en lista av boids (så som de är beskrivna i avsnitt 2.1). Varje boid är ett objekt som har en implementation av varje av de tre reglerna som Reynolds ställde upp, en position i rummet, en hastighetsvektor och ett synfält. 7 8 3.1.1 KAPITEL 3. METOD Grannar Grannar till en boid definieras som de individer som ligger inom boidens synfält. Synfältet approximeras av en sfär eller i 2 dimensioner en cirkel, så allt som ligger innanför sfären ligger i boidens synfält. För att få ett mer realistiskt och bättre resultat framförallt när det gäller statiska objekt, till exempel ifall en flock delas upp av en vägg, kan man implementera mer avancerade algoritmer för beräkning av synfältet. Man skulle till exempel kunna använda sig av en enkel ray tracingalgoritm, men för syftet med den här rapporten anses det fullt tillräckligt med en naiv algoritm för synfältet. Jag valde att implementera synfältet som en enkel cirkel centrerad kring boiden. I figur 3.1 ser vi hur den förenklade modellen för boidens synfält fungerar. Alla boids som är innom den aktuella individens synfält, dvs innanför cirkeln, är grannar till den aktuella individen. I det här fallet är det de mellangrå boidsen som är grannar till den mörkgrå. Figur 3.1. Grannar 3.1. FLOCKSIMULERING 3.1.2 9 Centrering För att centrera flocken så använder sig varje individ sig av ett upplevt flockcentrum. Metoden för att beräkna det upplevda flockcentra beräknas som medelvärdet av alla grannars position, vilket ger en punkt i rymden. För att beräkna riktningen individen skall röra sig åt för att centrera flocken används riktningen för vektorn som bildas mellan individens nuvarande position och dess upplevda centrum. Längden på kraftvektorn som används för att beräkna hur boiden skall förflytta sig är en fraktion av längden till det upplevda flockcentra. I grundutförandet valde jag att den skulle vara en tiondel av vektorns längd. Längden på kraftvektorn måste vara mindre än längden till centrum för att individen inte ska överkompensera i sin förflyttning. Att kraftvektorn är relaterad till avståndet och riktningen från det upplevda centra leder till att individer som ligger på randen av flocken dras mot mitten av flocken medan individer som redan befinner sig inne i flocken inte påverkas lika kraftigt eller alls av centreringen. I figur 3.2 beskriver de grå pilarna varje grannes inverkan på det upplevda flockcentrat och den svarta pilen är vektorn mellan den aktuella individen och dess upplevda flockcentrum. Den aktuella individen kommer alltså att påverkas av en kraftvektor som är en tiondel av den svarta pilen. Figur 3.2. Centrering 10 3.1.3 KAPITEL 3. METOD Kollisionsundvikning På samma sätt som för centrering tar jag för varje individ fram dess grannar, för varje granne beräknas sedan en kraftvektor med riktning direkt motsatt riktningen från den aktuella individen till den aktuella grannen. Storleken på kraftvektorn växer när avståndet krymper mellan individen och grannen, det vill säga de närmsta grannarna har störst inverkan på individen. Medelvärdet av alla kraftvektorer som bildas mellan individen och grannarna är det som sedan påverkar hur individen kommer att förflytta sig. För att ytterligare undvika kollisioner så varierar kraftvektorn omvänt proportionellt med avståndet, vilket betyder att kraftvektorn växer snabbare ju mindre avståndet är. I figur 3.3 beskriver de grå pilarna varje grannes inverkan på kraftvektorn, och den svarta pilen är den slutgiltiga kraftvektorn som påverkar den aktuella individen. Figur 3.3. Kollisionsundvikning 3.1. FLOCKSIMULERING 3.1.4 11 Hastighetsmatchning På samma sätt som för centrering skapas en upplevd flockhastighetsvektor för varje individ genom att jag går igenom alla grannars hastighetsvektorer och med hjälp av medelvärdet skapar en upplevd flockhastighet. Skillnaden mellan individens egen hastighet och flockens hastighet blir kraftvektorn som returneras för att styra individens hastighet. Hastighetsmatchning leder både till flocksammanhållning och till mindre kollisioner inom flocken, eftersom ifall alla individer har samma hastighet och riktning så splittras inte flocken och det att sannolikheten för kollisioner minskar. I figur 3.4 ser vi varje individs hastighet som ljusgrå pilar, den mörkgrå linjen är den upplevda flockhastigheten och den svarta pilen är kraftvektorn som påverkar den aktuella individen. Figur 3.4. Hastighetsmatchning 3.1.5 Regler specifika för animering Eftersom man vid animering ofta vill styra flockens rörelse kan man lägga till ytterligare regler som inte förändrar flockdynamiken men som styr flocken i sin helhet. Det kan till exempel göras genom att man lägger till en global kraftvektor som påverkar alla individer lika mycket, man kan se det som en ström eller stark vind, så flocken dras åt ett håll. Ifall man vill att den simulerade flocken skall hålla sig inom något område kan man lägga till en regel som ger varje individ som börjar närma sig kanten till det området eller ligger utanför området en kraft mot mitten 12 KAPITEL 3. METOD av området. Det kommer leda till att när tillräckligt många individer ur flocken har hamnat utanför gränsen kommer det finnas en nettokraft på hela flocken som styr flocken tillbaka in mot området igen. Den typen av svaga randvillkor leder till ett mjukt och realistiskt beteende hos flocken. Man skulle också kunna tänka sig att kombinera dessa två och ha en kraftvektor som varierar i rummet, på så sätt kan man skapa en bana flocken följer, men utan att man behöver styra varje individs beteende. I min implementation har jag lagt till regeln som håller boidsen innom ett visst område, som i mitt fall är lika stort som skärmen de visas på. 3.2 Evolutionsalgoritmer För att evolvera gruppen av flockinstanser så körs en Boidsimulering för varje individ i populationen, efter det beräknas fitnessvärdet för varje individ och de som väljs ut i urvalsprocessen förökar sig för att bygga upp nästa generation. Fitnessfunktionen och urvalet kommer gås i genom i detalj senare i det här kapitlet. Oservera att en individ i det här fallet är en hel flocksimulering, det som menas med en individ är alltså en hel flock, inte en individuell boid. I texten skiljs dessa åt genom att benämnas som individ respektive boid. För att simuleringen skall vara rättvis men samtidigt ge upphov till olika instanser av flockbeteende så ges varje Boid en slumpmässig position innan simuleringen körs, sedan körs simuleringen i tusen steg och resultatet bedöms av fitnessfunktionen. Populationen består av 100 individer, som vid simuleringens start initieras med slumpmässiga värden. För att dessa värden skall vara relevanta är de normerade kring de värden den klassiska boidsimuleringen har. Det vill säga i det fallet där styrkan av de olika reglerna skall bestämmas så får de ett slumpmässigt värde som ligger i området 1.00 ± 1. Evolutionen körs i tio generationer, och den individ med bästa fitnessvärde efter den tionde generationen returneras som lösning till instansen. Anledningen till att tio generationer valdes var för att populationens evolution avstannade oftast innan tio generationer hade körts. 3.2.1 Fitnessfunktion Fitnessfunktionen som används är fokuserad på tre olika områden, för det första så ger krockar upphov till ett lägre fitnessvärde, för det andra belönas instanser för varje individ som syns på skärmen och för det tredje belönas instanser som har lagom stora flockar där vad som är lagom är upp till animatören. En krock definieras som att ifall en boid är närmre en något epsilon till en annan boid så har de krockat, här kan man som animatör både styra hur mycket en krock skall straffas och värdet på epsilon. Skärmen definieras som beskrivet i avsnittet Regler specifika för animering, här har animatören möjlighet att styra hur stor skärmen skall vara och hur mycket en instans skall belönas för varje boid som syns på skärmen. För att animatören skall få möjlighet att styra hur stora flockar som skall premieras så ges det möjlighet att styra hur många grannar varje boid skall ha för att ha en positiv inverkan på fitnessvärdet. Det ges möjlighet att styra både en undre och en övre 3.3. DE VARIABLER SOM FLOCKEN EVOLVERADES KRING 13 gräns för hur många grannar en boid skall ha, och även hur stort avståndet skall vara för att de skall räknas som grannar. Självklart går det även att styra hur stor inverkan detta skall ha på fitnessvärdet. Det totala fitnessvärdet för en flockinstans är de ingående boidsens sammanlagda fitnessvärde. 3.2.2 Variationsoperation och Urval För att ta fram nya individer till urvalet används tre olika metoder, den första är kombination av individer från nuvarande population, den andra är mutationer hos individer i den nuvarande populationen och den tredje är introducering av helt nya individer. För att kombinera individer, det som i naturen kallas fortplantning, tar man två individer slumpmässigt från den nuvarande populationen och kombinerar dessa genom att för varje egenskap de har slumpmässigt välja en av de två värdena som finns för varje egenskap. För att mutera nuvarande individer så görs samma sak som för att kombinera individer, men istället för att välja mellan två individers egenskaper så är urvalet antingen den nuvarande individens värde eller ett slumpvärde. Introducering av nya individer görs genom att ge dem slumpvärden för alla egenskaper. Vilka individer som väljs ut för att muteras eller paras ihop är slumpmässigt, sannolikheten att bli vald till reproduktion är normalfördelat kring den individ med högst fitnessvärde. När alla nya individer har tagits fram så kombineras de med den nuvarande populationens bästa 10%, för att populationen inte skall växa med varje generation så är det bara de med högst fitnessvärde som sparas, och antalet individer i populationen hålls konstant. Det som avses med individer i populationen är olika instanser av flocksimuleringar, alltså inte antalet boids i en flocksimulering. Antalet boids i varje flocksimulering varierar när boids dör på grund av krockar. 3.3 De variabler som flocken evolverades kring I figur 3.5 kan ni se de värden som förändrades under evolutionen, de värden ni ser till höger är de värden som användes för flocksimuleringen utan evolution. viewRange är den variabel som styr hur stor cirkeln som beskriver synfältet är. personalSpace är också radien för en cirkel, i det här fallet är det bara de grannar som är innanför den cirkeln som påverkar kollisionsundvikningsbeteendet. change är en variabel för att styra hur snabbt en boid ändrar sin hastighetsvektor och ownWill är en variable för att styra hur benägen en boid är att matcha sin hastighet med dess grannars hastighet. gather, avoid och match hör till reglerna centrering, kollisionsundvikning och hastighetsmatchning, där värdet styr hur stor påverkan de olika reglerna har på boiden. exit är en variabel för regeln att boidsen skall stanna innom ett visst område. 14 KAPITEL 3. METOD viewRange personalSpace change ownWill gather avoid match exit 100 50 100 10 1 1 1 1 Figur 3.5. Variabler för evolution Kapitel 4 Resultat och diskussion 4.1 Resultat Efter tio generationers evolution nåddes i nästan varje försök ett stabilt optimum, vilket betyder att sannolikheten att fler evolutionssteg skulle ge ett bättre resultat är väldigt låg. Det gick inte att se något nytt beteende hos flocken, det var inte heller något av de tidigare beteendena som försvann. Trots att inga nya beteenden introducerades så rörde sig flocken mer naturligt och man kunde se en klar förbättring i kvalitén på animationen. I figur 4.1 kan vi se hur flocken såg ut med standardvärdena som kan ses i figur 4.2. Vi kan se att flocken ser väldigt organiserad ut, alla avstånd mellan boids är nästan identiska och riktningarna innom delflockarna är också väldigt uniforma. I figur 4.3 som är en bild av hur flocken såg ut efter tio generationers evolution uppvisar flocken en större variation i både avstånd och riktning innom delflockarna. Så trots att det inte finns några nya beteenden i flocken efter evolutionen så anser jag att flocken ser naturligare ut efter evolutionen. De värden som anvädes för figur 4.3 kan ses i 4.4, man kan se att de har ett lägre värde för personalSpace än standard, vilket låter dem komma närmre varandra. Båda värdena för avoid och gather är nere på en fjärdedel av standard och match är större än standard. Vilket leder till att de är mindre benägna att sammlas i flockar, och mindre benägna att undvika andra boids, men att de är mycket benägna att matcha sin hastighet med grannarnas hastighet. De har också ett nästan dubbelt så stort värde på viewRange, vilket gör att de myckte tidigare kommer se andra boids och dras mot dem. 15 16 KAPITEL 4. RESULTAT OCH DISKUSSION Figur 4.1. Flockbeteende innan evolution public double viewRange public double personalSpace public double ownWill public double gather public double avoid public double match public double exit public double change 100 50 10 1 1 1 1 100 Figur 4.2. Variabler innan evolution 4.1. RESULTAT 17 Figur 4.3. Flockbeteende efter evolution public double viewRange public double personalSpace public double ownWill public double gather public double avoid public double match public double exit public double change 182.34784168267228 29.661291668942212 17.511015855979846 0.26091639900431907 0.2530519661978641 1.4020290081905358 0.7335319605669874 196.43149293116718 Figur 4.4. Variabler efter evolution 18 4.2 KAPITEL 4. RESULTAT OCH DISKUSSION Diskussion Jag tycker att man kan se en förbättring i utseendet från figur 4.1 till figur 4.3, som jag beskrev det i resultatdelen ser figur 4.1 mer ordnat och mindre naturlig ut, medan figur 4.3 ser mer naturlig ut. När man tittar på siffrorna i figur 4.2 och 4.4 så är det ganska svårt att gissa sig till hur de båda olika flockarna skulle bete sig. Man skulle till exempel kunna tro att flocken i figur 4.3 inte skulle samlas så tydligt eftersom värdet på variabeln gather, som styr centreringen, är mycket lägre än för figur 4.1. Så jag anser att det är svårt att bara genom att välja värden till flocksimuleringen få ett önskat beteende. Anledningen till att inga nya beteenden introducerades är antagligen eftersom det inte sker någon förändring i algoritmen för flockbeteende, det är endast skalära värden som ändras. För att få ett ändrat flockbeteende tror jag att man förutom att evolvera fram hur stor påverkan de olika reglerna för flockbeteende skall ha i körningen, så behöver man också evolvera med avseende på hur reglerna skall implementeras. Ett exempel på det skulle kunna vara att man evolverar vilken typ av avstånd som skall användas i de olika reglerna, ger till exempel kvadratisk avståndsbedömning ett bättre resultat än linjär, osv. Man kan också tänka sig att istället för att som nu bara evolvera hur stor radien för synfältet skall vara så kan man tänka sig att man låter synfältets form variera. Den slutsats jag anser att man kan komma till är att den här typen av enkla evolutionsalgoritmer inte kan ge något förändrat eller förbättrat resultat, men att de fortfarande kan användas som ett kraftfullt verktyg för att styra utseendet på en simulerad flock. Till exempel inom animation, där man vill använda sig av flocksimuleringar, kan det vara enklare att istället för att försöka styra simuleringen genom att gå in och manuellt ändra de olika styrkorna för reglerna för flocksimulering så kan man istället beskriva det önskvärda beteendet och evolutionera fram styrkorna. Anledningen till att jag anser att det är enklare att beskriva det önskvärda beteendet istället för att försöka ställa in det manuellt är att trots att reglerna är väldigt enkla så samverkar de på ett relativt komplicerat sätt. Det är inte direkt uppenbart vad man ska göra för att få en tätare flock, för det första försöket att stärka regeln för flocksammanhållning kan leda till icke önskvärda beteenden, till exempel krockar eller underliga beteenden då flocken förväntas dela upp sig på grund av statiska hinder. Så genom att istället använda sig av evolutionsalgoritmer blir det enklare att formulera vilken typ av beteende man vill förstärka, men utan att förändra andra beteenden som anses positiva. Litteraturförteckning [1] Craig W Reynolds. Flocks, herds and schools: A distributed behavioral model. In ACM SIGGRAPH Computer Graphics, volume 21, pages 25–34. ACM, 1987. [2] J.E. Smith A.E. Eiben. Introduction to Evolutionary Computing. Springer, 2003. [3] Marcus Beausang Martin Nygren. Flockbeteende och animering. Technical report, Kungliga Tekniska Högskolan, 2004. [4] Johanna Neander. Animerat flockbeteende. Technical report, Linköpings universitet, 2009. 19 Kapitel 5 Appendix .1 Källkod för flocksimulering //MainClass.java package flockSim; import javax.swing.*; public class MainClass{ public static void main(String[] args) { JFrame f = new JFrame("Title"); f.setResizable(false); f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); Display d = new Display(); f.add(d); f.setSize(Settings.WIDTH,Settings.HEIGHT); f.setVisible(true); Boid[] myBoids = new Boid[100]; d.myBoids = myBoids; Stats s = new Stats(); s.print(); for(int i = 0; i < myBoids.length; i++) { myBoids[i] = new Boid( (int)(Math.random()*Settings.WIDTH), (int)(Math.random()*Settings.HEIGHT), Math.random()*Math.PI*2, s); } while(true) { 21 22 KAPITEL 5. APPENDIX for(Boid b : d.myBoids) { b.move(myBoids); } try { Thread.sleep(100); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } //Boid.java package flockSim; import java.awt.Color; public class Boid { public int X; public int Y; public double rotation; public double[] speed = {0,0}; public Color color; public Stats stats; public Boid(int x, int y, double rot, Stats s) { X = x; Y = y; rotation = rot; stats = s; double tmp = Math.random()*3; double radianer = rotation; speed[0] = Math.cos(radianer) * tmp; speed[1] = Math.sin(radianer) * tmp; color = new Color((float)Math.random(),(float)Math.random(),(float)Math.random()); } private void scalarMul(double[] in , double val) { in[0] *= val; in[1] *= val; return; } .1. KÄLLKOD FÖR FLOCKSIMULERING public void move(Boid[] { double[] vector1 double[] vector2 double[] vector3 double[] vector4 myBoids) = = = = {0,0}; {0,0}; {0,0}; {0,0}; vector1 = ruleGather (myBoids); vector2 = ruleAvoid (myBoids); vector3 = ruleMatch (myBoids); vector4 = dontExit(); scalarMul(vector1, stats.gather); scalarMul(vector2, stats.avoid); scalarMul(vector3, stats.match); scalarMul(vector4, stats.exit); double[] change = new double[2]; change[0] = vector1[0] + vector2[0] + vector3[0] + vector4[0]; change[1] = vector1[1] + vector2[1] + vector3[1] + vector4[1]; if(delta(change)>stats.change) { double diff = delta(change)/stats.change; change[0] = change[0]/diff; change[1] = change[1]/diff; } speed[0] += change[0]; speed[1] += change[1]; if(delta(speed)>5) { double diff = delta(speed)/5; speed[0] = speed[0]/diff; speed[1] = speed[1]/diff; } X = X + (int)(speed[0]); Y = Y + (int)(speed[1]); rotation = Math.atan2(speed[1], speed[0]); } public double[] ruleGather(Boid[] myBoids) { double[] result = {0,0}; double[] center = {0,0}; int neighbours = 0; 23 24 KAPITEL 5. APPENDIX for(Boid b : myBoids) { double[] diff = {X-b.X , Y-b.Y}; if(delta(diff)<stats.viewRange) { center[0] += b.X; center[1] += b.Y; neighbours++; } } center[0] = center[0]/neighbours; center[1] = center[1]/neighbours; result[0] = (center[0] - this.X)/100; result[1] = (center[1] - this.Y)/100; return result; } public double[] ruleAvoid(Boid[] myBoids) { double[] result = {0,0}; int neighbours = 0; for(int i = 0; i < myBoids.length; i++) { if(myBoids[i] != this) { double[] tmp = new double[2]; tmp[0] = myBoids[i].X - this.X; tmp[1] = myBoids[i].Y - this.Y; if(delta(tmp)<stats.viewRange) if(delta(tmp)<stats.personalSpace) { if(tmp[0] != 0) result[0] = result[0] stats.personalSpace/tmp[0]; if(tmp[1] != 0) result[1] = result[1] stats.personalSpace/tmp[1]; neighbours++; } } } if(neighbours>0) { result[0] = result[0]/neighbours; result[1] = result[1]/neighbours; .1. KÄLLKOD FÖR FLOCKSIMULERING } return result; } public double[] ruleMatch(Boid[] myBoids) { double[] result = {0,0}; int neighbours = 0; for(Boid b : myBoids) { double[] diff = {X-b.X , Y-b.Y}; if(this != b && delta(diff)<stats.viewRange) { result[0] += b.speed[0]; result[1] += b.speed[1]; neighbours++; } } if(neighbours == 0) return result; result[0] = result[0]/neighbours; result[1] = result[1]/neighbours; result[0] = (result[0] - this.speed[0])/stats.ownWill; result[1] = (result[1] - this.speed[1])/stats.ownWill; return result; } public double[] dontExit() { double[] result = {0,0}; if(X < 0) result[0] = -X; else if(X > Settings.WIDTH+10) result[0] = Settings.WIDTH-X; if(Y < 0) result[1] = -Y; else if(Y > Settings.HEIGHT+10) result[1] = Settings.HEIGHT-Y; //System.out.println("ruleEXIT: x" + result[0] + " y" +result[1]); return result; } private double delta(double[] vect) 25 26 KAPITEL 5. APPENDIX { return Math.sqrt(vect[0]*vect[0] + vect[1]*vect[1]); } } //Stats.java package flockSim; public class Stats { public public public public public public public public double double double double double double double double viewRange = 100; personalSpace = 50; ownWill = 10; gather = 1; avoid = 1; match = 1; exit = 1; change = 100; /*public double viewRange = 182.34784168267228; public double personalSpace = 29.661291668942212; public double ownWill = 17.511015855979846; public double gather = 0.26091639900431907; public double avoid = 0.2530519661978641; public double match = 1.4020290081905358; public double exit = 0.7335319605669874; public double change = 196.43149293116718; */ public void random() { viewRange = 2*Math.random()*viewRange; personalSpace = 2*Math.random()*personalSpace; ownWill = 2*Math.random()*ownWill; gather = 2*Math.random()*gather; avoid = 2*Math.random()*avoid; match = 2*Math.random()*match; exit = 2*Math.random()*exit; change = 2*Math.random()*change; } public void print() { System.out.println("viewRange:" + viewRange); System.out.println("personalSpace:" + personalSpace); System.out.println("ownWill:" + ownWill); .1. KÄLLKOD FÖR FLOCKSIMULERING System.out.println("gather:" + gather); System.out.println("avoid:" + avoid); System.out.println("match:" + match); System.out.println("exit:" + exit); System.out.println("change:" + change); } } //Display.java package flockSim; import java.awt.*; import javax.swing.*; public class Display extends JPanel { Color c = Color.BLACK; Boid[] myBoids; public Display() { this.setBackground(c); } public void paintComponent(Graphics g) { super.paintComponent(g); paintBoids(g); } public void paintBoids(Graphics g) { //super.paintComponent(g); //repaint(); g.setColor(Color.CYAN); for(Boid b : myBoids) { g.setColor(Color.WHITE); Shape p = new Shape(b); g.drawPolygon(p); //g.drawOval(b.X-5, b.Y-5, 10, 10); } repaint(); } 27 28 KAPITEL 5. APPENDIX /* * Generated automaticly */ private static final long serialVersionUID = 759338174544585169L; } //Shape.java package flockSim; import java.awt.Polygon; public class Shape extends Polygon{ double length = 15; double middleDist = Math.sqrt(length*length (length/2)*(length/2)) / 2; public Shape(int x, int y, double rotation) { double radianer = rotation; double cosAngle = Math.cos(radianer); double sinAngle = Math.sin(radianer); //Right point int ptX = x + (int)(middleDist*cosAngle); int ptY = y + (int)(middleDist*sinAngle); this.addPoint(ptX, ptY); //Bottom point ptX = x + (int)(-middleDist*cosAngle - (length/2)*sinAngle ptY = y + (int)(-middleDist*sinAngle + (length/2)*cosAngle this.addPoint(ptX, ptY); //Middle point ptX = x - (int)((middleDist/2)*cosAngle); ptY = y - (int)((middleDist/2)*sinAngle); this.addPoint(ptX, ptY); //Top point ptX = x + (int)(-middleDist*cosAngle + (length/2)*sinAngle ptY = y + (int)(-middleDist*sinAngle - (length/2)*cosAngle this.addPoint(ptX, ptY); } public Shape(Boid b) { this(b.X,b.Y,b.rotation); } private static final long serialVersionUID = 5429989775653640585L; } ); ); ); ); .1. KÄLLKOD FÖR FLOCKSIMULERING //Settings.java package flockSim; public class Settings { public static int WIDTH = 1200; public static int HEIGHT = 800; } 29 30 KAPITEL 5. APPENDIX .2 Källkod för evolution //MainClass.java //For evolution computing package flockSim; import java.util.Arrays; public class MainClass{ public static void main(String[] args) { Boid[] myBoids = new Boid[100]; Stats s = new Stats(); for(int i = 0; i < myBoids.length; i++) { myBoids[i] = new Boid( (int)(Math.random()*Settings.WIDTH), (int)(Math.random()*Settings.HEIGHT), Math.random()*Math.PI*2, s); } Stats[] population = new Stats[100]; for(int i = 0; i < population.length; i++) { population[i] = new Stats(); population[i].random(); } for(Stats stat : population) fitness(stat, myBoids); Arrays.sort(population); population[0].print(); for(int i = 0; i < 10; i++) { System.out.println("Generation:" + i); System.out.println(population[0].fitness); Stats[] offspring = mate(population); for(Stats stat : offspring) fitness(stat, myBoids); Arrays.sort(offspring); Stats[] all = new Stats[110]; for(int j = 0; j < 100; j++) all[j] = offspring[j]; for(int j = 0; j < 10; j++) all[j+100] = population[j]; .2. KÄLLKOD FÖR EVOLUTION 31 Arrays.sort(all); population = Arrays.copyOfRange(all, 0, 100); } for(Stats stat : population) System.out.println(stat.fitness); population[0].print(); } public static int roll() { int res = 0; res = (int)((Math.random()+Math.random()+Math.random()+Math.random()+Math.random()+ if(res > 100) return res-101; return 100-res; } public static Stats[] mate(Stats[] parents) { Stats[] offspring = new Stats[100]; //Mating for(int i = 0; i < 5; i++) { double[] mom = parents[0].toArray(); double[] dad = parents[1].toArray(); for(int j = 0; j < mom.length; j++) { if(Math.random()>0.5) mom[j] = dad[j]; } Stats s = new Stats(mom); offspring[i] = s; } for(int i = 5; i < 50; i++) { double[] mom = parents[roll()].toArray(); double[] dad = parents[roll()].toArray(); for(int j = 0; j < mom.length; j++) { if(Math.random()>0.5) mom[j] = dad[j]; } Stats s = new Stats(mom); offspring[i] = s; } 32 KAPITEL 5. APPENDIX //Mutations for(int i = 0; i < 5; i++) { double[] mom = parents[i/2].toArray(); for(int j = 0; j < mom.length; j++) { if(Math.random()>0.5) mom[j] = mom[j]*2*Math.random(); if(Math.random()>0.7) mom[j] += 100*Math.random(); } Stats s = new Stats(mom); offspring[50+i] = s; } for(int i = 5; i < 25; i++) { double[] mom = parents[roll()].toArray(); for(int j = 0; j < mom.length; j++) { if(Math.random()>0.5) mom[j] = mom[j]*2*Math.random(); } Stats s = new Stats(mom); offspring[50+i] = s; } //Totally random for(int i = 0; i < 25; i++) { Stats s = new Stats(); s.random(); offspring[75+i] = s; } return offspring; } public static void fitness(Stats s, Boid[] myBoids) { double score = 0; for(Boid b : myBoids) { b.alive = true; b.stats = s; b.X = (int)(Math.random()*Settings.WIDTH); b.Y = (int)(Math.random()*Settings.HEIGHT); .2. KÄLLKOD FÖR EVOLUTION b.rotation double tmp b.speed[0] b.speed[1] 33 = = = = Math.random()*Math.PI*2; Math.random()*3; Math.cos(b.rotation) * tmp; Math.sin(b.rotation) * tmp; } for(int i = 0; i < 100; i++) { for(Boid b : myBoids) { if(b.alive) { b.move(myBoids); if(b.death(myBoids)) { b.X = -10000; b.Y = -10000; score -= 100; } } } } for(Boid b : myBoids) { int group = 0; if(b.alive) { score += 0.01*b.delta(b.speed); if(b.X<Settings.WIDTH && b.X>0) score += 0.25; if(b.Y<Settings.HEIGHT && b.Y>0) score += 0.25; for(Boid b2 : myBoids) { if(b2.alive) { if(b.delta(b, b2)<100) group++; } } if(group>20) score -= group*0.01; else score += group*0.1; } } s.fitness = score; } 34 KAPITEL 5. APPENDIX } //Boid.java package flockSim; import java.awt.Color; public class Boid { public boolean alive = true; public int X; public int Y; public double rotation; public double[] speed = {0,0}; public Color color; public Stats stats; public Boid(int x, int y, double rot, Stats s) { X = x; Y = y; rotation = rot; stats = s; double tmp = Math.random()*3; double radianer = rotation;//(rotation/180)*Math.PI; speed[0] = Math.cos(radianer) * tmp; speed[1] = Math.sin(radianer) * tmp; color = new Color((float)Math.random(),(float)Math.random(),(float)Math.random()); } private void scalarMul(double[] in , double val) { in[0] *= val; in[1] *= val; return; } public void move(Boid[] { double[] vector1 double[] vector2 double[] vector3 double[] vector4 vector1 vector2 vector3 vector4 = = = = myBoids) = = = = {0,0};//new double[2]; {0,0};//new double[2]; {0,0};//new double[2]; {0,0}; ruleGather (myBoids); ruleAvoid (myBoids); ruleMatch (myBoids); dontExit(); .2. KÄLLKOD FÖR EVOLUTION scalarMul(vector1, scalarMul(vector2, scalarMul(vector3, scalarMul(vector4, 35 stats.gather); stats.avoid); stats.match); stats.exit); double[] change = new double[2]; change[0] = vector1[0] + vector2[0] + vector3[0] + vector4[0]; change[1] = vector1[1] + vector2[1] + vector3[1] + vector4[1]; if(delta(change)>stats.change) { double diff = delta(change)/stats.change; change[0] = change[0]/diff; change[1] = change[1]/diff; } speed[0] += change[0]; speed[1] += change[1]; if(delta(speed)>5) { double diff = delta(speed)/5; speed[0] = speed[0]/diff; speed[1] = speed[1]/diff; } X = X + (int)(speed[0]); Y = Y + (int)(speed[1]); rotation = Math.atan2(speed[1], speed[0]); } public double[] ruleGather(Boid[] myBoids) { double[] result = {0,0}; double[] center = {0,0}; int neighbours = 0; for(Boid b : myBoids) { double[] diff = {X-b.X , Y-b.Y}; if(delta(diff)<stats.viewRange) { center[0] += b.X; center[1] += b.Y; neighbours++; } } center[0] = center[0]/neighbours; 36 KAPITEL 5. APPENDIX center[1] = center[1]/neighbours; result[0] = (center[0] - this.X)/100; result[1] = (center[1] - this.Y)/100; return result; } public double[] ruleAvoid(Boid[] myBoids) { double[] result = {0,0}; int neighbours = 0; for(int i = 0; i < myBoids.length; i++) { if(myBoids[i] != this) { double[] tmp = new double[2]; tmp[0] = myBoids[i].X - this.X; tmp[1] = myBoids[i].Y - this.Y; if(delta(tmp)<stats.viewRange) if(delta(tmp)<stats.personalSpace) { if(tmp[0] != 0) result[0] = result[0] stats.personalSpace/tmp[0]; if(tmp[1] != 0) result[1] = result[1] stats.personalSpace/tmp[1]; neighbours++; } } } if(neighbours>0) { result[0] = result[0]/neighbours; result[1] = result[1]/neighbours; } return result; } public double[] ruleMatch(Boid[] myBoids) { double[] result = {0,0}; int neighbours = 0; for(Boid b : myBoids) { double[] diff = {X-b.X , Y-b.Y}; .2. KÄLLKOD FÖR EVOLUTION if(this != b && delta(diff)<stats.viewRange) { result[0] += b.speed[0]; result[1] += b.speed[1]; neighbours++; } } if(neighbours == 0) return result; result[0] = result[0]/neighbours; result[1] = result[1]/neighbours; result[0] = (result[0] - this.speed[0])/stats.ownWill; result[1] = (result[1] - this.speed[1])/stats.ownWill; return result; } public double[] dontExit() { double[] result = {0,0}; if(X < 0) result[0] = -X; else if(X > Settings.WIDTH+10) result[0] = Settings.WIDTH-X; if(Y < 0) result[1] = -Y; else if(Y > Settings.HEIGHT+10) result[1] = Settings.HEIGHT-Y; return result; } public double delta(double[] vect) { return Math.sqrt(vect[0]*vect[0] + vect[1]*vect[1]); } public double delta(Boid x,Boid y) { double[] vect = new double[2]; vect[0] = x.X; vect[1] = x.Y; vect[0] -= y.X; vect[1] -= y.Y; return Math.sqrt(vect[0]*vect[0] + vect[1]*vect[1]); } 37 38 KAPITEL 5. APPENDIX public boolean death(Boid[] myBoids) { if(X<-100) alive = false; if(X>Settings.WIDTH+100) alive = false; if(Y<-100) alive = false; if(Y>Settings.HEIGHT+100) alive = false; if(!alive) return true; for(Boid b : myBoids) { if(b != this) { if(delta(this,b)<5) { this.alive = false; return true; } } } return false; } } //Stats.java package flockSim; public class Stats implements Comparable<Stats> { public double fitness = 0; public double viewRange = 100; public double personalSpace = 50; public double ownWill = 10; public double gather = 1; public double avoid = 1; public double match = 1; public double exit = 1; public double change = 100; public Stats() { } .2. KÄLLKOD FÖR EVOLUTION 39 public Stats(double[] tmp) { viewRange = tmp[0]; personalSpace = tmp[1] ; ownWill = tmp[2] ; gather = tmp[3] ; avoid = tmp[4] ; match = tmp[5] ; exit = tmp[6] ; change = tmp[7] ; } public double[] toArray() { double[] tmp = new double[8]; tmp[0] = viewRange; tmp[1] = personalSpace; tmp[2] = ownWill; tmp[3] = gather; tmp[4] = avoid; tmp[5] = match; tmp[6] = exit; tmp[7] = change; return tmp; } public void random() { viewRange = 2*Math.random()*viewRange; personalSpace = 2*Math.random()*personalSpace; ownWill = 2*Math.random()*ownWill; gather = 2*Math.random()*gather; avoid = 2*Math.random()*avoid; match = 2*Math.random()*match; exit = 2*Math.random()*exit; change = 2*Math.random()*change; } public void print() { System.out.println("public double viewRange = " + viewRange +";"); System.out.println("public double personalSpace = " + personalSpace +";"); System.out.println("public double ownWill = " + ownWill +";"); System.out.println("public double gather = " + gather +";"); System.out.println("public double avoid = " + avoid +";"); System.out.println("public double match = " + match +";"); System.out.println("public double exit = " + exit +";"); 40 KAPITEL 5. APPENDIX System.out.println("public double change = " + change +";"); } @Override public int compareTo(Stats o) { // TODO Auto-generated method stub return (int)((int)o.fitness - (int)this.fitness); } } //Settings.java package flockSim; public class Settings { public static int WIDTH = 1200; public static int HEIGHT = 800; }