126
Java programmering med Greenfoot Total Viking Power! Dette dokument beskriver et undervisningsforløb i program-mering, til afholdelse i faget Informationsteknologi i gymnasieskolen. Mere specifikt er der tale om et forløb i Java-programmering, hvor udviklings-værktøjet Greenfoot benyttes. Per Laursen 10/1/2010

Java programmering med Greenfoot - Roskilde …laerer.rhs.dk/psl/rhs/HHX-materiale/Programmering... · Web viewNår man programmerer en større mængde kode, kan det imidlertid være

Embed Size (px)

Citation preview

Java programmering med GreenfootTotal Viking Power!

Dette dokument beskriver et undervisningsforløb i program-mering, til afholdelse i faget Informationsteknologi i gymnasieskolen. Mere specifikt er der tale om et forløb i Java-programmering, hvor udviklings-værktøjet Greenfoot benyttes.

Per Laursen10/1/2010

1 Indledning.................................................................................................................................................3

2 Introduktion til Java og Greenfoot............................................................................................................4

3 Installation af Greenfoot..........................................................................................................................5

4 I gang med Greenfoot...............................................................................................................................6

5 Introduktion til Objekt-orientering...........................................................................................................9

6 Greenfoot og Wombats-scenariet..........................................................................................................11

6.1 Aktivering af opførsel......................................................................................................................13

7 Total Viking Power!................................................................................................................................17

7.1 Java-kode i TVP_Kap7.....................................................................................................................20

7.2 Klasse-definition i Java....................................................................................................................21

7.3 Variabel-definition i Java................................................................................................................22

7.4 Metode-definition i Java.................................................................................................................24

7.5 Opbygningen af Viking-klassen.......................................................................................................25

7.5.1 act metoden............................................................................................................................26

7.5.2 If-sætningen............................................................................................................................27

7.5.3 Move metoden.......................................................................................................................29

7.5.4 Kald af metoder fra andre klasser...........................................................................................29

7.5.5 Metoderne move…()...............................................................................................................30

7.5.6 meetsObject metoden............................................................................................................31

7.5.7 interactWithObject metoden..................................................................................................33

7.5.8 InteractWithFire og –Food metoderne...................................................................................34

7.5.9 Metoderne isDead og doDeathBehavior (første version).......................................................35

7.6 Scenariet TVP_Kap7_Opg7_4_efter................................................................................................35

7.7 Klassen TVPWorld...........................................................................................................................36

7.7.1 Brug af arrays i Java................................................................................................................37

7.7.2 Konstanter..............................................................................................................................39

7.7.3 TVPWorld constructoren........................................................................................................40

7.7.4 populateTVPWorld metoden..................................................................................................41

7.7.5 for-løkker i Java.......................................................................................................................42

7.7.6 populateFire metoden............................................................................................................43

7.7.7 while-løkker i Java...................................................................................................................44

1

7.7.8 Metoderne populateFood og populateViking.........................................................................45

7.7.9 createWallSegment metoden.................................................................................................45

7.7.10 Ændringer grundet indførslen af Wall-objekter i verdenen....................................................46

7.8 Scenariet TVP_Kap7_Opg7_6_efter................................................................................................47

7.9 Forbedringer af kodens design.......................................................................................................49

7.9.1 Principper for ændringer af kodens design, første runde.......................................................49

7.9.2 Redesign af move… metoderne..............................................................................................50

7.9.3 Redesign af canMove… metoderne........................................................................................51

7.9.4 Redesign af populate… metoderne.........................................................................................52

7.10 Scenariet TVP_Kap7_Opg7_7_klar..................................................................................................55

7.10.1 Tilføjelse af en bevægelig Actor..............................................................................................56

7.10.2 Respawn af Actor-objekter i verdenen...................................................................................58

7.11 Scenariet TVP_Kap7_Opg7_14_efter..............................................................................................63

7.11.1 Indførsel af nye base-klasser...................................................................................................63

7.11.2 Indførsel af base-klassen Respawnable..................................................................................64

7.11.3 Indførsel af base-klasse Interactable......................................................................................68

7.12 Scenariet TVP_Kap7_Opg7_17_efter..............................................................................................71

7.12.1 Indførelse af Infoboard...........................................................................................................71

7.12.2 Øvre grænse for health-points................................................................................................73

7.12.3 Indførsel af experience-points................................................................................................74

7.13 Scenariet TVP_Kap7_Opg21_efter..................................................................................................75

7.13.1 Revision af move()-metoden...................................................................................................78

7.13.2 At slippe en drage fri…............................................................................................................81

7.14 Scenariet TVP_Kap7_Opg7_26_klar................................................................................................84

7.14.1 Udviking af en Moving-klasse.................................................................................................84

7.14.2 Mulighed for at vinde spillet...................................................................................................87

7.14.3 Indførsel af sværhedsgrad......................................................................................................89

2

Java programmering med Greenfoot

Dette dokument beskriver et undervisningsforløb i programmering, til afholdelse i faget informations-teknologi i gymnasieskolen. Mere specifikt er der tale om et forløb i Java-programmering, hvor udviklingsværktøjet Greenfoot benyttes.

1 Indledning

Forløbet i Java-programmering er baseret på et gennemgående eksempel, hvor der opbygges et – meget simpelt – Role Playing Game (RPG), kaldet Total Viking Power (TVP). Spillet vil være løst inspireret af elementer fra websiten http://www.viking.virkelighed.dk/, som jo rummer alt man bør vide om vikinger

Ideen er at tage udgangspunkt i et ekstremt simpelt ”skelet” for spillet, hvor kun nogle ganske fundamen-tale typer funktionalitet er til stede. På dette skelet tilføjes der gradvist flere og flere funktionaliteter, som kan gøre spillet mere interessant og spilbart. Dette vil være funktionaliteter inspireret af gængse elementer i kendte RPGs, f.eks. World of Warcraft, DotA, etc..

Det underliggende formål er naturligvis at give deltagerne i forløbet kundskaber i Java programmering. I forhold til mere traditionelle gennemgange af et programmeringssprog, vil gennemgangen være drevet af den type funktionalitet, vi i hvert trin ønsker at tilføje til udgangspunktet. De sproglige elementer i Java gennemgås derfor mere på en ”need-to-know” basis, frem for f.eks. at gennemgå alle datatyper for variable, før de benyttes. Således vil udvidelsen af kendskabet til Java gå hånd i hånd med udvidelsen af programmets funktionalitet.

Det er derfor vigtigt at bemærke, at dette forløb ikke er tænkt som en fuldstændig gennemgang af alle elementerne i Java-programmering, og dertil hørende teori om Objekt-Orienteret programmering. Kun de nødvendige dele medtages. Har man brug for mere komplette beskrivelser af f.eks. en specifik sproglig konstruktion i Java, må man opsøge dette fra andre kilder, f.eks. www.javabog.dk

3

2 Introduktion til Java og Greenfoot

Java er et programmeringssprog, d.v.s. et sprog med hvilket man kan specificere et antal instruktioner, som en computer efterfølgende kan udføre. En sådan samling af instruktioner kan f.eks. specificere hvordan man finder alle primtal fra 1 til 10000, hvordan man tegner nogle datapunkter i en graf, eller noget helt tredje, f.eks. en specifikation af et spil.

Java hører til kategorien af Objekt-Orienterede programmeringssprog, hvilket vil sige at man prøver at modellere det problem man vil løse ved hjælp af såkaldte klasser og objekter. Meget kort fortalt prøver man at samle data samt opførsel (hvordan data bruges) i klasser, som kan repræsentere ting i den model, programmet arbejder på. Er programmet f.eks. et skak-program, kunne man have en klasse for hver type af skakbrik, der specificerer forskellige egenskaber ved netop den type skakbrik. Vi vender tilbage til klasser og objekter i meget større detalje senere.

Java rummer mange muligheder, og man kan stort set konstruere et Java-program til brug indenfor hvilket som helst område. Java er også et af de førende sprog indenfor programmering i det hele taget. På trods heraf er det bestemt ikke en let opgave at lære Java, og i det hele taget er programmering ikke et let emne. At omsætte ideer og beskrivelser (i menneskesprog) af f.eks. et spil til et Java-program kræver tålmodighed, koncentration, præcision og sans for detaljer. Selv en tilsyneladende ubetydelig unøjagtighed i programmet kan gøre at programmet enten slet ikke vil køre, eller opfører sig forkert mens det kører. I modsætning til en dansk stil – hvor læseren nok kan forstå hele stilen på trods af lidt stavefejl og grammatiske fejl hist og her – er computeren ikke til at forhandle med; programmet skal være 100 % præcist formuleret.

En konsekvens af dette er, at man ved brug af de traditionelle værktøjer til Java-programmering (et værktøj til Java-programmering er sådan set bare et andet program, som er i stand til at ”oversætte” et Java-program til et sprog som en computer kan forstå) ofte oplever, at man skal vide ret meget om Java, før man overhovedet kan lave et program der kan ”noget”, og dette ”noget” er ofte bare at skrive nogle linier tekst ud på skærmen. At lave noget der involverer grafik kræver ofte temmeligt meget programmering, hvilket kan virker ret frustrerende på nybegynderen, der gerne vil lave ”noget spændende” ret hurtigt. Den indsats der skal til for at lave et program, der bare tilnærmelsesvist kan noget a la det som ”rigtige” programmer kan – såsom moderne spil – kan virke meget overvældende og nedslående…

I erkendelse af dette problem har man flere gange prøvet at lave programmerings-værktøjer, som gør det nemmere at komme i gang med at lære programmering. Et nyere bud på dette er værktøjet Greenfoot, som grundlæggende blot er et værktøj til Java-programmering, men med nogle tilføjelser der dels gør det nemmere at visualisere forskellige problemstillinger i Java-programmering og Objekt-Orienteret program-mering i det hele taget, dels gør det noget nemmere at få lavet programmer der indeholder (simpel) grafik. Med andre ord skulle dette værktøj gøre det muligt at lære Java og programmering på en lettere og – ikke mindst – sjovere måde. Derfor benyttes Greenfoot som programmeringsværktøj i dette forløb.

4

3 Installation af Greenfoot

Greenfoot er udviklet specifikt til brug i undervisning, og er derfor gratis og frit tilgængeligt for alle interes-serede. For at hente Greenfoot går man til www.greenfoot.org/download, og følger instruktionerne derfra. På nuværende tidspunkt (januar 2010) er version 1.5.6 den nyeste version, men generelt bør man blot hente den nyeste version. I øvrigt rummer www.greenfoot.org i sig selv meget spændende materiale – dog alt sammen på engelsk – som man selv kan se nærmere på.

Inden man installerer Greenfoot på sin computer, skal man lige sikre sig, at man har det såkaldte JDK (Java SE Development Kit) installeret. Dette JDK rummer forskellige Java biblioteker, som Greenfoot skal bruge for at kunne fungere korrekt. Det er ikke noget man behøver at tænke over når man bruger Greenfoot, men det skal altså være tilstede på computeren. På en Mac-computer vil det allerede være installeret, mens det ikke som sådan er en del af en Windows-computer. Har man på nogen måde været i forbindelse med Java på sin computer – typisk ved at benytte en website der rummer et Java-program – vil man dog oftest allerede have JDK installeret. Er man usikker, kan man checke i Kontrolpanelet, under Programmer. Har man brug for at installere JDK, kan det hentes på http://java.sun.com/javase/downloads/index.jsp. På nuværende tidspunkt (januar 2010) er JDK 6 Update 17 den nyeste, men generelt bør man blot hente den nyeste version.

Når man har sikret sig at JDK er korrekt installeret, kan man installere Greenfoot selv. Man følger blot de instruktioner, der vises undervejs, og man bør ikke ændre på de indstillinger der foreslås. Bemærk, at Greenfoot som udgangspunkt installeres i ”roden” af ens filsystem, d.v.s. i folderen C:\Greenfoot. Intentionen er at alt som har med Greenfoot at gøre ligger i denne folder, så personer der ikke er så velbevandrede i Windows’ filsystem nemt kan finde alt der relaterer til Greenfoot.

Når installationen er overstået, burde der være fremkommet et Greenfoot-ikon på desktoppen, og vi er klar til at se nærmere på Greenfoot.

Figur 3.1: Greenfoot-ikonet

5

4 I gang med Greenfoot

Inden vi går i gang, skal konceptet scenarie (eng.: scenario) lige på plads. Et scenarie er i denne sammen-hæng en betegnelse for alle de elementer, der indgår i et Java-program. I ethvert Java-program af en vis størrelse vil selve Java-koden være delt op i flere filer, for at give bedre overblik over koden. Desuden kan der indgå billeder, lyd med videre i et program, som også ligger i separate filer. Hele denne samling af filer udgør et scenarie, og alle disse filer ligger i den samme folder. Der følger et antal færdige scenarier med Greenfoot-installationen, og disse scenarier ligger alle i folderen C:\Greenfoot\scenarios, i hver deres separate folder.

Når Greenfoot startes for allerførste gang, fremkommer der en dialog a la nedenstående:

Figur 4.1: Greenfoot start dialog (første gang)

Her vælges blot muligheden Continue without scenario, hvorefter vi kommer frem til selve Greenfoot-hovedvinduet

6

Figur 4.2: Greenfoot hovedvindue

Det første man bemærker ved Greenfoot, er de relativt få valgmuligheder der er i programmet. Dette er helt tilsigtet. Mange andre værktøjer til programmering rummer langt flere muligheder, men er også langt mere uoverskuelige og vanskelige at komme i gang med. I Greenfoot behøver man kun kende ganske få valgmuligheder for at komme i gang.

Til at starte med har vi to muligheder:

Lav et nyt scenarie Åbn et eksisterende scenarie

Vi arbejder med eksisterende scenarier her i starten. I første omgang ser vi nærmere på et af de scenarier, der følger med Greenfoot. Dette giver os også mulighed for at se nærmere på de forskellige områder i hovedvinduet.

Vi ser først på scenariet Wombats. Vi åbner dette scenarie ved at vælge menuen Scenario | Open…, klikke på Wombats, og klikke på Open-knappen:

Figur 4.3: Greenfoot Open Scenario dialog

7

Når Wombats-scenariet er åbnet, ser Greenfoot nogenlunde således ud:

Figur 4.4: Greenfoot hovedvindue med scenarie

Dette er hovedvinduet vi allerede så i Figur 4.2, men nu har vi indlæst et scenarie, så der er flere ting at se på nu. De fire vigtige hovedområder i vinduet er:

Menu (øverst): Denne fungerer som i ethvert andet program. Hvis man studerer menuerne lidt, vil man se at der faktisk ikke er så mange funktioner at vælge imellem. Det gør Greenfoot overskueligt at arbejde med. Bemærk, at der under Help er en del links til forskelligt nyttigt materiale om Greenfoot.

Kontroller (nederst): Når et scenarie er indlæst, kan det igangsættes (køres) ved hjælp af disse kontroller. Den enkleste måde er blot at klikke på Run, hvorefter afviklingen af programmet går i gang. Hastigheden hvormed programmet afvikles kan reguleres med kontrollen Speed. Knappen Act giver mulighed for at afvikle programmet i enkelte skridt – mere om det senere.

Verden (venstre): Langt de fleste Greenfoot-programmer vil have en grafisk del, der som udgangspunkt er en 2-dimensionel ”verden” opdelt i et antal celler. I eksemplet er verden delt op i 8x8 celler, og de objekter scenariet rummer kan indsættes i celler i verdenen.

Klassediagrammet: (højre) Som nævnt er Greenfoot et værktøj til at lave Java-programmer, og Java er et Objekt-Orienteret sprog. Et scenarie vil derfor også rumme et antal klasser, som vi kan bruge til at lave konkrete objekter. Disse klasser vil ”arve” fra basis-klasserne World og Actor. Arv er et begreb indenfor objekt-orientering, som vi nu skal se lidt nærmere på.

8

5 Introduktion til Objekt-orientering

Objekt-Orientering er ikke et programmeringssprog, men en teknik til at lave modeller for et område, vi gerne vil lave et program for. Denne måde at lave modeller på er efterhånden anerkendt som meget effektiv, og benyttes derfor bredt. Mange programmeringssprog – inklusive Java – gør det muligt direkte at omsætte en objekt-orienteret model til programkode.

Et helt centralt koncept indenfor Objekt-Orientering er en klasse. Formålet med en klasse er at modellere en bestemt ting fra det emneområde, man prøver at modellere. Hvis man gerne vil modellere f.eks. et øko-system, vil man sandsynligvis lave klasser for de dyr, planter med videre, som findes i øko-systemet. I en lille model af et øko-system kunne man måske have klasser for dyrene orm, fugl og ræv. Som regel skriver man en klasses navn med stort, f.eks. Orm, Fugl og Ræv.

Hvad menes der så med, at en klasse skal ”modellere” noget? Helt overordnet set vil en klasse altid beskrive to egenskaber ved en ting:

Tingens tilstand: Langt de fleste ting har en tilstand, som vi i en klasse kan vælge at modellere mere eller mindre komplet. En meget simpel model for et menneskes tilstand kunne være værdierne for blodtryk og puls, altså bare tre talværdier (f.eks: puls=70, blodtryk=80/130). En så simpel model giver naturligvis et meget forsimplet billede af et menneskes tilstand, men det kan være tilstrækkeligt alt efter modellens formål. I et meget simpelt RPG kunne en spillers tilstand være modelleret ved et antal health points, som så kan ændre sig ved interaktion med andre elementer i spillet, f.eks. et monster.

Tingens opførsel: Ting har ligeledes en opførsel, d.v.s. forskellige handlinger de kan foretage sig. Har man en klasse der modellerer et rovdyr, kan den måske have en opførsel kaldet hunt. Når denne bestemte opførsel ”aktiveres”, vil (modellen af) rovdyret udføre de handlinger, vi har defineret som det man gør, når man jager. Bemærk, at en opførsel sagtens kan være afhængig af tilstanden for tingen. Hvis vores model for et rovdyr også rummer et tal der beskriver dyrets mæthed, kan dyrets hunt-opførsel måske være forskellig, alt efter om dyret er sultent eller mæt.

Når vi har lavet en tilstrækkelig specifikation af tingens tilstand og opførsel, kan vi samle alt dette i en klasse, som altså er en model for tingen. Når klassen er defineret, kan vi ud fra klassen lave konkrete objekter. Objekter er et andet centralt koncept i Objekt-Orientering. Hvor en klasse er en slags ”opskrift” på, hvordan en eller anden ting opfører sig, er et objekt et helt konkret eksemplar af denne klasse. Et eksempel kunne være en klasse for menneske. Denne klasse skal specificere tilstand og opførsel for et menneske helt generelt, og ud fra den klasse kan vi så lave konkrete mennesker, f.eks. Jørgen, Signe og Bente. Dette er tre individuelle mennesker, men deres tilstand og opførsel er modelleret på den samme måde, nemlig som defineret i klassen Menneske. Man siger så, at Jørgen, Signe og Bente er objekter af typen Menneske

9

Det er meget vigtigt at forstå, at selv om de tre nævnte personer alle er af typen Menneske, og derved har en fælles definition af tilstand og opførsel, kan de sagtens have forskellige tilstande. På et givent tidspunkt er tilstanden af de tre personer måske således:

Person Puls BlodtrykJørgen 70 80/130Bente 160 110/180Signe 55 70/110

Disse værdier kan måske indikere, at Jørgen er vågen og afslappet, Signe sover, mens Bente dyrker hård motion. Dette kan så medføre, at en bestemt opførsel vil udmønte sig forskelligt for personerne, fordi de har forskellige tilstande. Men igen, de er alle objekter af typen Menneske

Det sidste koncept i Objekt-Orientering vi ser på i denne omgang er nedarvning. Hvis man laver en model for f.eks. et øko-system, vil man sandsynligvis lave modeller – og dermed klasser – for mange forskellige dyr. Højst sandsynligt vil det være sådan, at mange dyr har meget tilfælles, både hvad angår tilstand og opførsel. Man kan derfor risikere at stå med en masse klasser, der har en stor fællesmængde der gentager sig igen og igen. Dette er ikke så hensigtsmæssigt. Hvad nu hvis man gerne vil ændre noget fundamentalt i sin model? Så kan man risikere at skulle rette dette i en masse klasser, hvilket dels giver unødigt meget arbejde, dels øger risikoen for fejl. Det ville være nemmere, hvis man kun behøvede at specificere tilstand og opførsel én gang, selv om det skal deles af mange klasser.

Dette kan faktisk opnås ved brug af nedarvning mellem klasser. I stedet for at lave klasser for f.eks. specifikke dyr (Tiger, Hest, Orm, Hund, Kanin,…), kan man lave klasser der er mere generelle. Måske kan vi starte med at finde de dele af tilstand og opførsel, som er fælles for alle dyr. Dette kan vi så definere i en klasse Dyr. Denne klasse rummer således det som alle dyr har tilfælles, men denne klasse er så generel, at det ikke giver mening at lave et objekt af typen Dyr. Det er ligesom i den virkelige verden – et levende væsen er ikke bare et ”dyr”, det er jo en tiger, hest, orm, og så videre. Det smarte er, at når vi så skal lave en klasse for f.eks. en tiger, behøves vi kun at inkludere det som et specifikt for en tiger, hvis blot vi lader Tiger-klassen arve fra Dyr-klassen.

På diagram-form tegner man nedarvning mellem klasser således:

Figur 5.1: Klassen Tiger arver fra klassen Dyr

En klasse vises som et rektangel med klassens navn i, og pilens retning angiver, hvilken klasse der nedarver fra hvilken. Ofte kalder man klassen der arves fra for en base-klasse, i dette tilfælde er Dyr altså en base-klasse.

10

6 Greenfoot og Wombats-scenariet

Efter denne korte introduktion til Objekt-orientering, burde vi bedre kunne forstå, hvordan Wombats-scenariet er bygget op. Lad os se på hoved-vinduet igen:

Figur 6.1: Greenfoot hovedvindue med Wombats-scenariet indlæst

Det burde nu være lidt mere forståeligt, hvordan klasse-diagrammet skal læses. I første omgang ser vi kun på del del, der hedder Actor classes. I Greenfoot er alting bygget op om en ”verden”, samt de ting der kan eksistere i denne verden. Sådanne ting kaldes under et for actors (aktører). En aktør er altså ”noget”, som eksisterer i verdenen. Dette medfører ganske naturligt, at Greenfoot rummer en på forhånd defineret base-klasse Actor, som definerer den tilstand og opførsel, alle aktører i verden deler. Enhver konkret ting i verdenen skal således arve fra Actor. I Wombats-scenariet er der defineret to klasser svarende til to konkrete aktører i verdenen, nemlig Wombat (et australsk pungdyr) samt Leaf (blad). Ligesom i eksemplet med Dyr og Tiger, kan vi altså læse ud fra diagrammet, at Wombat og Leaf begge arver fra Actor-klassen.

Hvad kan vi gøre med en klasse? Hvis vi prøver at markere og højre-klikke på Wombat-klassen i klasse-diagrammet, fremkommer en menu med forskellige valgmuligheder. I første omgang er det kun punktet new Wombat(), der er interessant. Hvis vi vælger dette punkt, fremkommer en Wombat-grafik ved muse-cursoren, og vi kan derefter indsætte en Wombat i en celle i verdenen

Opgave 6.1: Prøv at indsætte et antal Wombats i verdenen, og find ud af, hvordan man fjerner en Wombat igen. Prøv efterfølgende det samme for Leaf-klassen. Prøv til sidst at klikke på Reset, og se hvilken effekt det har.

11

Hvad var det egentlig vi gjorde? Når vi vælger new Wombat(), laver vi et nyt objekt af typen Wombat. Det kan vi gøre så mange gange vi vil, og dermed indsætte mange objekter af denne type i vores verden. Det samme gælder også for Leaf-klassen. Bemærk dog, at vi ikke kan lave et objekt af typen Actor (prøv selv), idet denne klasse jo blot er en base-klasse. Skal man være helt præcis, siger man dog at f.eks. et objekt af type Leaf også har typen Actor, fordi Actor er en base-klasse for Leaf. Men vi kan altså ikke lave et objekt som ”kun” er en Actor.

Som nævnt flere gange definerer en klasse dels tilstand og dels opførsel. Med hensyn til tilstand defineres dette ved en eller flere egenskaber, som kan have en værdi (f.eks. eksemplet med puls og blodtryk). For et objekt af typen Wombat kan vi se objektets tilstand ved at højreklikke på objektet, og vælge Inspect (bemærk, at man skal klikke på et konkret objekt der er indsat i verdenen, ikke på klassen i klasse-diagrammet). Derved fremkommer et vindue som nedenstående (muligvis med andre talværdier)

Figur 6.2: Tilstand for et Wombat-objekt

Dette kan se lidt forvirrende ud, men i første omgang skal vi blot se på de to øverste linier:

private int direction private int leavesEaten

Indtil videre ignorerer vi ordene private int, som har noget at gøre med de værdier, egenskaben kan have. Så vi har altså to egenskaber direction (retning) og leavesEaten (antal spiste blade). Åbenbart har skaberen af dette scenarie valgt, at modellen af en Wombat rummer en retning for Wombaten, samt det antal blade, den har spist. Med hensyn til retning viser det sig, at forfatteren har defineret fire retninger (EAST, WEST, NORTH, SOUTH), som repræsenteres ved talværdierne (0, 1, 2, 3). For dette Wombat-objekt er direction lig 0, og snuden peger ganske rigtigt mod øst. leavesEaten er lig 0, hvilket vel virker meget naturligt, eftersom dette Wombat-objekt lige er blevet skabt. Vi kommer senere ind på, hvordan man rent praktisk sætter disse start-værdier til noget fornuftigt.

12

Vi kan se på Figur 6.2, at der er flere egenskaber for en Wombat: x, y og rotation. Ved lidt eksperimenteren kan man nok finde ud af, at x og y angiver hvor i verden den specifikke Wombat er placeret. Egenskaben rotation angiver, hvordan objektet er roteret i forhold til vandret – det er i sig selv ikke så interessant; det interessante i denne sammenhæng er, at disse tre egenskaber ikke er specifikke for et Wombat-objekt, men er defineret for alle objekter af typen Actor. Dette giver vel også god mening – hvis alle objekter i verdenen skal være af typen Actor, vil de jo også alle have en position (og rotation). Med andre ord er disse egenskaber defineret i Actor-klassen.

Opgave 6.2: Indsæt et par Leaf-objekter i verdenen, og prøv at bruge Inspect på dem. Nu burde du kunne se, at alle Leaf-objekter også har egenskaberne x, y og rotation, men ikke direction og leavesEaten .

De sidste par egenskaber world og image er lidt mere komplicerede, og dem ser vi ikke på i denne omgang. De er også defineret i Actor-klassen.

6.1 Aktivering af opførsel

Et objekt er ikke specielt spændende, hvis ikke dets tilstand kan ændres. For at ændre et objekts tilstand, skal vi som regel aktivere et af objektets mulige opførsler. Disse opførsler er til rådighed i form af såkaldte metoder, som er defineret i den tilhørende klasse. Vi kan aktivere (eller ”kalde”) en metode på et objekt. Hvis vi igen prøver at indsætte et Wombat-objekt i verdenen, og efterfølgende højre-klikke på det, får vi en menu som den nedenstående frem:

Figur 6.3: Metoder på et Wombat-objekt

Alle menupunkterne i den midterste del af menuen er de metoder, vi kan aktivere på objektet (fra act() til turnLeft()). Ud over selve metoden navn, f.eks. eatLeaf(), kan vi se at der dels er et ord foran metodens navn, dels er et sæt parenteser efter metodens navn. Det første er metodens returtype, det andet er metodens parametre.

13

En metode-definition ser altid således ud:

returtype metodeNavn(parametre…)

Og hvad betyder det så? Det kan sammenlignes lidt med en almindelig funktion fra matematik. Her kan vi f.eks. definere en funktion f(x) = x + 2. En sådan funktion tager et enkelt tal som input, og giver et enkelt tal som output. I metode-terminologien siger vi, at funktionen tager en enkelt parameter af typen tal som input, og returnerer en værdi af typen tal. Metoder er imidlertid mere generelle end funktioner, idet de dels kan tage nul, en eller flere parametre som input, dels kan have andet end tal som deres returtype.

Tag f.eks. metoden int getLeavesEaten(). Vi kan nok gætte, hvad denne metode gør; den beder objektet om at fortælle, hvor mange blade det har spist (hvilket jo blot er den øjeblikkelige værdi for egenskaben leavesEaten). Der er ikke brug for nogle parametre til at svare på dette spørgsmål, derfor tager metoden ingen parametre. Svaret kommer i form af et tal, derfor er metodens returtype int (int er en kort form af integer, som betyder ”heltal”, altså et tal som 1,2,3,4,…).

Et andet eksempel er metoden void setDirection(int). Denne metode kan benyttes til at ændre den retning, Wombat-objektet peger. For at løse denne opgave skal vi jo fortælle objektet, hvad den nye retning skal være, derfor tager metoden en enkelt parameter af typen int (husk at man i dette program har defineret retningen ved hjælp af heltal). På den anden side er der ikke rigtigt noget fornuftigt at returnere fra denne metode. Vi beder jo bare objektet om at gøre noget, ikke om at fortælle noget om sig selv, som i det forrige eksempel. At en metode ikke returnerer noget, angiver man med det specielle ord void som returtype. Void betyder noget a la ”tomrum” eller ”ingenting”. Med andre ord skal en metode altid have en returtype, men hvis den i praksis ikke returnerer noget, angiver man returtypen til void.

Den allersimpleste form for metode er en som f.eks metoden void eatLeaf(). Vi beder objektet om at gøre noget, så der er ikke noget at returnere, og ligeledes har objektet ikke brug for nogen information for at løse opgaven, så der er heller ikke nogen parametre. Den kan ikke blive simplere end dette.

Hvordan aktiverer vi så en af disse metoder? Ganske enkelt ved at vælge metoden i den menu, der frem-kommer ved at højre-klikke på objektet. Hvis metoden har brug for parametre (som for setDirection), bliver vi bedt om at indtaste en parameter-værdi:

Figur 6.4: Indtast en parameter-værdi til metoden setDirection

14

Hvis metoden returnerer en værdi (som for metoden getLeavesEaten), bliver værdien vist efter metoden er blevet aktiveret:

Figur 6.5: Returværdi fra metoden getLeavesEaten

Opgave 6.3: Prøv at aktivere nogle forskellige metoder på et eller flere Wombat-objekter. Prøv at under-søge, hvordan hver aktivering ændrer objektets tilstand (det er ikke alle metoder, som ændrer objektets tilstand). Hvordan får man en Wombat til at spise nogle blade? Prøv det!

Hvis man leger lidt med et Leaf-object, vil man se at man ikke umiddelbart kan kalde nogle metoder på et sådant objekt (der står no accessible methods i menuen)…og dog. Tilbage på Figur 6.3 kan vi se, at der er to menupunkter ovenover listen af metoder, kaldet inherited from Object, og inherited from Actor. Vi har allerede nævnt, at Actor-klassen er base-klasse for alle ting som skal eksistere i vores verden. Det betyder så, at en klasse som f.eks. Wombat vil arve tilstand og opførsel fra Actor. Mere konkret betyder det, at alle de metoder der er defineret i Actor-klassen, også er til rådighed for Wombat-objekter, via undermenuen inherited from Actor.

Opgave 6.4: Prøv at aktivere nogle af de metoder, som Wombat arver fra Actor, på et eller flere Wombat-objekter. Prøv at undersøge, hvordan hver aktivering ændrer objektets tilstand (det er ikke alle metoder, som ændrer objektets tilstand).

At Wombat arver fra Actor-klassen, er imidlertid ikke hele historien. I Java-sproget er det sådan, at alle klasser automatisk arver fra en base-klasse kaldet Object (lidt forvirrende navngivning, men sådan er det altså…). De metoder man arver fra Object-klassen er nogle meget basale metoder, hvis navne ikke giver meget mening på dette tidspunkt. Vi beskæftiger os ikke yderligere med disse metoder nu.

Ser man i menuen over metoder, som Wombat arver fra Actor, er den første metode act() lidt speciel. I menuen står der:

void act() [redefined in Wombat]

15

Med visse metoder man arver fra en base-klasse er det sådan, at man kan vælge at omdefinere metoden til at udføre noget andet, end det den gør i base-klassen. I visse tilfælde skal man rent faktisk omdefinere metoden. Når man definere en base-klasse, kan man ofte godt regne ud, at alle de kommende klasser der arver fra base-klasse skal have en bestemt opførsel, men i base-klassen kan man ikke sige noget konkret om, hvordan den opførsel vil være. Hvis vi igen ser på vores Dyr-klasse, kan vi vælge at sige, at alle dyr kan lave en lyd, og derfor definerer vi en metode lavLyd() i Dyr-klassen Men vi kan på den anden side ikke sige noget konkret om, hvordan denne lyd lyder i praksis, netop fordi den er specifik for hver konkret slags dyr. Her vil det så være smart at forlange, at man i en klasse for et konkret dyr – som dermed arver fra Dyr – skal omdefinere metoden lavLyd() fra at ”gøre ingenting” til at lave den lyd, som dyret nu engang laver.

Det er dette princip der er benyttet for metoden act(). Man har defineret, at alle aktører i verdenen skal ”agere” (eller hvordan man nu vil oversætte ”to act”), men hvordan hvert objekt konkret skal agere, skal defineres i den tilhørende klasse (f.eks. Wombat-klassen).

Opgave 6.5: Prøv at aktivere act()-metoden på et eller flere Wombat-objekter. Prøv beskrive i ord og regler, hvad den gør. Kan man beskrive hvad act()-metoden gør ved hjælp af de andre metoder, som er til rådighed i klassen Wombat?

16

7 Total Viking Power!

Vi skulle nu gerne have en grundlæggende forståelse af, hvordan et Greenfoot-scenarie fungerer. Vi har et antal klasser, som arver fra en af de to grundlæggende klasser Actor eller World. Ud fra disse klasser kan vi lave et antal objekter, som kan eksistere og interagere i vores verden. Reglerne for et objekts opførsel findes i form af de metoder, der er defineret i den tilhørende klasse.

Tiden er nu kommet til at begynde at se på det scenarie, som skal være det gennemgående eksempel i dette programmeringsforløb: Total Viking Power – spillet!

Vi forkorter Total Viking Power til TVP, og vil undervejs i forløbet arbejde med at tilføje forskellige typer funktionalitet til det grundlæggende spil. For at alle har det samme udgangspunkt at arbejde videre fra, vil vi – efter at have arbejdet med en bestemt funktionalitet – undervejs få stillet nye versioner af TVP til rådighed, hvor funktionaliteten er inkluderet. Dermed gør det ikke noget, hvis man finder forskellige løsninger til et givent problem (det vil faktisk kun gøre det hele mere interessant), idet vi alle får det samme udgangspunkt efterfølgende.

Den første version af TVP hedder TVP_Kap7. Alle versioner af TVP vil kunne findes på klassens intranet-side, under Dokumenter | Programmering | TVP Scenarier. Et specifikt scenarie findes i form af en .zip fil, som man skal unzippe i scenarios-folderen under Greenfoot (altså i mappen C:\Greenfoot\scenarios). Når man har unzippet filen, skulle der gerne være kommet en folder TVP_Kap7 i scenarios-folderen.

Når man indlæser TVP_Kap7 scenariet, skulle man gerne se noget a la det nedenstående i Greenfoot:

Figur 7.1: Greenfoot med TVP_Kap7 indlæst

17

TVP_Kap7 er en lille smule mere komplekst end Wombats-scenariet, men der er stadig mange ligheds-punkter her til at starte med.

Først og fremmest har vi klassen Viking, som repræsenterer den figur, vi kan styre i spillet. Vi ser senere nærmere på en Vikings opførsel – lige nu skal vi blot bemærke, at en Viking rent grafisk vises som en lille vikinge-agtig ikon, omgivet af en smal rød ramme, for at gøre vikingen mere synlig.

Vikingen lever i en verden, som overordnet set minder om Wombat-verdenen. Den består af et kvadrat, som er delt op i et antal kvadratiske celler, mere specifikt 32 gange 32 celler. Hver celle er på 25 gange 25 pixels, så hele verdenen er altså på 800 gange 800 pixels. Idet vi gerne vil holde grafikken for en figur indenfor en celle, kan figuren altså ikke være større end 25 gange 25 pixels, så der er grænser for, hvor naturtro en figur kan blive…

Ud over Viking-klassen har vi også en Fire-klasse og en Food-klasse. Dette er ”passive” elementer, som vi ikke kan styre. Som vi skal se senere, kan en Viking dog godt interagere med Fire- og Food-objekter.

Hvis man prøver at klikke på Reset-knappen i bunden af vinduet, vil man se at TVP-verdenen tager sig anderledes ud efter hvert klik. Dette skyldes, at man ved at klikke på Reset får genereret hele TVP-verdenen forfra. Lige nu er denne generering baseret på tilfældighed, således at der genereres et antal Food- og Fire-objekter på tilfældige steder i verdenen. Hvordan det mere detaljeret foregår, skal vi også se nærmere på senere i forløbet.

Lad os nu få lidt gang i vores Viking. Hvad kan han så? Lad os prøve at højre-klikke på ham, og se hvilke metoder vi kan aktivere på ham:

Figur 7.2: Metoder på et Viking-objekt

Her i starten er det ganske få metoder, vi har til rådighed. Det er nok også rimeligt indlysende, hvad de gør.

Opgave 7.1: Prøv at aktiver nogle af metoderne på Viking-objektet. Gør de, hvad du ville forvente? Hvad sker der, hvis vikingen bevæger sig hen over en celle, hvor der i forvejen er et Fire- eller Food-objekt? Hvad sker der, hvis du aktiverer act()-metoden?

18

Ikke overraskende kan de fire move…() metoder få vikingen til at gå i forskellige retninger. Hvis man går hen over et Fire- eller Food-objekt, sker der imidlertid ingenting; objektet bliver bare liggende i sin celle. Dette er måske ikke hvad man ville forvente; det ville være mere naturligt, hvis der var en eller anden form for interaktion mellem vikingen og disse objekter. Dette skyldes, at de fire move…() metoder blot håndterer det at bevæge sig fra en celle til en anden. Den logik, der beskriver hvordan en viking skal interagere med de objekter han møder, findes i andre metoder.

Umiddelbart sker der heller ikke noget, hvis vi aktiverer act()-metoden. Husk at det er denne metode, der skal rumme den overordnede logik for, hvordan et Viking-objekt opfører sig. Hvis vi i stedet klikker på Run – som blot kalder act()-metoden på alle objekter igen og igen – sker der heller ikke noget. Det er imidlertid sådan, at man skal bruge piletasterne for at styre vikingen rundt i verdenen, så meget naturligt sker der ikke noget, med mindre man trykker på piletasterne.

Opgave 7.2: Prøv at sætte scenariet i gang, ved at klikke på Run-knappen. Derefter kan vikingen styres rundt i verdenen, ved hjælp af piletasterne. Hvordan er vikingens interaktion med de andre objekter nu?

Nu kan vi få vikingen til at gå rundt i verdenen, og ligeledes er der nu en interaktion, når han går hen over et Fire- eller Food-objekt; objektet forsvinder. Det ligner dog ikke, at det påvirker vikingen på nogen måde at møde disse objekter. Rent faktisk har det en effekt på vikingens tilstand. Lad os prøve at se på Viking-objektets tilstand; det gør vi ved først at sætte spillet på pause (klik på Pause-knappen), højre-klikke på vikingen, og vælge Inspect. Herved fremkommer Object Inspector vinduet:

Figur 7.3: Viking-objektets tilstand

Vi så også alle disse værdier for tilstand på Wombat-objektet, undtagen en; healthPoints. Denne værdi er specifik for et Viking-objekt, mens de andre værdier er del af den generelle Actor-klasse. Idet TVP gerne skulle udvikle sig til et lille RPG-agtigt spil, har vores figur (selvfølgelig) et antal ”livspoint”, som kan ændre sig i løbet af spillets gang. Dette er en fundamental ingrediens i næsten alle RPG-spil. Disse ”livspoint” er netop det som healthPoints repræsenterer.

19

Opgave 7.3: Start TVP fra ny, ved at klikke på Reset-knappen. Undersøg værdien af vikingens healthPoints før du begynder at flytte rundt på ham. Hvordan ændrer det vikingens tilstand at gå hen over henholdsvist et Food- eller Fire-objekt?

Det har altså en effekt at gå hen over de andre objekter.

Hvis man går hen over et Food-objekt, stiger vikingens healthPoints med 2. Hvis man går hen over et Fire-objekt, falder vikingens healthPoints med 3.

Altså har vi nu et – ekstremt simpelt – skelet til et spil. Vi kan gå rundt i en verden, møde og interagere med forskellige objekter, og se hvordan det påvirker vores tilstand.

I en nøddeskal er det jo faktisk hvad de fleste RPG-spil går ud på. I denne form skal vi nok ikke lige forvente, at TVP vil vippe de store RPG-spil spil af pinden, men der er faktisk nok funktionalitet til, at det giver mening at begynde at kigge ind under overfladen på spillet. Med andre ord skal vi nu til at se på den Java-kode, som definerer objekternes tilstand og opførsel.

7.1 Java-kode i TVP_Kap7

Første gang man kigger på program-kode, kan det virke ganske overvældende. Selv om Java er et af de nemmere programmeringssprog at forstå, skal man vide noget om sprogets opbygning (eller syntaks), før det begynder at give mening. Som nævnt i indledningen vil vi ikke gennemgå hver eneste element i Java-sproget, før vi begynder at kigge på selve koden. Det kan betyde, at der kan forekomme ting i koden, man ikke umiddelbart forstår. Bevar roen, det hele falder på plads efterhånden!

Lad os se på noget Java-kode! Vi kaster os direkte ud i koden for Viking-klassen, som er den mest interes-sante klasse her i starten. Vi kan få koden at se simpelthen ved at dobbeltklikke på Viking-kassen ude i klasse-diagrammet til højre på skærmen. Herved fremkommer et vindue a la nedenstående:

20

Figur 7.4: Kode-editor vindue i Greenfoot

Dette vindue er en kode-editor. Det betyder bare, at det er her vi kan arbejde med koden for en specifik klasse i vores scenarie. Da kode jo er tekst, er der egentlig bare tale om en almindelig tekst-editor a la NotePad, med lidt ekstra funktionalitet. Selve editoren er ikke frygtelig interessant, og fungerer stort set som de fleste tekst-editorer med mulighed for copy-paste og lignende.

Bemærk dog Compile-knappen yderst til venstre. Før computeren kan udføre de handlinger, som et Java-program definerer, skal programmet dels kontrolleres for sproglige fejl, dels oversættes til et andet sprog, som computeren kan forstå direkte. Denne proces hedder ”compilation” på engelsk, og i mangel af et bedre ord siger vi ofte ”kompilering” på dansk. Nogle gange siger man bare ”oversættelse” i stedet, men strengt taget er oversættelsen kun en del af kompilerings-processen. Der er dog sjældent grund til at skelne skarpt mellem de to begreber.

Her i første omgang skal vi dog ikke rette på koden, men blot se på den. Altså får vi heller ikke brug for at kompilere kode i starten. Vi vender tilbage til kompilerings-processen lidt senere.

7.2 Klasse-definition i Java

Som nævnt kan et stykke Java-kode se ret uoverskueligt ud i starten. Imidlertid har en Java-klasse altid en bestemt struktur, så hvis man kender denne struktur, bliver det noget nemmere at finde rundt i en Java-klasse. Som det allerførste skal det lige nævnes, at man i Java-kode kan indsætte såkaldte kommentarer, som er ment som en hjælp til den der skal læse koden. Disse kommentarer er altså ikke en del af selve koden, og er ikke noget som programmet skal udføre. Kommentarer er afmærket på en ganske bestemt måde i koden:

// Dette er en kommentar på en enkelt linie. Den skal startes med to skråstreger //(slashes). Man kan dog godt have flere linier efter hinanden

/* Dette er en kommentar på flere linier* Man kan synes dette er en lidt mærkelig måde at skrive kommentarer på,* men sådan er det altså…*/

Som sagt skal man betragte kommentarer som en hjælp, der forhåbentligt gør det nemmere at forstå selve programkoden. Hvis man ikke synes de giver nogen hjælp, kan man bare ignorere dem…

De første linier i koden for Viking-klassen er således blot nogle kommentarer om, hvad denne klasse overhovedet handler om, hvem der har skrevet den, og så videre. Dette er en meget almindelig måde at begynde en klasse på. Selve Java-koden for klassen begynder ved linierne:

public class Viking extends Actor{ ...

21

De tre prikker betyder bare ”her følger noget mere kode, som ikke er interessant lige i denne sammen-hæng…”.

Koden for enhver klasse skal indeholde en klasse-definition. En klasse-definition rummer følgende ting:

Angivelse af klassens synlighed Klassen navn Eventuelle base-klasser, som klassen arver fra

Altså skal den første linie kode læses således:

public: Denne klasse er synlig for andre klasser, og kan dermed bruges af andre klasser. Hvis man vil forhindre klassen i at være synlig for andre, benyttes ordet private. Langt de fleste klasser defineres dog til at være synlige for andre.

class Viking: Denne klasse skal hedde Viking. Der er tradition for at klasse-navne skal begynde med stort bogstav, hvilket gør det nemmere at adskille dem fra metode-navne (som traditionelt begynder med småt).

extends Actor: Denne klasse arver fra klassen Actor, hvilket angives på denne måde.

Efter denne linie følger der en krøllet parentes: {. Denne type parentes kaldes tit for en Tuborg-parentes. Disse parenteser bruges overalt i Java, til at markere at ”her kommer noget kode, der hører sammen”. Således ser ”skelettet” til definitionen af Viking-klassen således ud:

public class Viking extends Actor{ ...}

Selve koden til klassen findes således mellem start-parentesen {, og slut-parentesen }. Hvis man bladrer lidt rundt i Viking-klassen, kan man se at der bruges rigtig mange parenteser i koden. En meget typisk fejl i ens kode er at man glemmer at få ”matchet” en start-parentes med en slut-parentes…

7.3 Variabel-definition i Java

Mellem de to parenteser findes så selve klassens indmad. Denne indmad følger også en bestemt struktur.

Kode til at repræsentere tilstand Kode til at definere opførsel

Tilstand defineres – som vi allerede har set udefra – ved et antal værdier for forskellige egenskaber. Disse værdier lagres i såkaldte variable. En variabel i Java er et lille stykke hukommelse, hvor vi kan gemme en værdi, og finde den frem igen. En variabel har et navn, som vi bruger når vi skal referere til den. En variabel har også en type, som angiver hvilken slags værdier, vi kan gemme i variablen. Ofte er det tal-værdier, vi

22

lagrer i variable, men det kan også være f.eks. en tekst, eller måske et objekt. Som navnet antyder, kan værdien af en variabel ændre sig, så hvis vi på et tidspunkt får brug for at ændre på den værdi en variabel har, gør vi blot det. Denne nye værdi bliver så lagret i variablen. Bemærk, at når man lagrer en ny værdi i en variabel, overskrives den gamle værdi, og kan ikke genskabes.

I vores Viking-klasse repræsenteres vikingens tilstand her i starten af en enkelt variabel:

private int healthPoints;

Dette er altså en definition af en variabel, ligesom vi så en definition af en klasse tidligere.

En variabel-definition rummer følgende ting:

Angivelse af variablens synlighed Variablens type Variablens navn

Altså skal den ovenstående variabel-definition læses således:

private: Denne variabel er ikke synlig for andre end klassen selv. Det betyder i praksis, at hvis man laver et Viking-objekt, kan andre objekter ikke læse værdien af denne variabel. Dette kan måske virke unaturligt, men som regel gør man variable usynlige for andre, og giver eventuelt kun adgang til dem via en metode. Dette har at gøre med principper i Objekt-Orientering generelt, hvilket vi kommer ind på senere.

int: Denne variabel har typen int (hvilket betyder ”helt tal”), og kan altså kun rumme værdier som er hele tal. Værdien kan dog godt være negativ.

healthPoints: Denne variabel har navnet healthPoints

Hvis vores tilstand er mere kompleks at repræsentere, vil vi sikkert få brug for yderligere variable, men selve måden at definere variable på er den samme. Bemærk i øvrigt, at en definition af en variabel afsluttes med et semi-kolon (;). Semi-kolon bruges også rigtig meget i Java-kode, til at markere at noget er ”afsluttet”, f.eks. et kald af en metode, eller definition af en variabel. At glemme at få sat semi-kolon de rigtige steder er også en meget klassisk fejl i Java-programmering…

Til sidst skal det nævnes, at variable ikke kun bruges til at repræsentere tilstand, men også kan bruges i metoder til forskellige formål. Derfor skelner man mellem variable der repræsenterer tilstand, og andre variable. Variable der repræsenterer tilstand kaldes på engelsk for instance fields, hvilket i denne gennem-gang er oversat til instans-variable, for at holde forbindelsen til variabel-konceptet. Ordet instans skal indikere, at når man laver en ny instans af en klasse (altså et objekt af den klasse), får dette objekt sit eget sæt af variable, der repræsenterer netop dette objekts tilstand. De er altså unikke for denne instans. Vi vil senere se andre eksempler på brug af variable.

23

7.4 Metode-definition i Java

Hvor instans-variable altså repræsenterer tilstand, er en klasses opførsel defineret via de metoder, der er defineret i klassen. Vi har allerede set eksempler på metoder ”udefra”, og prøvet at aktivere dem.Nu ser vi på, hvordan en metode defineres i Java.

En metode-definition rummer følgende ting:

Angivelse af metodens synlighed Metodens returtype Metodens navn Metodens parameterliste Selve koden for metoden

Lad os som eksempel se på metoden moveNorth

public void moveNorth(){ setLocation(getX(), getY() - 1);}

Den første linie skal altså læses således:

public: Denne metode er synlig for andre klasser, og kan dermed bruges af andre klasser. Hvis man vil forhindre metoden i at være synlig for andre, benyttes ordet private. Om en metode skal være synlig eller usynlig, afhænger af dens formål. Hvis metoden skal bruges til at aktivere en bestemt opførsel, er den som regel synlig. Hvis den derimod er en slags ”hjælpemetode” til brug internt i klassen, er den som regel usynlig. Dette har at gøre med principper i Objekt-Orientering generelt, hvilket vi kommer ind på senere.

void: Denne metode har den specielle returtype void, hvilket vil sige at den faktisk ikke returnerer nogen værdi. Hvis metoden havde haft returtypen int, ville den som resultat give en værdi af typen int (helt tal).

moveNorth: Denne metode har navnet moveNorth

(): Mellem disse to almindelige parenteser angiver man eventuelle parametre til metode. Parametre er en anden slags variable, som man kan give med til metoden, i fald den har brug for nogle specifikke værdier til at udføre sin funktionalitet. Ganske som en almindelig funktion skal have noget input, for at kunne producere noget output.

Til sidst følger så selve koden for metoden, mellem de to Tuborg-parenteser. Selve koden vil være en blanding af kald af andre metoder, læsning og lagring af værdier i variable, brug af forskellige kontrol-strukturer i Java, og så videre. Det kommer vi til at se mange eksempler på. I dette tilfælde kan metoden åbenbart udføre sit job ved bare at kalde en enkelt anden metode, setLocation. Dette er en metode som er en del af Greenfoot , og som flytter en Actor til en anden position, angivet i koordinater.

Alle metoder i en klasse er skåret over denne skabelon. Dog er der en enkelt metode, som har en lidt anden struktur; den såkaldte constructor.

24

En constructor i Java (det kunne måske oversættes til ”konstruktør”, men vi bruger den engelske betegnelse her) er en metode, der kaldes i det øjeblik man laver et objekt af den type, klassen repræsenterer. Hvis vi laver et nyt Viking-objekt, vil constructor-metoden i Viking-klassen blive aktiveret på det objekt, der lige er blevet skabt. Hvorfor nu det? At have en constructor giver os mulighed for at bringe objektet i en fornuftig start-tilstand (dette kaldes også at initialisere objektet), inden nogen begynder at bruge objektet. Mere specifikt kan vi sætte alle instans-variablene til en fornuftig værdi. Gør vi ikke det, kan de i princippet få en tilfældig værdi!

En constructor adskiller sig fra almindelige metoder på et par punkter:

Den har samme navn som selve klassen Den har ingen returværdi

At returnere en værdi fra en constructor giver ikke rigtig mening, så det har man undladt at give mulighed for. Lad os se på constructoren for Viking-klassen:

public Viking(){ healthPoints = 20;}

Bemærk navnet (samme som klassen), samt manglen på en returtype. Da tilstanden af en Viking kun er defineret ved instans-variablen healthPoints, er det altså blot denne variabel, der skal sættes til en fornuftig værdi (vi sætter den til 20). Hvis vi synes at en Viking skal have noget mere liv til at starte med, er det altså her vil skal rette. Bemærk, at et enkelt lighedstegn faktisk ikke betyder ”lig med”, men ”sæt lig med”. Det er en lille, men vigtig forskel, som vender tilbage til senere.

7.5 Opbygningen af Viking-klassen

Nu skulle vi være nogenlunde i stand til at læse (og forstå) de metoder, der findes i Viking-klassen. Vi har allerede set den første metode, nemlig constructuren. Vi gennemgår de andre metoder i det følgende. Bemærk, at koden i Greenfoot rummer kommentarer – disse er fjernet her, for at holde koden rimeligt ren og kompakt. Når man senere læser koden direkte i Greenfoot, kan man støtte sig til kommentarerne.

25

7.5.1 act metoden

public void act() { move();

if (meetsObject()) { interactWithObject(); }

if (isDead()) { doDeathBehavior(); } }

Metoden act() er den centrale metode i Viking-klassen, idet det er den metode, der bliver kaldt igen og igen, når programmet køres (ved at trykke på Run-knappen). Koden i metoden falder naturligt i tre dele:

1. Flyt vikingen2. Hvis vikingen møder et objekt, så interagér med objektet3. Hvis vikingen er død, så udfør ”døds-opførsel”

Første bid er ret simpel, idet den udføres ved at kalde en anden metode, move(). I denne metode findes detaljerne for, hvordan vikingen skal flytte sig. Man kan spørge sig selv, hvorfor vi dog ikke bare skriver disse detaljer her. Generelt prøver man at holde metoder rimeligt korte, idet de ellers bliver meget svære at overskue. Hvis metoden begynder at blive meget lang, flytter man som regel noget af koden ud i nye metoder, som man så kalder fra den oprindelige metode.

Anden bid er lidt mere kompleks:

if (meetsObject()) { interactWithObject(); }

Her møder vi for første gang en såkaldt ”kontrol-struktur” i Java; en if-sætning.

26

7.5.2 If-sætningen

En if-sætning gør det muligt at sørge for, at noget kode kun udføres, hvis en betingelse er opfyldt. Helt generelt er strukturen for en if-sætning:

if (betingelse er sand) { (kode som skal udføres) }

Det som er inden i den første parentes er et såkaldt logisk udtryk. Det lyder farligt fint, men er bare en slags matematisk udsagn, som kan være enten sandt eller falsk (i Java bruges true eller false). I Java findes der en type, som netop passer til dette formål; en boolean.

En variabel af typen boolean er en variabel som kun kan have en af to mulige værdier; true eller false. Man kunne f.eks. skrive

private boolean isDead = false;

Hermed laver man en variabel isDead af typen boolean, og sætter dens værdi til false.

Hvis vi vender tilbage til if-sætningen, vil den allersimpleste betingelse vi kan konstruere være at indsætte værdien true eller false direkte i parentesen:

if (false) { (kode som skal udføres) }

Det vil imidlertid være ret uinteressant – koden mellem Tuborg-parenteserne udføres kun, hvis betingelsen er sand (true), og når man har indsat værdien false direkte, vil det jo aldrig ske… Så er der jo ikke nogen grund til at bruge en if-sætning.

I praksis vil man derfor have noget mere kompliceret som sin betingelse, f.eks. en variabel:

if (isDead) { (kode som skal udføres) }

Dette giver lidt mere mening. Når vi når frem til if-sætningen, har vi sikkert udført noget kode, som har sat variablen til enten true eller false, alt efter hvordan spillet er forløbet indtil nu. Så nogle gange vil koden blive udført (når isDead er true), andre gange ikke (når isDead er false).

Ser vi tilbage på koden fra TVP, var det jo ikke en variabel vi havde som betingelse, men derimod et kald af en metode, meetsObject(). Hvordan nu det!? Husk på, at en metode – ganske ligesom en variabel – jo også har en type, nemlig returtypen. En metode kan således sagtens have boolean som sin returtype, og dermed

27

returnere true eller false. Dermed giver det ganske fin mening at have et metodekald som betingelse i en if-sætning, blot metoden har returtypen boolean.

Dermed skulle vi være i stand til at forstå den lille stump kode fra TVP, som vi lige gentager her:

if (meetsObject()) { interactWithObject(); }

Metoden meetsObject() afgør, om vikingen møder et objekt i sin nuværende position. Hvis det er tilfældet, vil meetsObject() returnere true, hvorved metoden interactWithObject() vil blive kaldt. I sidstnævnte metode foretages selve interaktionen mellem vikingen og det objekt, vikingen møder.

Den sidste stump kode i act-metoden har samme struktur:

if (isDead()) { doDeathBehavior(); }

Igen en if-sætning, hvor vi kalder en metode isDead(), der derfor må returnere en boolean. Metoden afgør, hvorvidt vikingen nu er død eller ej. Hvis vikingen vitterligt er død, kaldes metoden doDeathBehavior(), som rummer den opførsel vikingen udfører, hvis den dør. I et computer-spil vil den figur man styrer som regel udføre en eller anden ”døds-rutine”, hvis den dør. Det kan være kombination af sjov eller dramatisk grafik, en karakteristisk lyd, et GAME OVER skilt, eller hvad man nu synes er passende.

Lad os runde act-metoden af med at beskrive dens funktion med almindelige ord. Først foretager vi en flyt-ning af vikingen. Som en konsekvens af dette møder vi måske et andet objekt i verden; hvis det er tilfældet, interagerer vi med det. Som en konsekvens af denne interaktion kan vikingens tilstand ændre sig, måske i en grad så den – alt efter vores definition – må betragtes som død. Hvis det er tilfældet, udfører vi en ”døds-rutine”, som gør spilleren opmærksom på, at vikingen er død…

28

7.5.3 Move metoden

private void move() { if (Greenfoot.isKeyDown("down")) // Er "pil ned" trykket ned? { moveSouth(); }

if (Greenfoot.isKeyDown("up")) // Er "pil op" trykket ned? { moveNorth(); }

if (Greenfoot.isKeyDown("right")) // Er "pil til højre" trykket ned? { moveEast(); }

if (Greenfoot.isKeyDown("left")) // Er "pil til venstre" trykket ned? { moveWest(); } }

Denne metode består af fire if-sætninger, med meget ens struktur. Den overordnede funktionalitet i metoden er at undersøge, hvorvidt en af de fire pile-taster er trykket ned, og i givet fald foretage en bevæg-else i den tilsvarende retning, ved at kalde en af de fire move…() metoder. De fire pile-taster benævnes i Greenfoot med ”down”, ”up”, ”left” og ”right”, og betydningen burde være indlysende. Det mest interes-sante her er den metode, der kaldes i betingelsen for hver if-sætning:

Greenfoot.isKeydown(”up”)

Dette er en metode, som er ”indbygget” i Greenfoot. Det betyder lidt mere specifikt, at den er defineret i en klasse ved navn Greenfoot, som er en klasse der følger med Greenfoot-programmet. Det er altså ikke en klasse vi selv har lavet.

Når metoden bruges som betingelse i en if-sætning, må den have retur-typen boolean. Ydermere tager metoden en parameter, nemlig det lille stykke tekst, der i eksemplet er ”up”. Tekst angives i Java altid i anførselstegn. Denne parameter angiver, hvilken af de fire piletaster der skal være trykket ned, for at metoden returnerer true, og betingelsen derved er opfyldt.

Den måde metoden skrives på, ser en smule mærkelig ud. Hvorfor skriver vi Greenfoot, efterfulgt af et punktum, efterfulgt af selve metodens navn? Først og fremmest skyldes det, at isKeyDown ikke er en metode i Viking-klassen, men derimod en metode i Greenfoot-klassen. Ydermere er netop denne metode en såkaldt statisk metode, hvilket gør at vi anvender denne specielle syntaks. Lad os lige dvæle lidt ved, hvordan metoder fra andre klasser bruges.

7.5.4 Kald af metoder fra andre klasser

29

Indtil videre har vi blot angivet en metodes navn, når vi skulle bruge den. Det har vi kunnet gøre, fordi metoden har været defineret i den klasse vi arbejder med (Viking) eller i dens base-klasse (Actor). Når vi skal bruge metoder fra andre klasser, skal der lidt mere til.

Den mest normale situation er den, hvor man skaber et nyt objekt af en eller anden klasse, og derefter ønsker at bruge en metode på dette objekt. Det kunne se således ud:

Viking harald;

harald = new Viking();

harald.moveWest();

I disse tre linier kode sker der følgende:

1. Vi definerer en variabel harald, af typen Viking. Variable kan sagtens have en klasse som sin type, på lige fod med mere simple typer som int og boolean. Bemærk, at når vi definerer en variabel midt inde i en metode, behøver vi ikke angive synlighed (private eller public), idet variablen altid vil være usynlig for andre klasser.

2. Vi laver et nyt Viking-objekt, og sætter variablen harald lig med dette nye objekt. Når man skriver new efterfulgt af et klassenavn, skaber vi netop et nyt objekt af denne klasse, og constructoren for klassen bliver kørt på objektet.

3. Vi kalder metoden moveWest på objektet, som variablen harald er lig med. Vi kunne jo have skabt flere Viking-objekter, og da de jo er selvstændige objekter, bliver vi nødt til at specificere præcist hvilket Viking-objekt, vi vil udføre metoden på. Derfor skriver vi harald.moveWest(), og ikke Viking.moveWest(). Syntaksen med at angive objektets navn (harald), efterfulgt af et punktum, efterfulgt af metodens navn, er den almindelige syntaks for at kalde en metode på et objekt.

Men er det ikke i direkte modstrid med det, vi lige har set i move-metoden? Der skrev vi jo netop bare Greenfoot.isKeyDown(”up”), uden at lave et Greenfoot-objekt først, og så kalde metoden på det objekt? Det skyldes den specielle omstændighed, at metoden isKeyDown er en såkaldt statisk metode i Greenfoot-klassen. En statisk metode kræver ikke et objekt at blive kaldt på, men kan kaldes bare ved at angive klassen navn i stedet for navnet på et objekt. Sådanne metoder udfører typisk noget, som ikke er afhængigt af tilstanden af et specifikt objekt. At checke om en piletast er trykket ned kan jo ikke rigtig afhænge af et specifikt objekt, så derfor er denne metode defineret til at være en statisk metode.

7.5.5 Metoderne move…()

Vi så i move-metoden, at der er defineret fire mere specifikke move-metoder; en for hver retning. Vi ser blot på en enkelt af dem her, idet de er meget ens i deres opbygning:

public void moveNorth() { setLocation(getX(), getY() - 1); }

30

Det eneste metoden gør er at kalde setLocation, som er en metode defineret i Actor-klassen. Et kald til setLocation vil ændre objektets position i verdenen, til den celle som er angivet ved en x- og en y-koordinat. Hvis vi vil flytte vikingen mod nord, betyder det i praksis at:

Vi vil bevare den nuværende position i x-retningen (horisontalt) Vi vil flytte én position opad i y-retningen (vertikalt). Bemærk i den forbindelse, at koordinat-

systemet for cellerne starter i øverste venstre hjørne, og ikke i nederste venstre hjørne, som man nok ville forvente. Så jo længere ned på skærmen vi kommer, jo højere y-værdi…

Med andre ord skal den nye position for objektet findes relativt i forhold til den nuværende position. Det er derfor vi kalder de to metoder getX() og getY(). Disse to metoder – som også er defineret i Actor-klassen – giver os objektets nuværende position, i form af en x- og y-koordinat. Derved er det ret nemt at udregne de nye koordinater:

Ny x-koordinat = nuværende x-koordinat Ny y-koordinat = nuværende y-koordinat minus 1

Man kunne godt have lavet koden lidt mere ”eksplicit” ved f.eks. at have defineret to nye variable for de to nye koordinater, udregne de nye værdier, og så give disse variable som parametre til setLocation:

public void moveNorth() { int newX = getX(); int newY = getY() - 1; setLocation(newX, newY); }

Denne stump kode gør præcis det samme som den oprindelige stump, men er formuleret lidt anderledes. Hvad er bedst? Det er der ikke noget simpelt svar på. Den korte formulering er kort (!), og bruger mindre lagerplads end den lange formulering, som til gengæld kan være lidt lettere at læse.

7.5.6 meetsObject metoden

Denne metode skal afgøre, hvorvidt vikingen i sin nuværende position ”møder” et andet objekt i verdenen:

private boolean meetsObject() { Actor anObject = getOneObjectAtOffset(0, 0, Actor.class); return (anObject != null); }

Først bemærker vi, at metoden har retur-typen boolean, hvilket er meget naturligt, når den bliver brugt i en betingelse (i act-metoden)

I den første linie sker der en del ting. Først definerer vi en variabel anObject af typen Actor. Denne variabel sættes så lig med returværdien fra metoden getOneObjectAtOffset, som åbenbart må have returtypen

31

Actor. Ligesom variable kan have en klasse som type, kan en metode have en klasse som returtype. Altså vil et kald til getOneObjectAtOffset returnere et objekt af typen Actor.

Hvad gør getOneObjectAtOffset så egentlig? Denne metode – som er defineret i Actor – kan undersøge, om der findes et andet objekt i verdenen, på en position angivet relativt i forhold til objektet selv. Hvilken position vi vil undersøge angives med de to første parametre, som altså er x-og y-koordinaten angivet relativt i forhold til vikingen selv. I dette tilfælde er det ret enkelt, idet vi vil undersøge, om der er et andet objekt på selvsamme position som vikingen. Derfor er både x og y sat til 0. Den sidste parameter angiver, hvilken type af objekt vi vil undersøge for. Dette angives ved klassens navn, efterfulgt af ”.class”. I dette tilfælde vil vi undersøge, om der overhovedet er et andet Actor-objekt på positionen. Vi er ligeglade med, om det er et Fire- eller Food-objekt. Bemærk, at Viking-objektet selv ikke ”regnes med”, selv om det jo er en Actor, og netop er på sin egen position…

Hvis der vitterligt er et Actor-objekt på positionen, vil getOneObjectAtOffset returnere dette, og anObject bliver sat lig med dette objekt. Hvad nu, hvis der ikke er noget objekt? Hvad skal getOneObjectAtOffset så returnere? I det tilfælde benytter man den specielle værdi null. Som navnet antyder, er der tale om en slags nul-værdi. null betyder, at ”her burde være et objekt, men det er der ikke”. Der er dog ikke tale om en fejl i sig selv, men blot det faktum, at der ikke er noget objekt at returnere. En variabel der har en klasse som sin type kan ligeledes sættes lig med null, så der er ikke noget problem i at getOneObjectAtOffset nogle gange vil returnere null. Det betyder bare, at anObject også bliver lig med null.

I den sidste linie finder vi ud af, hvad vi skal returnere som resultat af metoden. Dette angives med ordet return, efterfulgt af den værdi der returneres. I dette tilfælde skal der returneres en boolean, altså enten true eller false. Det som står i parentesen skal derfor være et logisk udtryk. Et ret simpelt logisk udtryk er at undersøge, hvorvidt en variabel har en bestemt værdi. Hvis vi f.eks. havde defineret en variabel alder af typen int, kunne sådan en undersøgelse se således ud:

(alder < 20)

Hvis variablen alder har en værdi mindre end 20, vil dette logiske udtryk blive sandt (true), ellers vil det blive falsk (false).

Hvad nu, hvis vi vil undersøge om alder har en helt præcis værdi, f.eks. 20. Her kunne man i første omgang tro, at man så skulle skrive

(alder = 20)

Det er imidlertid ikke korrekt! Det man egentlig prøver her er at sætte alder lig med 20, hvilket er det som et enkelt lighedstegn betyder i Java. Vi kan derfor ikke bruge denne syntaks til sammenligning, idet den allerede er ”optaget”. I stedet skriver man

(alder == 20)

Dette er syntaksen for at undersøge, om to værdier er lig med hinanden. De to lighedstegn efter hinanden betyder altså sammenligning, hvor et enkelt lighedstegn betyder ”sæt lig med”. Dette er endnu en klassisk kilde til fejl, men heldigvis bliver man gjort opmærksom på dette, når man kompilerer sit program. Der er en lignende syntaks for at undersøge, om to værdier er forskellige fra hinanden:

32

(alder != 20)

Sammenligning kan udføres mellem alle typer, også mellem objekter. Dog skal det være den samme type objekt på hver side af lighedstegnet. Det er præcis det vi gør i den sidste linie i meetsObject. Vi er jo ude efter at finde ud af, om vikingen møder et objekt. Det er sandt, hvis ikke kaldet af getOneObjectAtOffset returnerer null. Derfor skal returværdien fra meetsObject netop være værdien af det logiske udtryk:

(anObject != null)

7.5.7 interactWithObject metoden

Denne metode skal definere selve den opførsel, vikingen skal udføre hvis han møder et objekt.

private void interactWithObject() { Actor anObject = null;

anObject = getOneObjectAtOffset(0, 0, Fire.class); if (anObject != null) { interactWithFire(anObject); return; }

anObject = getOneObjectAtOffset(0, 0, Food.class); if (anObject != null) { interactWithFood(anObject); return; } }

Vi ved på dette tidspunkt, at vikingen rent faktisk har mødt et Actor-objekt. Nu skal vi finde ud af, hvilken type denne Actor har. På dette tidspunkt kan det være enten et Fire-objekt eller et Food-objekt. Vi laver os derfor igen en variabel anObject af typen Actor, og kalder igen getOneObjectAtOffset. Bemærk, at vi nu er mere specifikke, i forhold til hvilken type objekter vi søger efter. Hvor den sidste parameter til getOne-ObjectAtOffset før var Actor.class, er den nu i det første kald sat til Fire.class. Med andre ord vil vi nu undersøge, om det er et Fire-object, vikingen har mødt. Hvis det er tilfældet, vil kaldet returnere et objekt, og vi vil gå ind i den første if-sætning:

interactWithFire(anObject); return;

Her kalder vi en metode interactWithFire, som åbenbart definerer den opførsel vikingen skal udvise, når han interagerer med et Fire-objekt. Bemærk, at Fire-objektet gives som en parameter til interactWithFire, for vi skal nemlig bruge dette objekt til noget i den metode…

Den sidste linie i if-sætningen hedder bare return;. Selv om en metode ikke har en returværdi (interactWith-Object har jo returværdien void), kan man godt benytte return alligevel. Det betyder bare, at nu er metoden

33

færdig, og programmet kan returnere til det sted som metoden blev kaldt fra. I dette tilfælde er der jo ikke mere at gøre efter kaldet til interactWithFire er afsluttet – et objekt kan jo ikke både være et Fire-objekt og et Food-objekt, så hvis vi er gået ind i den første if-sætning, kommer vi med garanti ikke ind i den anden if-sætning. Derfor kan metoden godt returnere med det samme.

Den anden del af interactWithObject er opbygget på samme måde, blot testes der om det er et Food-objekt vikingen har mødt. I så tilfælde kaldes metoden interactWithFood.

7.5.8 InteractWithFire og –Food metoderne

Disse to metoder definerer helt konkret, hvordan vikingen interagerer med henholdsvis Fire- og Food-objekter. Metoden interactWithFire ser således ud:

private void interactWithFire(Actor fireObject) { getWorld().removeObject(fireObject); healthPoints = healthPoints - 3; }

To ting skal ske, når vikingen interagerer med et Fire-objekt.

Fire-objektet skal fjernes fra verdenen, idet vi definerer at det ”opbruges” ved en interaktion. Bemærk, at dette er vores valg som spil-designere, ikke noget som er et krav i forhold til Greenfoot.

Vikingens livs-point skal reduceres med 3, idet vi – som spil-designere – definerer at interaktion med ild er skadeligt for vikingen. Selv seje vikinger med Total Viking Power er ikke (helt) immune overfor ild.

Vi fjerner objektet – som jo meget bekvemt er til rådighed som en parameter til metoden – ved at kalde metoden removeObject, som netop tager et objekt som parameter. Denne metode er defineret i klassen World, som TVPWorld arver fra. For ethvert Actor-objekt gælder det, at man via metoden getWorld kan få fat i den verden, objektet lever i. Mere specifikt returnerer getWorld altså et objekt af typen World, som vi så kan kalde removeObject på.

Den sidste linie ændrer værdien af instans-variablen healthPoints til 3 mindre end dens nuværende værdi. Bemærk, at man godt kan bruge en variabel på begge sider af lighedstegnet, når man regner den nye værdi af variablen ud. Husk at lighedstegn i denne forbindelse ikke betyder ”lig med”, men betyder ”sæt lig med”.

Metoden interactWithFood er opbygget på helt tilsvarende måde, blot øges vikingens livs-point nu med 2. Mad er godt, ild er skidt!

7.5.9 Metoderne isDead og doDeathBehavior (første version)

34

De sidste to metoder isDead og doDeathBehavior i scenariet TVP_0001 er kun defineret, men har ikke nogen reel opførsel endnu. isDead har returtypen boolean, så derfor skal den returnere en værdi. Lige nu returnerer den altid værdien false – en viking dør aldrig! doDeathBehavior gør lige nu slet ingenting…

Opgave 7.4: Find på nogle fornuftige definitioner af de to metoder isDead og doDeathBehavior. isDead er nok rimeligt nem, mens doDeathBehavior kan gøres mere kompleks. Måske kunne man ændre grafikken for vikingen, afspille en lyd, med videre. For at se hvilke metoder der er til rådighed i Greenfoot, vælger man menupunktet Help | Greenfoot Class Documentation. Hvis man leder lidt, finder man metoder til f.eks. de ovenstående forslag. Tænk lidt, før du leder. Hvor vil det f.eks. være naturligt at definere en metode, der kan ændre grafikken for et Actor-objekt?

7.6 Scenariet TVP_Kap7_Opg7_4_efter

Vi fortsætter nu fra det fælles udgangspunkt TVP_Kap7_Opg7_4_efter. Dette scenarie rummer en løsning på Opgave 7.4, i form af kode for de to metoder isDead og doDeathBehavior:

private boolean isDead() { return (healthPoints < 0); }

private void doDeathBehavior() { setImage("cross_25.jpg"); Greenfoot.playSound("death.wav"); }

NB: Idet vi kommer til at benytte mange forskellige scenarier i løbet af dette forløb, benyttes der en navngivning af scenarier, som måske kan se indviklet ud ved første øjekast. Navnet på scenariet TVP_Kap7_Opg7_4_efter skal læses således: Scenariet hørende til kapitel 7, efter at opgave 7.4 er blevet løst. Hvis man ikke kan finde ud af at løse en opgave, kan man således altid hente et scenarie, som rummer en løsning på opgaven; disse scenarier ender altid på _efter. Tilsvarende kan der være situationer, hvor man af den ene eller anden grund ikke har et scenarie ”klar” til den næste opgave, som skal løses. I så tilfælde vil man kunne hente et scenarie, som er klar til at løse den givne opgave, d.v.s. alle forudsætninger i scenariet er på plads. Sådanne scenarier vil ende på _klar.

Meget naturligt har vi implementeret isDead ved simpelthen af checke, om healthPoints er negativ. Hvis det er tilfældet, betragtes vikingen som værende død, og isDead vil returnere true.

Metoden doDeathBehavior kan naturligvis implementeres på mange måder. I denne løsning afspilles en lyd, mere specifikt filen death.wav. Desuden ændres grafikken for vikingen til et kors (filen cross_25.jpg). Altså gøres brugeren opmærksom på vikingens død både ved brug af lyd og billede.

35

Vil man forvente andre ændringer i opførsel, når vikingen er død? Først og fremmest vil man nok forvente, at vi ikke længere kan flytte rundt med vikingen. Husk på, at selv om vi har ændret grafikken for vikingen, og metoden isDead returnerer true, bliver metoden act stadig kaldt på Viking-objektet. Det er derfor nødvendigt med en lille ændring af act-metoden:

public void act() { if (!isDead()) {

// Her er den oprindelige kode i act-metoden } }

Med andre ord udføres al den oprindelige kode i act-metoden kun hvis vikingen stadig er levende.

Hvis man ser på klasserne under Actor-klassen, vil man se at klassen Wall er kommet til. Denne klasse skal bruges til at tilføje vægge til vores verden. Dette leder os hen til at kigge nærmere på klassen TVPWorld, som er den klasse hvori vores verden konstrueres.

7.7 Klassen TVPWorld

Vi har i det ovenstående undersøgt Viking-klassen i meget stor detalje. Nu er tiden kommet til at se nær-mere på TVPWorld-klassen, som rummer logikken for konstruktion af den verden, vores viking bevæger sig rundt i.

TVPWorld er væsensforskellig fra Viking. Først og fremmest arver den ikke fra Actor, men i stedet fra World, som er den anden af de base-klasser, der følger med Greenfoot.

Lad os se nærmere på den første bid af TVPWorld-klassen. Vi burde jo allerede nu være i stand til at læse og forstå selve klasse-definitionen, så den dvæler vi ikke for meget ved:

public class TVPWorld extends World{ private Random generator;

boolean[][] cellIsUsed;

private static final int cellCount = 32; private static final int cellSize = 25; private static final int fireObjects = 50; private static final int foodObjects = 80; // Klasse-definitionen fortsætter her

Allerede i denne bid kode er der flere nye ting. Denne klasse har åbenbart flere end én instans-variable, og den første af disse har typen Random…? Typen Random er faktisk en klasse, der er defineret i Java-sproget. Objekter af denne type kan benyttes til at frembringe tilfældige (eng.: random) tal, og det får vi netop brug for i denne klasse. Derfor har vi en instans-variabel af denne type.

36

Den næste instans-variabel celIsUsed ser også lidt mystisk ud, idet den åbenbart har typen boolean[][]. Vi har jo hørt om typen boolean før (kun to mulige værdier: true eller false), så hvad betyder de efterfølgende to par kantede parenteser? I Java har man meget ofte brug for at kunne repræsentere ikke bare én variabel af en bestemt type, men en hel mængde variable af samme type. Det kan man gøre ved hjælp af et array, som vi nu ser lidt nærmere på.

7.7.1 Brug af arrays i Java

Lad os forestille os, at vi vil lave en klasse, som skal holde styr på en masse måleresultater fra et eksperi-ment. De enkelte måleresultat er bare et helt tal, men vi får brug for at kunne holde styr på f.eks. 100 måleresultater. Det kan man godt gøre med variable som vi kender dem, men det er noget klodset:

public class ExperimentManager{ private int measurement1; private int measurement2; private int measurement3; // Og så videre… private int measurement100; ...}

Det er mildest talt en dødssyg måde at holde styr på 100 tal på, og det vil være meget besværligt at arbejde med i praksis. Det ville være meget nemmere at putte alle tallene ind i en slags liste af tal. Det er netop det, et array er.

37

Hvis vi definerer en variabel på denne måde:

private int[] allMeasurements;

er dens type en liste (på engelsk array) af variable af typen int. Bemærk, at vi ikke på dette tidspunkt udtaler os om, hvor stor denne liste skal være. Det skal vi imidlertid gøre, før vi begynder at bruge listen, idet programmet skal afsætte plads i hukommelsen til listen. Dette gøres i klassens constructor:

public ExperimentManager() { allMeasurements = new int[100]; }

Det kan virke lidt mærkeligt, at man ikke definerer størrelsen af listen med det samme, men denne måde giver også lidt mere fleksibilitet. Vi kunne faktisk give størrelsen på listen med som en parameter til constructoren, så den ikke altid var præcis 100 elementer lang.

Når vi nu har fået initialiseret vores liste, kan vi begynde at bruge elementerne. Hvert element i listen fungerer faktisk som en selvstændig variabel, blot skal man angive et såkaldt index, når man vil have fat i et bestemt element i listen. Det kan se således ud:

allMeasurements[27] = 440;

Denne linie skal altså læses som ”sæt element nummer 27 i listen lig med 440”

En faldgrube – og endnu en klassisk kilde til fejl i Java-programmer – er at det første element i sådan en liste ikke findes ved brug af index nummer 1, men derimod index nummer 0. Man tæller altså fra nul og opefter. I ”den anden ende” af listen er det også nemt at blive snydt. Det ville være meget naturligt hvis det sidste element en liste af længde 100 havde index 100, men det er ikke tilfældet…det har index 99. Rent logisk er det jo helt i orden; hvis man starter med at tælle fra 0, og listen har længde 100, når man jo kun op til 99. I praksis er det dog meget let at glemme dette, og hvis man rent faktisk prøver at bruge 100 som index i denne liste, vil man få en fejl fra programmet.

Bortset fra disse små spidsfindigheder er en liste dog en ekstremt nyttig type, som man bruger igen og igen i Java-programmer. Der findes andre såkaldte data-strukturer til at holde styr på mange variable af samme type, men dem kommer vi ikke ind på nu.

Vi er dog ikke helt færdige. Den variabel cellIsUsed vi så på i TVPWorld-klassen så jo lidt anderledes ud. Hvis cellIsUsed havde haft typen boolean[], var det nok til at regne ud; en liste af variable af typen boolean. Men cellIsUsed har jo typen boolean[][]…hvad betyder det så? Det er sådan set ”bare” en måde at gå en dimen-sion højere op på. I nogle tilfælde har vi ikke brug for en liste, men mere en ”liste af lister”, altså en tabel. Hvor en liste jo er en en-dimensionel struktur, er en tabel en to-dimensionel struktur. Alt efter hvilket formål variablen tjener, kan det passe bedre at vælge en tabel frem for en liste. I denne sammenhæng skal cellIsUsed bruges til at holde styr på, om en given celle i vores verden er optaget. Vi har jo netop en to-dimensionel, kvadratisk verden, så derfor er det mest naturligt at bruge en tabel. Hvis man vil have fat i et bestemt element i tabellen, skriver man f.eks.

38

cellIsUsed[6][8] = true;

Dette skal læses som ”cellen med koordinaterne 6,8 sættes til at være optaget”.

Afsluttende skal det nævnes, at vi kan have så mange dimensioner vi har lyst til, når vi definerer et array. Vi kan godt skrive:

private int[][][][] allMeasurements4D;

hvis det giver mening i en given sammenhæng. Igen skal vi blot angive koordinaterne, når vi skal have fat på et specifikt element.

7.7.2 Konstanter

De næste fire instans-variable er også lidt specielle. De ser alle ud som noget a la:

private static final int fireObjects = 50;

Det ligner jo lidt en definition af en almindelig instans-variabel, men der er noget mere med. Det første er ordet static. I Java kan man definere, at en metode eller en variabel ikke hører til de enkelte objekter af klassens type, men derimod er noget som er fælles som klassen som sådan. Med andre ord vil hvert objekt af typen TVPWorld ikke have sig egen instans af variablen fireObjects.

Hvorfor vil man vælge at gøre noget til en static (dansk: statisk) metode eller variabel? Med hensyn til metoder kan det være at metoden har en funktionalitet, der egentlig ikke rigtig knytter sig til et specifikt objekt. For eksempel brugte vi tidligere metoden playSound, der er defineret som en statisk metode i klassen Greenfoot, der rummer forskellige bekvemme metoder. playSound har ikke rigtig noget at gøre med et specifikt objekt; det er bare noget, vi gerne vil have programmet til at gøre. Derfor er den gjort til en statisk metode. Det har den fordel, at vi ikke behøver lave et Greenfoot-objekt for at kalde metoden. I stedet skriver vi bare:

Greenfoot.playSound("death.wav");

Denne linie skal læses som “kald den statiske metode playSound, som er defineret i klassen Greenfoot”

For variable er argumentet nogenlunde det samme. Vi kan have noget data, der ikke rigtig knytter sig til et specifikt objekt, men alligevel naturligt hører sammen med klassen. For vores ExperimentManager klasse kunne vi måske have brug for at gemme tidspunktet for den seneste måling. Denne oplysning har noget med eksperimentet som sådan at gøre, men ikke noget med den enkelte måling at gøre.

De fire instans-variable fra TVPWorld-klassen rummer imidlertid også ordet final. Ved at angive ordet final angiver man, at denne variabel faktisk ikke skal betragtes som en varibel, men derimod det modsatte; en konstant. En konstant kan kun have én værdi, nemlig den værdi som angives sammen med definitionen af variablen, f.eks.

private static final int fireObjects = 50;

39

Dette skal således læses som “variablen fireObjects sættes til 50, og denne værdi kan ikke ændres”.

Hvorfor vil man dog lave en variabel, for derefter at give den en værdi, der ikke kan ændres? Man skal ikke tænke på en konstant som en ”frosset” variabel, men derimod som et slags ”alias” for en konkret værdi. Dette alias kan så benyttes andre steder i koden, hvor man har brug for at benytte denne værdi. Og hvorfor så det? Kan man ikke bare skrive værdien direkte i koden, frem for at spilde en variabel på det? Det kunne man i princippet godt, men det vil gøre koden sværere at læse og vedligeholde.

Lad os sige, at værdien for konstanten fireObjects skal benyttes mange steder i koden, og alle disse steder beslutter vi at skrive 50 direkte, i stedet for at skrive fireObjects. Et eller andet sted i koden laver vi måske en sammenligning mellem en variabel til optælling (f.eks. kaldet counter), og så konstanten. Hvilke af disse to stykker kode er mon nemmest at fortså, når man et år senere prøver at læse koden igen?

(fireObjects < counter)

eller

(50 < counter)

Det er selvfølgelig det øverste stykke. Konstantens navn skulle gerne gøre det nemmere at forstå, hvad formål konstanten egentlig har.

Den anden fordel er relateret til vedligeholdelse. Lad os sige vi pludselig beslutter, at værdien af fireObjects skal være 70 i stedet for 50. Hvis vi benytter en konstant, er det klaret i et snuptag:

private static final int fireObjects = 70;

Med andre ord skal vi kun rette dette ene sted i koden! Alle andre steder i koden bliver der jo refereret til konstanten, ikke til værdien selv. Hvis vi derimod bare havde skrevet værdien direkte i koden, skulle vi rette den alle disse steder, hvilket klart forøger risikoen for at glemme at rette den et eller andet sted. Man kan selvfølgelig prøve med noget søg-og-erstat, men hvad nu hvis der står 50 andre steder i koden, hvor det har en helt anden betydning? Brug af konstanter fjerner dette problem, og fjerner såkaldte ”magiske tal” fra koden. I kode-jargon er et ”magisk tal” netop en værdi skrevet direkte i koden, som man ikke umiddelbart kan finde betydningen af.

7.7.3 TVPWorld constructoren

Constructoren i TVPWorld-klassen er ret vigtig, idet den skal stå for at ”befolke” vores verden med passende Actor-objekter. Den ser således ud:

public TVPWorld() { super(cellCount, cellCount, cellSize);

generator = new Random(); cellIsUsed = new boolean[cellCount][cellCount];

populateTVPWorld(); }

40

Den første linie er et kald til baseklassens constructor. TVPWorld arver fra World, og i constructoren for World skal man åbenbart angive, hvor stor ens verden er. Dette angives som antallet af celler i x-retningen, antallet af celler i y-retningen, og cellens størrelse i pixels (mere præcist kantlængden i pixels på hver af de kvadratiske celler). Ordet super bruges altså her til at angive, at man kalder baseklassens constructor, uden at man behøver nævne den ved navn. Bemærk i øvrigt hvorledes vi bruger konstanter som parametre til baseklassens constructor, i stedet for at angive værdier direkte.

De næste to linier er initialisering af de to ”rigtige” instans-variable. Husk, at når en variabel har en eller anden klasse som sin type (et array betragtes som en klasse), skal man altid initialisere variablen ved at lave et nyt objekt af klassens type, og sætte variablen lig med dette nye objekt. Bemærk igen hvordan vi bruger konstanter til at angive tabellens (cellIsUsed) konkrete størrelse.

Den sidste linie er blot et kald til en anden metode defineret i TVPWorld; populateTVPWorld.

7.7.4 populateTVPWorld metoden

I denne metode har vi samlet logikken til at få befolket (eng.: populate) vores verden med Actor-objekter af forskellige typer. Metoden ser således ud:

public void populateTVPWorld() { initialiseCellIsUsed(); populateWall(); populateFire(); populateFood(); populateViking(); }

Metoden rummer blot kald til andre metoder i TVPWorld, så den er ikke så spændende i sig selv. Vi kan dog læse ud af metoden, at det at befolke TVPWorld åbenbart går ud på at sætte Wall-, Fire- og Food-objekter ind i verdenen, og til sidst et Viking-objekt. P.t. sættes der dog ikke Wall-objekter ind i verdenen, men det vender vi tilbage til…

Den næste metode er initialiseCellIsUsed:

private void initialiseCellIsUsed() { for (int x = 0; x < cellCount; x++) for (int y = 0; y < cellCount; y++) cellIsUsed[x][y] = false; }

41

Denne metode rummer en konstruktion vi ikke har set før; en for-løkke. Den vender vi tilbage til lige om lidt, men hvad er metodens formål i sig selv? Vi så i constructoren, at vi initialiserede cellIsUsed til en tabel (eller to-dimensionelt array, om man vil), hvor hvert element har typen boolean. Vi skal imidlertid også huske at initialisere hvert element til en fornuftig værdi. Formålet med tabellen er at holde styr på, om en given celle er ”optaget” af et Actor-objekt. Da vi ikke er begyndt på at indsætte objekter i verdenen endnu, må det være rimeligt at sætte værdien af alle elementer til false. Dette kan gøres mest bekvemt med en for-løkke.

7.7.5 for-løkker i Java

For ganske kort tid siden lærte vi om arrays, som er en meget nyttig type til at håndtere en mængde af variable med samme type. Den er dog ikke så meget værd, hvis vi ikke har en fornuftig måde at behandle sådan en mængde på. Hvis vi igen ser på vores allMeasurements array fra tidligere, kunne vi måske have brug for at skrive værdien af alle målingerne ud. Det kunne gøres således:

private void printMeasurements() { print(allMeasurements[0]); print(allMeasurements[1]); print(allMeasurements[2]); // Og så videre… print(allMeasurements[99]); }

Ikke lige den mest elegante måde… Inden vi går i kødet på en for-løkke, skal vi lige se, hvordan ovenstående kan skrives ved at bruge en for-løkke:

private void printMeasurements() { for (int index = 0; index < 100; index++) { print(allMeasurements[index]); } }

I hvert fald noget kortere og nemmere at rette, hvis vi pludselig får 500 målinger (måske skulle vi have brugt en konstant her…?). Men hvad er det egentlig der sker?

En for-løkke har helt generelt følgende struktur og syntaks:

for (initialisering; betingelse; ændring) { // Kode som skal udføres i hvert gennemløb af løkken }

42

I almindelig tale sker der følgende:

1. Udfør initialisering2. Check om betingelse er sand3. Hvis betingelse er sand, gør følgende:

a. Udfør koden inde i løkken (mellem { og })b. Udfør ændringc. Gå tilbage til 2

4. Vi er færdige

Det som adskiller en for-løkke fra en if-sætning er muligheden for at udføre ”kroppen” af løkken (det mellem { og }) mere end én gang. Så længe betingelsen er sand, tager vi en tur mere i løkken. Med den viden kan vi prøve at læse koden fra før:

for (int index = 0; index < 100; index++) { print(allMeasurements[index]); }

I initialiserings-skridtet definerer vi en variabel index (det er ikke noget variablen skal hedde, den kunne bare hedde i eller noget andet), og sætter den til 0. Dette skridt udføres kun én gang.

Nu checker vi så vores betingelse, index < 100. Da vi lige har sat index til 0, er betingelsen sand, og vi udfører koden i kroppen af løkken. Den udskriver værdien af allMeasurements[0], idet index er 0.

Herefter foretager vi ændrings-skridtet. Her står index++. Hvad betyder det nu? Faktisk er det blot en kort notation for at lægge én til værdien af en variabel. Altså ændrer vi nu værdien af index fra 0 til 1. Det er en meget typisk notation at bruge i en for-løkke. Bemærk, at det ville være helt legalt at skrive index = index +1 i stedet for.

Efter ændrings-skridtet går vi tilbage og checker betingelsen. Nu er index lig 1, så betingelsen er stadig opfyldt. Altså tager vi en tur til i løkken. Denne gang udskrives værdien af allMeasurements[1]. Derefter ændrings-skridtet, og så videre, og så videre. Denne løkke ender altså med at skrive alle de 100 værdier i allMeasurements ud, på en meget bekvem måde. Arrays og for-løkker passer fortrinligt sammen, og man vil se konstruktioner som denne gang på gang i Java-kode.

7.7.6 populateFire metoden

Vi bruger en for-løkke allerede i populateFire metoden (populateWall er tom indtil videre, så den springer vi over). populateFire skal befolke vores verden med et antal Fire-objekter, hvilket gøres meget naturligt med en for-løkke. Logikken for at indsætte et Fire-objekt er:

43

1. Find en celle i verdenen, som ikke er optaget2. Indsæt et Fire-objekt i denne celle3. Marker cellen som optaget

Dette skal gøres for alle de Fire-objekter vi ønsker at indsætte, så grundstrukturen i metoden bliver:

for (int fireCount = 0; fireCount < fireObjects; fireCount++) { // Indsæt et Fire-objekt }

Selve strukturen af løkken er identisk med eksemplet fra før, og vil resultere i at fireObjects Fire-objekter bliver indsat i verdenen (fireObjects er lig med 50). Kroppen i løkken er en lille smule mere kompliceret:

int x = generator.nextInt(cellCount); int y = generator.nextInt(cellCount); while (cellIsUsed[x][y]) { x = generator.nextInt(cellCount); y = generator.nextInt(cellCount); } addObject(new Fire(), x, y); cellIsUsed[x][y] = true;

I hvert gennemløb af løkken vil vi gerne indsætte et nyt Fire-objekt i verden, i en celle som endnu ikke er optaget. For at få en rimelig jævn fordeling vælger vi den nye position tilfældigt. Det er det som sker i de første to linier, hvor vi får en tilfældig x- og y-koordinat ved at benytte tilfældigheds-generatoren. Hvis man giver tilfældigheds-generatoren et tal n, vil man få et tilfældigt tal mellem 0 og (n-1) tilbage. Overvej selv, hvorfor vi så kalder tilfældigheds-generatoren med cellCount…

Der er imidlertid en lille komplikation. Den tilfældige koordinat vi genererer kunne jo hænde at udpege en celle som er brugt i forvejen. Hvis det er tilfældet, må vi prøver at generere en ny koordinat. Dette skal vi fortsætte med, indtil vi rent faktisk finder en tom celle. Det er præcis det som sker i den såkalde while-løkke i den ovenstående kode.

7.7.7 while-løkker i Java

En while-løkke minder meget om en for-løkke, men med et par vigtige forskelle. Den generelle struktur er således:

while (betingelse) { // Kode som skal udføres i hvert gennemløb af løkken }

44

Umiddelbart måske lidt simplere end en for-løkke, men til gengæld skal man holde tungen lidt mere lige i munden. Strukturen er simpel; så længe betingelsen er opfyldt, udføres koden mellem { og }. Det er netop det vi gør i while-løkken i populateFire; så længe cellen defineret ved x- og y-koordinaten er optaget (det er den jo netop hvis cellIsUsed[x][y] er true), genererer vi en ny koordinat. På et eller andet tidspunkt skulle vi så gerne ”ramme” en ledig celle. Dette gør betingelsen falsk, og vi forlader while-løkken.

Det problematiske ved en while-løkke opstår, hvis koden i løkkens krop ikke foretager sig noget, der kan ændre på betingelsen. I så tilfælde kommer vi aldrig ud af løkken, og programmet er dermed fanget i en uendelig løkke. En uendelig løkke er også en klassisk fejl i programmering, der kan udmønte sig i at programmet pludselig ikke reagerer længere, men stadig er aktivt.

Som det sidste i populateFire-metoden indsætter vi et nyt Fire-objekt på den position i verdenen, som viste sig at være ledig. Dette gøres ved at kalde metoden addObject, som er defineret i World-klassen. Endelig skal det tilsvarende element i cellIsUsed sættes til true, så cellen nu står markeret som optaget.

7.7.8 Metoderne populateFood og populateViking

De to resterende populate… metoder er skåret over den samme læst som populateFire, så de gennemgås ikke yderligere. Bemærk dog, at populateViking kun indsætter et enkelt Viking-objekt i verdenen, hvilket vel også er meget naturligt…

7.7.9 createWallSegment metoden

Den sidste metode i TVPWorld er createWallSegment. Husk, at der også er defineret en populateWall metode i TVPWorld, som dog er tom indtil videre. I modsætning til de andre objekter i vores verden, giver det ikke helt så meget mening bare at lægge vægge helt tilfældigt ud. Som regel vil vi have en eller anden mere overordnet struktur i vores verden, som vi kan definere med vægge. Måske kan vi definere en slags hule eller borg i et område.

Tanken er derfor, at vi vil udlægge væggene i vores verden i såkaldte væg-segmenter, som er en sammen-hængende mængde Wall-objekter, der danner et rektangel. For at danne sådan et væg-segment har vi brug for at kende en start-koordinat og en slut-koordinat for rektanglet. Start-koordinaten kan være det øverste venstre hjørne af væg-segmentet, og slut-koordinaten det nederste højre hjørne:

45

Figur 7.5: Væg-segment fra (1,1) til (4,2)

Vi kan så sætte væg-segmenter sammen til egentlige strukturer i vores verden, ved gentagne kald af createWallSegment fra populateWall. Det vil selvfølgelig være smart at udlægge vægge før de øvrige objekter i verdenen, idet vi ellers kunne komme til at lægge vægge oveni eksisterende objekter.

Selve koden for createWallSegment er rimeligt ligetil:

private void createWallSegment(int xStart, int yStart, int xEnd, int yEnd) { for (int x = xStart; x <= xEnd; x++) { for (int y = yStart; y <= yEnd; y++) { addObject(new Wall(), x, y); cellIsUsed[x][y] = true; } } }

Metoden tager som nævnt de to hjørne-koordinater som parametre, og ved hjælp af to for-løkker får vi fyldt Wall-objekter i de celler, som tilsammen udgør væg-segmentet. Bemærk, at de to for-løkker ser en smule anderledes ud end dem vi så tidligere. Dette illustrerer blot, at der f.eks. ikke er noget krav om at der tælles fra 0 og opefter i en for-løkke; vi kan definere elementerne i en for-løkke ret frit, så længe betingel-sen vitterligt er en betingelse, og så videre.

Opgave 7.5: Benyt populateWall og createWallSegment til at skabe nogle strukturer i verdenen, efter eget valg. Det kunne være en slag hule, borg, labyrint eller hvad man nu kan finde på. Prøv efterfølgende at spille spillet igen. Opfører vikingen sig som forventet, når vikingen går ind i en væg?

7.7.10 Ændringer grundet indførslen af Wall-objekter i verdenen

Tilføjelsen af vægge i verdenen har umiddelbart den uheldige effekt, at vikingen er i stand til at gå gennem vægge… Det kunne selvfølgelig være et bevidst valg (undervurdér ikke Total Viking Power!!), men lad os indtil videre antage, at en viking ikke skal kunne gå gennem vægge. Hvis vi skal opnå det, må vi ændre på spillets logik, og dermed kode.

Mere specifikt skal vi nok have fat i logikken for, hvordan en viking bevæger sig rundt i verdenen. Lige nu er logikken noget i denne stil:

1. Tag et skridt til en ny position, alt efter hvilken piletast spilleren holder nede2. Undersøg, om der er et objekt på den nye position3. Interagér med objektet, hvis der var et, og gå tilbage til skridt 1.

Denne logik må skulle ændres, men til hvad…?

46

Opgave 7.6: Definér en ny logik for at lade vikingen tage et skridt. Måske må man undersøge noget, før man rent faktisk tager skridtet. Når logikken er på plads, skal den omsættes til ændringer i koden. Husk at overveje grundigt hvordan der skal rettes, før du går i gang.

7.8 Scenariet TVP_Kap7_Opg7_6_efter

Vi fortsætter nu fra det fælles udgangspunkt TVP_Kap7_Opg7_6_efter. Dette scenarie rummer en løsning på Opgave 7.5 og Opgave 7.6. I Opgave 7.5 skulle vi bygge en struktur i verdenen, ved hjælp af metoderne populateWall og createWallSegment. Dette kan naturligvis gøres på mange måder, og ingen måde er for så vidt bedre end en anden. I TVP_Kap7_Opg7_6_efter er der bygget en relativt simpel struktur, ved at indsætte følgende linier kode i populateWall:

private void populateWall() { createWallSegment(2,2,22,2); createWallSegment(2,3,2,10); createWallSegment(12,3,12,10); createWallSegment(22,3,22,10); createWallSegment(3,10,9,10); createWallSegment(15,10,21,10); }

Dette giver en struktur med følgende udseende.

Figur 7.6: Væg-struktur i verdenen (TVP_Kap7_Opg7_6_efter)

Opgave 7.6 er den mere udfordrende problemstilling med at ændre logikken for at flytte vikingen, så han ikke kan gå ind i en væg. I stedet for logikken beskrevet tidligere, må vi i stedet gøre følgende:

1. Alt efter hvilken piletast spilleren holder nede, undersøg da, om der befinder sig et Wall-objekt på den position, vikingen er på vej hen til (dette undersøges altså før selve flytningen foretages)

2. Hvis der befinder sig et Wall-objekt på den position, vikingen er på vej hen, så bliv stående på den nuværende position, og gå tilbage til skridt 1. Ellers fortsæt til skridt 3.

3. Udfør flytningen til den nye position

47

4. Undersøg, om der er et objekt på den nye position5. Interagér med objektet, hvis der var et, og gå tilbage til skridt 1.

Med andre ord kan vi ikke længere bare ukritisk gå i den retning, der bliver udpeget af den piletast, som brugeren holder nede. Derfor skal vi ændre i koden for at udføre en flytning. Oprindeligt så koden ud på denne måde (med tilsvarende kode for de andre tre retninger):

if (Greenfoot.isKeyDown("down")) // Er "pil ned" trykket ned? { moveSouth(); }

Her skal altså indføres et check, før metoden moveSouth kaldes. Det kunne se således ud:

if (Greenfoot.isKeyDown("down")) // Er "pil ned" trykket ned? { if (canMoveSouth()) { moveSouth(); } }

Bemærk, at vi jo ikke har sådan en metode canMoveSouth til rådighed endnu, men hvis nu vi havde den, ville ændringen i koden kunne udføres således. Dette er et eksempel på et meget brugt princip i program-mering. Når vi skal kode et eller andet, er det næsten altid umuligt at overskue alle detaljer i hovedet på én gang. Det er derfor altid en god idé at bryde problemet ned i mindre stykker, som man nemmere kan overskue. Her bruger vi ”ønskefe”-princippet . Vi har godt nok ikke sådan en canMoveSouth metode lige nu, men hvis nu den gode fe gav os sådan en metode, ville det være ret let at ændre koden for at udføre en flytning, som vist ovenfor. Dermed har vi nu reduceret problement til at finde en passende måde at kode selve metoden på; hvordan metoden så skal bruges, har vi allerede løst.

Hvordan skal canMoveSouth så kodes? I Actor-klassen er metoden getOneObjectAtOffset defineret, og vi har i øvrigt allerede brugt den i interactWithObject. Metoden kan undersøge, om der findes et objekt af en given klasse på en bestemt position relativt til objektet selv. Med andre ord – hvis vi kalder denne metode på et Viking-objekt, vil den undersøge om der befinder sig objekter af den specificerede type på den specificerede position relativt til vikingen.

Hvad betyder ”relativt til vikingen” egentlig? Normalt når vi angiver en position ved en x- og y-koordinat, er det ”absolutte” koordinater i forhold til verdenen, hvor (0,0) er cellen oppe i øverste venstre hjørne. Men når det er relativt til et objekt (her vores Viking-objekt), regner vi så at sige ud fra vikingens position. Altså vil Viking-objektet per definition altid stå på position (0,0):

(-1,-1) (0,-1) (1,-1)

(-1,0) (0,0)(her står vikingen)

(1,0)

48

(-1,1) (0,1) (1,1)

Dermed giver de tidligere kald af getOneObjectAtOffset også god mening. Vi brugte den til at undersøge, om vikingen i sin nye position nu ”stod ovenpå” et objekt, altså om et objekt havde samme koordinater som vikingen selv. Derfor så kaldet f.eks. således ud:

getOneObjectAtOffset(0, 0, Fire.class);

Dette skal altså læses som “undersøg om der et Fire-objekt på samme position som jeg selv”.

I canMoveSouth skal vi jo undersøge, om der er et Wall-objekt umiddelbart syd for os. Som vi kan se på figur 7.7, er ”umiddelbart syd” for vikingen jo koordinaten (0,1) relativt til Viking-objektet selv. Altså bliver koden for canMoveSouth:

public boolean canMoveSouth() { Actor anObject = getOneObjectAtOffset(0, 1, Wall.class); return (anObject == null); }

I første linie undersøger vi, om der er et Wall-objekt umiddelbart mod syd. Hvis der ikke er det, vil metoden getOneObjectAtOffset returnere den specielle værdi null. Altså vil det være legalt at flytte mod syd, hvis anObject er lig med null. Det er netop det vi undersøger i sidste linie.

Med denne metode på plads er det en simpel sag at kode tilsvarende metoder for de tre andre retninger. Se ved selvsyn hvordan den endelige kode tager sig ud i Viking-klassen. Endelig kan man prøve spillet af, og se om vikingen vitterligt ikke længere er i stand til at gå gennem vægge.

7.9 Forbedringer af kodens design

Vi har efterhånden foretaget flere ændringer af vores kode. Når vi har foretaget en ændring, har den været drevet af at tilføje noget ny funktionalitet til vores program. Vi har ikke tænkt så meget over, hvordan ændringen ændrer programmets overordnede struktur eller design. Når man programmerer en større mængde kode, kan det imidlertid være en god idé til tider at lave kodeændringer, der ikke ændrer på programmets virkemåde, men udelukkende har til formål at forbedre programmets design. Erfaringen har vist at det er ganske vanskeligt at gøre begge dele samtidigt, så ofte er det godt givet ud udelukkende at koncentrere sig om designet i en periode.

7.9.1 Principper for ændringer af kodens design, første runde

49

Hvornår har man et program med et godt design? Det er et meget omfattende spørgsmål, som rummer mange facetter af programmering. Det er også et emne der er skrevet ganske tykke boger om… Her ser vi i første omgang på nogle meget grundlæggende principper.

Et meget grundlæggende princip er at undgå at skrive den samme kode mere end ét sted i programmet. Det er klart at vi f.eks. ikke kan skrive præcis den samme metode to gange i en klasse – så vil compileren brokke sig! Men vi kan sagtens skrive to (eller flere) metoder, der minder rigtig meget om hinanden. Tag dette meget simple eksempel, hvor vi åbenbart har behov for at skrive tallene 1 og 2 ud. Derfor har vi lavet to metoder til det:

public void print_1() { print(1); }

public void print_2() { print(2); }

Dette kan sikkert virke fint et stykke tid, men hvad nu når vi en dag for brug for også at skrive 3 ud? Skal vi så lave en ny metode print_3? Det virker jo noget klodset, og en bedre løsning er (selvfølgelig) at lave en anden metode, der tager et tal som en parameter, og udskriver dette tal:

public void printNumber(int n) { print(n); }

Dette er langt mere robust, i den forstand at vi nu kan printe alle tal ud vi lyster, uden at skulle lave en ny metode hver gang. Det illustrerer også et meget brugt princip til forbedring af kodens design; gør metoden mere generel ved at ændre konstanter til parametre.

At det er besværligt hele tiden at skulle lave nye metoder, er et godt argument for at gøre en metode mere generel. Et andet argument er vedligeholdelse af koden. Måske får vi en dag lyst til, at vi efter at have udskrevet tallet lige udskriver en blank linie, for at give lidt mere luft i udskriften. Hvis vi havde separate metoder til at udskrive hvert tal, skulle vi jo lave denne ændring i hver metode. Dette er dels besværligt, og der er en risiko for at glemme at få det gjort i alle metoder. Altså et argument i stil med argumentet for at bruge konstanter. Med en generel metode til rådighed skal der kun rettes ét sted i koden.

7.9.2 Redesign af move… metoderne

Lad os prøve at anvende dette princip på noget af koden i Viking-klassen. Lad os først se på de metoder, som foretager en flytning af vikingen:

50

public void moveNorth() { setLocation(getX(), getY() - 1); }

public void moveSouth() { setLocation(getX(), getY() + 1); }

// …og tilsvarende for moveEast og moveWest

Disse fire metoder er meget ens – det eneste der adskiller dem er hvor meget vi justerer på x- og y-koordinaten. Dette burde kunne erstattes af en mere generel metode, hvor vi gør konstanterne til parametre i stedet:

public void moveRelative(int xRelative, int yRelative) { setLocation(getX() + xRelative, getY() + yRelative); }

Denne metode ser lidt mere indviklet ud, men kan til gengæld gøre alt det som de fire andre metoder kan, og mere til. Hvis vi et eller andet sted i koden laver kaldet:

moveNorth();

kan vi nu erstatte det med kaldet:

moveRelative(0, -1);

Noget helt tilsvarende kan selvfølgelig gøres for de andre retninger. Tilmed kan vi, hvis vi en dag skulle få behov for at gå i f.eks. retningen syd-øst, gøre dette meget nemt med den nye metode:

moveRelative(1, 1);

Det er lidt af en balancegang, hvor smarte og generelle man skal lave sine metoder. Der er mange fordele ved generelle metoder, men de kan nogle gange blive så generelle, at kan være svært at se hvad de over-hovedet laver… På en måde flytter man noget af metodens funktionalitet ud i parametrene, og til sidst kan det være et meget bart kode-skelet, der står tilbage. Hvis man synes en metode er ved at blive ”for generel”, bør man overveje en ekstra gang, om man vitterligt skal ændre mere på den.

7.9.3 Redesign af canMove… metoderne

Problemstillingen for disse fire metoder er helt tilsvarende, og vi vil derfor ikke argumentere så meget for hvorfor og hvordan dette skal ændres. Vi ender med den mere generelle metode canMoveRelative:

51

public boolean canMoveRelative(int xRelative, int yRelative) { Actor anObject = getOneObjectAtOffset(xRelative, yRelative, Wall.class); return (anObject == null); }

Ved at indføre disse to mere generelle metoder canMoveRelative og moveRelative, kan vi med et slag fjerne hele otte metoder i den eksisterende kode.

7.9.4 Redesign af populate… metoderne

Ser vi på TVPWorld-klassen, er der tydeligvis også potentiale for at erstatte flere nært beslægtede metoder med en mere generel metode. Klassen rummer flere metoder af typen populate…, nærmere bestemt en for hver Actor-type. Metoden populateWall har en lidt speciel struktur, men de øvrige metoder er skåret over den samme læst. Tag for eksempel koden for populateFood:

private void populateFood() { // Placer et antal Food objekter for (int foodCount = 0; foodCount < foodObjects; foodCount++) { int x = generator.nextInt(cellCount); int y = generator.nextInt(cellCount); while (cellIsUsed[x][y]) { x = generator.nextInt(cellCount); y = generator.nextInt(cellCount); } addObject(new Food(), x, y); cellIsUsed[x][y] = true; } }

Hvad er der egentlig af Food-specifik kode i denne metode? Hvis vi sammenligner med f.eks. populateFire, er der faktisk kun to linier:

Starten på for-løkken, hvor vi refererer til konstanten foodObjects Kaldet til addObject, hvor vi laver et Food-objekt

Ud over disse to linier er koden identisk. Dette gælder også i forhold til populateViking, idet den blot er et specialtilfælde af den mere generelle udgave. Vi skal kun skabe et enkelt Viking-objekt, så derfor behøver vi ikke en for-løkke, men der er i sig selv ikke noget galt i at have en for-løkke, selv om den kun bliver løbet igennem en enkelt gang.

Hvordan skal en mere generel version af populate… metoderne se ud? Vi kan i hvert fald nemt gøre kon-stanten som angiver antallet af objekter til en parameter i stedet. Det er straks lidt sværere at gøre den type af objekt der skal laves til en parameter!

52

Et første bud på en mere generel metode populateActorType kunne være dette:

// NB! Dette stykke kode virker ikke!! private void populateActorType(int actorObjects, ClassName theActorType) { for (int actorCount = 0; actorCount < actorObjects; actorCount++) { int x = generator.nextInt(cellCount); int y = generator.nextInt(cellCount); while (cellIsUsed[x][y]) { x = generator.nextInt(cellCount); y = generator.nextInt(cellCount); } addObject(new theActorType(), x, y); cellIsUsed[x][y] = true; } }

Linierne fremhævet med blåt er de to linier, der er ændret i forhold til de oprindelige metoder. Den første linie er helt i orden; vi har indført parameteren actorObjects, som angiver hvor mange objekter af den givne type der skal skabes. Vi skal så blot sørge for at benytte den rigtige konstant som parameter til metoden, når vi kalder den.

Den anden linie er mere problematisk. Vi har her forsøgt at gøre typen af objekt til en parameter, med den fiktive type ClassName. Det ser jo nemt ud, men det virker ikke… Vi bliver i stedet nødt til at lave en ny metode, som kan skabe et objekt af en specificeret type. Med andre ord skal den nye metode tage en form for specifikation af ønskede type objekt som input, og returnere et objekt af netop den type. Her får vi den hjælp, at vi kun er interesserede i at lave objekter af typen Actor. Det kan så i visse tilfælde være et Food-objekt, Fire-objekt og så videre, men base-klassen vil altid være Actor. Det gør, at metoden kan have Actor som sin returtype, også selv om det egentlig er et mere specialiseret objekt, der er skabt.

Metoden til at producere Actor-objekter kan derfor se således ud:

private Actor createActorType(ActorType theActorType) { switch (theActorType) { case fire: return new Fire(); case food: return new Food(); case viking: return new Viking(); } return null; }

53

Der er flere nye ting i denne metode. Først og fremmest tager metoden en parameter af typen ActorType. Dette er ikke en del af Java-sproget, men en type vi selv har defineret! Tidligere i koden for klassen indsætter vi denne linie:

private enum ActorType {fire, food, viking};

I Java vil man ofte gerne definere sine egne typer, hvis man har et tilfælde hvor f.eks. en int eller en string ikke lige er det rigtige. Og hvorfor er det ikke det rigtige her? Det vi er i gang med, er at finde en god måde at fortælle metoden på, hvilken type objekt den skal lave. Dette kunne vi måske godt gøre med en tekst-streng, så et kald af metoden kunne se således ud:

createActorType(“Food”);

Dette kunne godt virke, men det er lidt ”farligt”. Vi kunne nemt komme til at stave navnet på typen forkert, hvilket ikke ville blive opdaget ved kompilering af programmet! Det vil kun blive checket, om vi kalder metoden med en lovlig tekst-streng, og strengen ”Fuud” er jo en fuldt lovlig tekststreng, selv om vi godt ved der ikke findes en Fuud-klasse. Dette kan så give anledning til problemer når programmet rent faktisk køres.

Det ville derfor være rart at have en type, som kun kan have nogle få, forudbestemte værdier. Dette gælder f.eks. for typen boolean, som kun kan være true eller false. Lige i dette tilfælde vil vi gerne have en type, som kun kan have værdierne food, fire og viking, svarende til de typer af objekter vi gerne vil kunne skabe. Sådan en type kan vi selv definere – den slags typer kaldes for enumererede typer.

En definition af en enumereret type benytter ordet enum, efterfulgt af et selvvalgt navn for typen, efter-fulgt af en liste af de mulige værdier, ganske som i eksemplet ovenfor. Hvis man senere ønsker at føje nye mulige værdier til, tilføjes de blot til listen.

Med denne definition af ActorType vil et kald af createActorType se således ud:

createActorType(food);

Det er ikke en forskel der ser ud af alverden, men den gør det umuligt at lave fejlagtige kald i form af en forkert stavet type, hvilket er med til at sænke risikoen for fejl i programmet.

Selve koden i createActorType rummer også noget nyt; en switch-sætning. I et program er det meget ofte sådan, at man på basis af værdien af en variabel skal foretage forskellige handlinger. Det har vi allerede set eksempler på, da vi så på if-sætningen. En switch-sætning er ret tæt beslægtet med en if-sætning. På én side er switch-sætningen mere generel, idet der kan være flere end to alternativer, på den anden side er den mere begrænset, idet man ikke kan lave betingelser som sådan for et alternativ, men kun teste på absolutte værdier for variablen.

Hvorfor hedder det så en switch-sætning? Man kan måske tænke på den som et ”omstillingsbord”, hvor man bliver ”stillet videre” til den rigtige stump kode, alt efter værdien af den variabel der testes på. Denne viderestilling specificeres med ordet case.

Den switch-sætning som udgør kroppen af metoden createActorType skal læses således:

Hvis theActorType er lig med food, så lav og returnér et Food-objekt Hvis theActorType er lig med fire, så lav og returnér et Fire-objekt

54

Hvis theActorType er lig med viking, så lav og returnér et Viking-objekt

Som et sikkerhedsnet returnerer vi null, hvis ikke der findes en case, der passer med værdien af variablen. Lige nu er det umuligt, men det kunne jo være, at vi senere tilføjede flere mulige værdier, og glemte at få rettet koden i metoden selv…

Med definitionen af createActorType på plads, kan vi endelig skrive den fejlagtige linie i populateActorType på korrekt vis. Vi erstatter:

addObject(new theActorType(), x, y);

med

addObject(createActorType(theActorType), x, y);

Dermed har vi en generel metode til at indsætte Actor-objekter i vores verden. Som det sidste skal vi så bruge vores nye metode i populateTVPWorld metoden:

public void populateTVPWorld() { initialiseCellIsUsed(); populateWall(); populateActorType(fireObjects, ActorType.fire); populateActorType(foodObjects, ActorType.food); populateActorType(1, ActorType.viking); }

Bemærk, at vi i kaldet af metoden ikke bare kan skrive f.eks. fire, men skal skrive Actortype.fire. Dette skyldes, at der jo godt kunne være andre enumererede typer, der indeholdt fire som en mulig værdi. Derfor skal selve navnet på typen også med.

NB: Før du går i gang med næste opgave, hent da scenariet TVP_Kap7_Opg7_7_klar.

Opgave 7.7: Kig nærmere på den reviderede kode i scenariet TVP_Kap7_Opg7_7_klar. Hvordan har størrelsen (antallet af kodelinier) af koden i klasserne TVPWorld og Viking udviklet sig, i forhold til det tidligere scenarie?

7.10 Scenariet TVP_Kap7_Opg7_7_klar

Alle disse ændringer af metodernes design er inkluderet i scenariet TVP_Kap7_Opg7_7_klar. Bemærk, at der således ikke er nogen ny funktionalitet i TVP_Kap7_Opg7_7_klar i forhold til TVP_Kap7_Opg7_6_efter, men den underliggende kode har fået et bedre og mere generelt design. Som nævnt er det en vældig god idé at have en sådan aktivitet med jævne mellemrum, når man udvikler et lidt større program. Dermed kan man nøjes med at fokusere på enten design eller funktionalitet, alt efter hvilken aktivitet man udfører. At

55

fokusere på begge dele samtidigt kan gøre det meget svært at overskue, hvordan man bedst kommer videre med programmet.

Scenariet TVP_Kap7_Opg7_7_klar er således det nye fælles udgangspunkt. I den næste fase vil vi igen fokusere på at tilføje funktionalitet, mere specifikt indenfor to områder:

Tilføjelse af en bevægelig Actor Respawn af Actor-objekter i verdenen

7.10.1 Tilføjelse af en bevægelig Actor

Det er svært at forestille sig et RPG uden en form for fjender, der også bevæger sig rundt i verdenen. Møder man en sådan fjende, vil der som regel udspille sig en eller anden form for kamp mellem hoved-personen (i vores tilfælde vikingen) og fjenden. Alt dette er ret komplekst at definere logik for, så i første omgang ser vi blot på, hvordan vi kan få et væsen til at bevæge sig rundt i verdenen af ”egen kraft”.

Den simpleste strategi for at bevæge sig rundt i verdenen er ganske enkelt at gå tilfældigt rundt. Lidt mere specifikt vil det betyde, at man i hvert kald af act (idet det vil være helt naturligt, at klassen som skal repræsentere en fjende også arver fra Actor), skal overveje, hvor man nu skal flytte sig hen. Tænker man lidt mere over det, er det faktisk meget lig vikingens opførsel. Blot skal fjenden ikke reagere på brugerens tastetryk, men i stedet selv generere den næste position. Dette kunne gøres ved tilfældigt at vælge en af nabo-positionerne:

(-1,-1) (0,-1) (1,-1)

(-1,0) (0,0)(her står Actor)

(1,0)

(-1,1) (0,1) (1,1)

Figur 7.8: Koordinater relativt til Actor-objektet

Derudover skal vi også overveje, hvordan en fjende skal interagere med andre Actor-objekter. Skal Food- og Fire-objekter forsvinde, hvis fjenden møder dem? Vi vælger, at fjenden ikke interagerer med Food- og Fire-objekter, men derimod interagerer med Wall-objekter, på samme måde som vikingen selv.

Endelig skal vi have defineret præcis hvordan vi vælger den næste position. Ser vi på Figur 7.8, kunne det gøres ved tilfældigt at vælge en ny x- og y-koordinat relativt til den nuværende position, dog indenfor grænserne -1 til 1 for begge koordinater. I så fald vil den nye position altid være en af de ni celler angivet i figuren. Er det så en fejl, hvis vi lige præcis får valgt (0,0), og dermed ikke flytter os? Ikke nødvendigvis – det

56

er jo os, der vælger hvordan logikken for en fjende skal virke, så hvis fjenden engang imellem lige stopper op, er det jo fint nok.

Hvordan laver vi tilfældige tal mellem -1 og 1? Vi har jo før brugt klassen Random til at lave tilfældige tal. Hvis vi har en variabel generator af typen Random, vil kaldet

generator.nextInt(100);

lave et tilfældigt tal mellem 0 og 99.

Opgave 7.8: Overvej, hvorfor denne stump kode:

int x = generator.nextInt(3) – 1;

vil lave et tilfældigt tal som er enten -1, 0 eller 1.

Med ovenstående stump kode til at lave en tilfældig koordinat, kan vi skrive en move-metode til den klasse, som skal repræsentere en fjende. Den traditionelle fjende i et RPG er jo en drage, så lad os definere en klasse Dragon, som arver fra Actor. I Dragon bliver move-metoden således:

private void move() { Random generator = new Random(); int xNext = generator.nextInt(3) - 1; int yNext = generator.nextInt(3) - 1; if (canMoveRelative(xNext, yNext)) { moveRelative(xNext, yNext); } }

Bemærk, at vi jo allerede har metoderne canMoveRelative og moveRelative; dem har vi jo defineret i Viking-klassen! For at kunne bruge dem i Dragon-klasse må vi kopiere koden fra Viking-klassen (dette er ikke den ideelle løsning, men det vender vi tilbage til…).

Når vi har indsat den kopierede kode, er selve klassen Dragon faktisk færdig (med den begrænsede funktionalitet, den vil have her i første omgang). Men før der begynder at dukke drager op i vores verden, skal der jo også ændres nogle steder i TVPWorld-klassen…

NB: Før du går i gang med næste opgave, hent da scenariet TVP_Kap7_Opg7_9_klar. Her er Dragon-klassen klar til brug.

Opgave 7.9: Ret koden i TVPWorld-klassen, således at der ved start vil være fem drager i vores verden.

Tip 1: Der skal rettes fire steder i TVPWorld-klassen

Tip 2: Man skal nok gøre stort set det samme som for Fire og Food

Vi skulle nu gerne have nogle drager gående rundt på må og få i vores verden. Men de virker ikke særlig farlige. Vores viking kan gå lige ind i dem, uden der sker noget. Det skal vi også have ændret.

57

NB: Før du går i gang med næste opgave, hent da scenariet TVP_Kap7_Opg7_10_klar. Her er der indsat Dragon-objekter i verdenen.

Opgave 7.10: Hvilken klasse skal der rettes i, hvis vi vil have en interaktion mellem en drage og vikingen? Lav i første omgang en interaktion, der virker ligesom interaktion med Fire-objekter, blot skal vikingen tabe 15 health-points ved interaktion med en drage.

NB: Scenariet TVP_Kap7_Opg7_10_efter rummer en løsning på Opgave 7.10.

Med denne funktionalitet har vi nu et ganske minimalt gameplay i vores RPG: Udryd alle dragerne uden at dø først! For at klare dette, skal man sørge for at få spist noget mad undervejs, ligesom man heller ikke skal gå ind i for meget ild. Hvis man synes spillet er for let, kan man f.eks. øge antallet af drager som skabes når spillet starter.

Opgave 7.11: Overvej, hvordan man kan få drager til at bevæge sig med lavere hastighed end vikingen. Måske skal man ikke kalde move-metoden hver eneste gang act bliver kaldt…?

7.10.2 Respawn af Actor-objekter i verdenen

I næsten alle RPG møder spilleren forskellige objekter undervejs, som spilleren kan interagere med. Ofte har objektet en positiv virkning på spilleren (som f.eks. et Food-objekt i TVP), mens andre kan have en negativ virkning (som f.eks. et Fire-objekt i TVP). Nogle ting kan have både negative og positive effekter, f.eks. kan vi møde et monster, kæmpe mod det (og dermed miste health-points) og modtage en eller anden form for belønning, når monsteret er nedkæmpet. Dette kan være i form af forskellige effekter monsteret bærer på sig (såkaldt loot), og/eller et antal ”erfarings-points”, som på et tidspunkt udløser en ellen anden gevinst i form af bedre evner for spilleren eller lignende.

I langt de fleste RPG er det tillige sådan, at et objekt ”forbruges” ved interaktion med spilleren, d.v.s. det forsvinder fra verdenen. Sådan er det også i TVP. Imidlertid er det også meget almindeligt at objektet (eller rettere: et objekt med egenskaber mage til det tidligere objekt) fremkommer igen efter et vist stykke tid. Dette fænomen kaldes på engelsk for respawn, og vi benytter den betegnelse her. Lige nu vil objekter i TVP ikke respawne, så efter et stykke tid vil verdenen være tom… Vi vil derfor nu indføre respawn af objekter i TVP.

Respawn virker umiddelbart ikke som en meget kompliceret egenskab, men der er alligevel flere ting, vi skal tage stilling til.

Hvor skal objektet respawne? Hvornår skal objektet respawne? Skal det altid være samme type objekt, der respawner samme sted? Hvem styrer respawn-processen? Er det det enkelte objekt, eller sker det centralt?

58

Nogle af disse spørgsmål er i nogen grad afhængige af hinanden. Med hensyn til det sidste spørgsmål vil vi højst sandsynligt opnå det mest fleksible design, hvis det er objektet selv der skal styre respawn-processen. Noget af det kode-omstrukturering i TVPWorld-klassen vi just har foretaget, har haft til formål at fjerne en masse gentagelse af kode, der kun adskiller sig med hensyn til den type objekt koden virker på. Hvis vi skal til at håndtere respawn-processen i TVPWorld-klassen, kommer vi nok nemt til at indføre den slags kode igen. Vi prøver derfor at lade objektet selv styre respawn-processen.

En første konsekvens af dette bliver, at vi ikke længere kan tillade os helt at fjerne et objekt fra verdenen, når vikingen har interageret med det. Hvis vi gør det, så er objektet jo væk, og act-metoden kan jo ikke blive kaldt på et objekt der ikke er der… I stedet for at fjerne objektet må vi derfor ændre på dets tilstand i stedet for. Indtil videre har f.eks. et Fire-objekt jo ikke haft nogen tilstand, men hvis vi vil have objektet til at blive i verdenen, selv om det er ”forbrugt”, må det på en eller anden måde styres via objektets tilstand. Vi må have styr på, om objektet er ”aktivt” eller ”passivt”. Kun når objektet er ”aktivt”, kan man interagere med det.

Da der kun er to muligheder med hensyn til denne tilstand, kan vi fint indføre en instans-variabel af type boolean til at holde styr på dette:

private boolean isActive;

Når et objekt forbruges, skal det sætte til at være passivt (isActive sættes til false), og når det engang respawner, sættes isActive til true. Til at håndtere dette kan vi passende lave to metoder despawn og respawn, der i første (og ikke komplette) version ser således ud:

public void respawn() { isActive = true; } public void despawn() { isActive = false; }

En konsekvens af dette bliver så, at koden i Viking-klassen for interaktion med et Fire-objekt skal ændres. I stedet for at fjerne Fire-objektet, skal vi i stedet kalde metoden despawn:

private void interactWithFire(Fire fireObject) { fireObject.despawn(); healthPoints = healthPoints - 3; }

Dette var den nemme del – et Fire-objekt skal despawne, når vikingen har interageret med det. Hvordan så med respawn? Der skal gå et vist stykke tid, før objektet respawner. Den eneste måde et objekt kan ”måle” tid på, er hvor mange gange act-metoden er blevet kaldt. Med andre ord bør objektet respawne, når act-metoden er blevet kaldt et vist antal gange. Dette skal objektet selv kunne holde styr på! Dermed må vi indføre en slags tæller, som styrer hvor mange kald af act der skal ske før objektet respawner. Det gøres nemmest med endnu en instans-variabel, denne gang af typen int.

59

private int timeLeftToRespawn;

Hvor mange gange skal act så rent faktisk kaldes, før objektet respawner? Her kan man vælge forskellige strategier, f.eks. et fast antal, eller et tilfældigt antal indenfor visse rammer. For nemheds skyld vælger vi et fast antal (100), og definerer en konstant timeToRespawn med denne værdi.

Opgave 7.12: Overvej logikken for ændring af værdien af timeLeftToRespawn. Hvad skal den sættes til i act? Hvad skal den sættes til i respawn? Hvad skal den sættes til i despawn?

Vi så før, at despawn-metoden bliver kaldt udefra (fra Viking-klassen), når vikingen interagerer med et Fire-objekt. Men hvem skal kalde respawn-metoden? Det må Fire-objektet jo selv skulle gøre! I hvert kald af act-metoden må Fire-objektet selv afgøre, om det er tid tid at respawne sig selv. Alt dette er naturligvis kun nødvendigt, i fald objektet ikke er aktivt. Koden for act bliver således:

public void act() { if (!isActive) { timeLeftToRespawn = timeLeftToRespawn - 1; if (timeLeftToRespawn == 0) { respawn(); } } }

Dermed er respawn-processen næsten klar for Fire-klassen. Der mangler dog et par detaljer. Først skal vi have sat start-tilstanden for et Fire-objekt til noget fornuftigt, hvilket gøres ved at skrive en constructor for Fire-klassen:

public Fire() { respawn(); }

Dette ser måske lidt overraskende ud, men vi vil netop gerne have, at det nyskabte Fire-objekt opfører sig præcis som et respawnet Fire-objekt, og det kan vi opnå på denne måde.

En anden nok så vigtig detalje er den grafiske tilstand af et Fire-objekt. Idet objektet som sådan ikke forsvinder fra verdenen efter interaktion, kan man lige nu ikke se på et Fire-objekt, om det er aktivt eller passivt. Det duer ikke i praksis! Vi skal derfor også skifte grafik for objektet, både når vi respawner og despawner, således at et passivt Fire-objekt ser anderledes ud end et aktivt.

Opgave 7.13: Overvej, hvad der vil være en passende grafik for et despawnet Fire-objekt. Se, om der allerede findes sådan en grafik i images-mappen i TVP_Kap7_Opg7_10_efter scenariet.

Har vi nu fået det hele med? Hvis man prøver at spille spillet, og prøver at gå hen over et Fire-objekt, vil man se at Fire-objektet ”forsvinder” (altså bliver sat til passivt), og efter et stykke tid fremkommer igen (bliver sat til aktivt). Dette ser godt nok ud. Men der er alligevel noget galt… Hvis man prøver at stille sig

60

oven i et Fire-objekt, vil man efter ganske kort tid dø, selv om man har rigeligt med health-points. Problemet ligger i Viking-klassen. Se på koden for interaktion med et Fire-objekt:

private void interactWithFire(Fire fireObject) { fireObject.despawn(); healthPoints = healthPoints - 3; }

Hvis vi stiller os oven i et Fire-objekt, bliver denne kode kaldt igen og igen, for hver gang act-metoden for Viking-objektet bliver kaldt. Vi forholder os ikke til det faktum, at Fire-objektet efter det første kald er blevet passivt, så vi mister 3 health-points hver eneste gang, uanset Fire-objektets tilstand!

Hvordan kan dette rettes? Umiddelbart virker løsningen ret oplagt; lav en metode isObjectActive i Fire-klassen, som returnerer værdien af isActive for et givent Fire-objekt. Så kunne koden rettes til noget i denne stil:

private void interactWithFire(Fire fireObject) { if (fireObject.isObjectActive()) { fireObject.despawn(); healthPoints = healthPoints - 3; } }

Dette vil virke, men er alligevel lidt problematisk. Ikke så meget rettelsen i sig selv, men mere det faktum at vi putter mere og mere af logikken omkring interaktion mellem et Viking-objekt og Fire-objekt ind i Viking-klassen. Jo flere klasser af typen Actor vi indfører, jo flere interactWith… metoder vil der vokse op i Viking-klassen, og jo mere stor og uoverskuelig bliver den. Og er det i det hele taget Viking-klassen, der skal bestemme hvordan interaktionen skal foregå?

Et bedre alternativ er at flytte logikken for interaktion over i den enkelte Actor-klasse. Dette kan gøres ved at indføre en metode interact i Fire-klassen, som skal rumme logikken for interaktion. Koden for interact-WithFire kunne da reduceres til:

private void interactWithFire(Fire fireObject) { fireObject.interact(); }

Koden for interaktionen i Fire-klassen kunne tilsvarende blive:

public void interact() { if (isActive) { despawn(); healthPoints = healthPoints - 3; // Ups!! } }

61

Det virker…næsten. Når interact er en metode i Fire-klassen, har vi ikke adgang til variablen healthPoints, idet den jo ligger i Viking-klassen. Vi vil derfor have brug for at kunne ”kalde tilbage” til Viking-objektet og sige ”reducér lige dine health-points med 3”. Dette kan også godt lade sig gøre. Først må vi indføre nogle metoder i Viking-klassen til at ændre på værdien af healthPoints. Disse metoder er ganske simple:

public int getHP() { return healthPoints; } public void setHP(int newHP) { healthPoints = newHP; } public void adjustHP(int change) { int newHP = getHP() + change; setHP(newHP); }

Den sidste metode er netop nyttig, hvis man bare skal ændre værdien af healthPoints med en vis værdi, uden at behøve kende selve værdien af healthPoints. Med denne metode til rådighed kan vi skrive en mere korrekt version af interact:

public void interact(Viking theViking) { if (isActive) { despawn(); theViking.adjustHP(-3); } }

Bemærk, at metoden nu tager et Viking-objekt som parameter. Hvor kommer det fra? Det skulle jo gerne være det Viking-objekt, som rent faktisk er i gang med interaktionen. Dette opnås ved at ændre lidt på metoden interactWithFire i Viking-klassen:

private void interactWithFire(Fire fireObject) { fireObject.interact(this); }

Bemærk det nye ord this. Dette ord har en ganske bestemt betydning i Java; det betyder ”objektet selv”. Idet metoden interact kræver et Viking-objekt som parameter – og mere specifikt det Viking-objekt, som nu interagerer med Fire-objektet – opnår vi netop dette med at skrive this som parameter. Det kan måske virke lidt forvirrende, at et objekt ved kald af en metode kan give ”sig selv” som parameter til metoden, men det er ikke desto mindre en konstruktion, der benyttes meget ofte i Java. Specielt når en metode kan have behov for at ”kalde tilbage” til det objekt som har kaldt den, er denne konstruktion meget nyttig!

Hermed har vi opnået at få respawn til at virke for Fire-klassen!

62

NB: Før du går i gang med næste opgave, hent da scenariet TVP_Kap7_Opg7_14_klar. Her er de beskrevne ændringer for Fire- og Viking klassen inkluderet i koden.

Opgave 7.14: Indfør på tilsvarende vis respawn for Food- og Dragon-klassen. Dette kan gøres ved at kopiere koden fra Fire-klassen, og rette den til på de relevante steder. Husk efterfølgende at rette koden i Viking-klassen, som beskrevet ovenfor.

Selv om vi undervejs i dette forløb også har forbedret designet en smule, ved at flytte koden for interaktion ud af Viking-klassen og ind i de respektive Actor-klasser, er koden i Viking-klassen reelt set ikke blevet specielt meget bedre. Vi har stadig en interactWith… metode for hver type Actor, hvilket vi gerne ville undgå. Tillige er meget af koden i de tre Actor-klassen meget ens, hvilket vi heller ikke bryder os så meget om. Som det næste emne skal vi se på, hvordan vi kan afhjælpe disse problemer med en bedre struktur for vores klasser.

7.11 Scenariet TVP_Kap7_Opg7_14_efter

Vi fortsætter nu fra det fælles udgangspunkt TVP_Kap7_Opg7_14_efter. I dette scenarie er respawn lavet for Fire-, Food- og Dragon-klassen. Med dette udgangspunkt skal vi se på mulighederne for forbedring af designet ved indførsel af nye base-klasser i vores klasse-struktur.

7.11.1 Indførsel af nye base-klasser

I et tidligere afsnit udførte vi redesign af koden ved at indføre mere generelle metoder. Mere specifikt brugte vi princippet om at gøre konstanter til parametre, hvilket resulterede i færre, men mere generelle, metoder. Dette princip kan bruges til at reducere antallet af metoder i en given klasse, hvis klassen rummer mange metoder der minder meget om hinanden. Men hvad nu hvis to forskellige klasser rummer en identisk metode? Dette bryder jo også vores princip om ikke at skrive den samme kode mere end ét sted i vores program. Dette problem kan løses ved at indføre nye base-klasser, og flytte de fælles metoder til base-klasserne.

Ideen med en base-klasse er netop at rumme den funktionalitet, som er fælles for en antal beslægtede klasser, for at undgå at definere denne funktionalitet mere end et enkelt sted. I vores tilfælde har vi oplagt nogle nært beslægtede klasser i klasserne Fire og Food. Bortset fra to egenskaber opfører de sig helt ens:

De har en forskellig grafik, når de er aktive i verdenen De har en forskellig interaktion med vikingen

Al funktionaliteten med hensyn til respawn er helt identisk.

63

7.11.2 Indførsel af base-klassen Respawnable

Vi indfører derfor er base-klasse Respawnable, som skal rumme alt det som er fælles for ”ting, som kan respawne”. Bemærk, at dette stadig kun vedrører Actor-klasser, så Respawnable skal være en sub-klasse til Actor, ligesom Fire og Food er det. Med andre ord:

Respawnable skal arve fra Actor Fire og Food skal arve fra Respawnable (og dermed også fra Actor)

Rent praktisk definerer vi den nye klasse Respawnable helt som vi definerede f.eks. Fire, altså ved at højre-klikke på Actor i klasse-diagrammet, og vælge New Subclass i menuen. Bemærk, at vi ikke vælger nogen grafik for Respawnable.

Dette var nemt nok, men hvordan gør vi så Fire og Food til sub-klasser af Respawnable? Her er Greenfoot desværre en smule klodset. Det ville være smart, hvis vi f.eks. bare kunne trække kassen for Food-klassen ned på Respawnable, hvorved Food så blev til en sub-klasse for Respawnable …men det kan man desværre ikke! I stedet bliver man nødt til at gå ind og ændre på selve koden for Food. Oprindeligt så klasse-definitionen for Food således ud:

public class Food extends Actor { // Og så videre…

Dette skal nu ændres til:

public class Food extends Respawnable { // Og så videre…

Samme ændring foretages for Fire-klassen. Når ændringen er foretaget, vil klasse-diagrammet se rigtigt ud:

Figur 7.9: Opdateret klasse-diagram med Respawnable

64

Med dette på plads, kan vi gå i gang med at flytte kode op i Respawnable-klassen. Det viser sig, at det er ganske lidt kode, vi behøver lade blive i Fire- og Food-klasserne. Kun to stumper kode i hver klasse er specifikke for netop den klasse:

setImage("fire_25.jpg"); // Kaldes i metoden respawn ... theViking.adjustHP(-3); // Kaldes i metoden interact

Alt anden kode er ikke specifik for den enkelte klasse. Vi kan altså gøre koden i Respawnable helt generel, hvis vi giver sub-klasserne ansvar for bare to ting:

Specificere filnavnet på den grafik, der er specifik for netop denne klasse. Specificere den del af interaktionen med vikingen, der er specifik for netop denne klasse.

Første punkt kan gøres på flere måder; her vælger vi at indføre en ekstra instans-variabel i Respawnable, der holder filnavnet på den klasse-specifikke grafik. For at være helt sikker på, at der altid bliver angivet et filnavn, ændrer vi på constructoren for Respawnable, således at den tager en tekststreng som parameter. En constructor kan sagtens have en parameter, og på den måde kan man forhindre, at nogen prøver at lave et objekt af denne klasse uden at angive et filnavn. Endelig skal kaldet af setImage selvfølgelig ændres, således at det bruger instans-variablen. Alt i alt får vi følgende ændringer i Respawnable:

private String imageFile; // Ny instans-variabel

public Respawnable(String theImageFile) // Constructor tager parameter { imageFile = theImageFile; // Vi sætter filnavnet på grafikken respawn(); } ... public void respawn() { isActive = true; timeLeftToRespawn = 0; setImage(imageFile); // Vi bruger det angivne filnavn }

Det andet punkt kan ligeledes gøres på flere måder. Den overordnede princip er at erstattet den klasse-specifikke interaktion med et kald til en ny metode, som netop rummer denne klasse-specifikke interaktion. Denne sidstnævnte metode skal så findes i sub-klassen, f.eks. i Fire-klassen. I Respawnable-klassen kommer interact derfor til at se således ud:

public void interact(Viking theViking) { if (isActive) { despawn(); specificInteract(theViking); // Skal defineres i sub-klassen } }

65

Ideen er således at vi ”udskyder” definitionen af specificInteract til sub-klassen, idet det jo er den som ved, hvordan interaktionen mellem den og vikingen specifikt skal være. Her er dog et lille problem. Hvis vi skriver interact således i Respawnable, og efterfølgende oversætter programmet, får vi en fejl. Oversæt-teren brokker sig over, at vi ikke har defineret specificInteract. Men det ville vi jo gerne vente med til sub-klassen… Vi kan jo ikke rigtig definere metoden til noget fornuftigt i base-klassen. Dette er et ret generelt problem i Objekt-Orienteret programmering:

Noget kode i en base-klasse skal benytte en metode, som vi gerne vil vente med at definere til vi skal skrive en sub-klasse til base-klassen

Der er ikke nogen fornuftig definition af metoden i base-klassen Vi skal være sikre på, at vi husker at definere denne metode i sub-klassen

Heldigvis er der en fornuftig måde at håndtere dette problem på; vi kan gøre specificInteract til en såkaldt abstrakt metode.

En abstrakt metode er et velkendt begreb i objekt-orienteret programmering. Det dækker netop over den situation, at vi ved vi skal bruge en bestemt metode, med vi kan ikke definere den før vi skal definere en sub-klasse. Dette kan vi fortælle oversætteren ved bruge ordet abstract i definitionen af metoden. I Respawnable kommer definitionen af specificInteract derfor til at se således ud:

public abstract void specificInteract(Viking theViking);

Bemærk, at der ikke følger noget kode efter denne linie – definitionen af metoden stopper simpelthen med denne linie!

Når en klasse rummer en eller flere abstrakte metoder, bliver klassen også selv abstrakt. Dette skal man også informere oversætteren om. Derfor skal selve definitionen af Respawnable også ændres en smule:

public abstract class Respawnable extends Actor

En konsekvens af at gøre en klasse abstrakt er, at man ikke længere kan lave objekter af netop denne type. Med andre ord giver følgende stump kode en fejl fra oversætteren:

Respawnable test = new Respawnable(“test.jpg”);

Det giver jo også fin mening – man kan sige at klasse-definitionen af Respawnable er ”ukomplet”, så derfor ville det ikke give mening at lave et objekt af denne type, og efterfølgende prøve at kalde interact på det objekt. Objektet ville jo ikke vide hvad det skulle gøre, når det mødte metoden specificInteract.

Med alle disse detaljer på plads kan vi endelig gøre brug af Respawnable, når vi skal (om)definere Fire- og Food-klasserne. Der bliver faktisk meget lidt tilbage i f.eks. Fire-klassen:

66

public class Fire extends Respawnable{ public Fire() { super("fire_25.jpg"); } public void act() { super.act(); }

public void specificInteract(Viking theViking) { theViking.adjustHP(-3); }}

Husk på, at ordet super benyttes til at kalde metoder i base-klassen, så f.eks. er kaldet super(”fire_25.jpg”) et kald til constructoren i Respawnable, hvormed vi sætter navnet på den specifikke grafik, der skal benyttes for et Fire-objekt.

Og hvorfor gik vi så gennem alle disse anstrengelser for at lave en base-klasse? Det smarte er nu, at hvis vi ønsker at lave en helt ny Actor, som er af samme type som Fire og Food (altså ”noget, som kan respawne”), skal vi blot definere det som er helt specifikt for denne nye klasse. Altså skal vi lave en klasse som Fire-klassen, blot med to forskelle:

Angive et andet filnavn for den klasse-specifikke grafik Angive den klasse-specifikke interaktion i metoden specificInteract

NB: Før du går i gang med næste opgave, hent da scenariet TVP_Kap7_Opg7_15_klar. Her er klassen Respawnable indført, og de beskrevne ændringer for Fire- og Viking er foretaget.

Opgave 7.15: Indfør en ny klasse MedKit, der fungerer på samme måde som f.eks. Food-klassen, blot giver den vikingen 10 health-points. Lav selv en passende grafik til klassen. Husk også at lave de nødvendige rettelser i TVPWorld og Viking-klassen. Et rimeligt antal MedKit-objekter i verdenen kunne være 10.

Opgave 7.16: Selv om klassen Dragon repræsenterer en lidt anderledes type figur end f.eks. Fire, er det jo også ”noget, som respawner”. Prøv at gøre Dragon til en sub-klasse af Respawnable.

NB: Før du går videre, hent da scenariet TVP_Kap7_Opg7_16_efter. Dette scenarie indeholder løsninger på opgave 7.15 og 7.16.

Når opgave 7.15 og 7.16 er gennemført, har vi således fire klasser i vores program, der alle arver fra base-klassen Respawnable. Det er forhåbentligt tydeligt, at det nu er noget nemmere at indføre en ny klasse af typen Respawnable, idet vi kun skal angive de dele af opførslen, der er helt specifikke for den nye klasse.

67

Det er nu også muligt at simplificere noget af koden i Viking-klassen. Koden i metoden interactWithObject har hidtil vokset med en lille bid, hver eneste gang vi har tilføjet en ny Actor, a la denne kode:

anObject = getOneObjectAtOffset(0, 0, MedKit.class); if (anObject != null) { interactWithMedKit((MedKit)anObject); return; } // Og så videre...

Ligeledes har vi måttet lave en ny interactWith… metode for hver ny klasse, a la:

private void interactWithMedKit(MedKit medkitObject) { medkitObject.interact(this); } // Og så videre...

Dette kan vi stryge nu, og erstatte med noget mere generel kode. Alle disse klasser er jo nu sub-klasser af Respawnable, og deres interaktion foregår – set fra et kode-synspunkt – helt på samme måde. Vi kan derfor omskrive interactWithObject til:

private void interactWithObject() { Actor anObject = getOneObjectAtOffset(0, 0, Respawnable.class);

if (anObject != null) { ((Respawnable)anObject).interact(this); } }

Med andre ord; hvis vi møder et objekt af typen Respawnable, så interagerer vi med det, ved at kalde interact metoden på objektet. Meget simpelt, og vi behøver nu ikke længere rette i Viking-klassen, hvis vi vil tilføje en ny klasse af typen Respawnable!

Kan man også ændre på koden, således at vi heller ikke skal rette i TVPWorld-klassen, hvis vi indfører en ny klasse af typen Respawnable? Dette er noget vanskeligere. Dette er trods alt stedet, hvor vi angiver præcis hvilke og hvor mange typer objekter vi vil have i verdenen, så hvis vi vil have en ny type objekt i verdenen, skal det angives her. Vi ændrer derfor ikke på koden i TVPWorld i denne forbindelse.

7.11.3 Indførsel af base-klasse Interactable

Indførelsen af base-klassen Respawnable har muliggjort en betydelig forenkling af koden. Men er det egentlig den helt rigtige base-klasse? Lige nu er spillets logik sådan, at alle objekter man kan interagere med er ”ting, som kan respawne”. På sigt kunne man vel godt forestille sig objekter, der ikke har en respawn-opførsel, men som vikingen stadig kan interagere med, f.eks. en NPC (Non-Player Character) i spillet. Vi bør

68

derfor have en mere fundamental base-klasse, som alle ”ting, som kan interagere” skal arve fra. Denne klasse kunne passende kaldes Interactable. Hvis vi indfører denne klasse, vil den skulle ligge mellem Actor og Respawnable. Altså arver Interactable fra Actor, mens Respawnable arver fra Interactable:

Figur 7.10: Opdateret klasse-diagram med Interactable

På et tidspunkt kunne vi så f.eks. indføre en base-klasse Permanent for objekter der ikke respawner – denne klasse vil så skulle være på niveau med Respawnable i klasse-diagrammet.

Fordelen ved denne ekstra base-klasse er, at vi pr. definition kan interagere med alle objekter af typen Interactable, og kun dem. Derfor skal vi være sikre på, at alle objekter af typen Interactable har en metode interact. Altså skal metoden interact ligge på klassen Interactable. Her har vi lidt det samme problem som tidligere: vi skal have indført metoden interact på klassen Interactable, men vi har ikke nogen fornuftig måde at kode den på i base-klassen…

Opgave 7.17: Indfør metoden interact på klassen Interactable, men som en abstrakt metode! Selve koden for metoden skal forblive i Respawnable (hvorfor?)

Det faktum at vi udelukkende kan interagere med objekter af type Interactable, gør det muligt at forenkle metoderne meetsObject og interactWithObject i Viking-klassen. I praksis gør vi det ved at erstatte dem med to andre metoder getInteractableObject og interactWithObject:

private Interactable getInteractableObject() { Actor anObject = getOneObjectAtOffset(0, 0, Interactable.class); return (Interactable)anObject; }

private void interactWithObject(Interactable theObject) { if (theObject != null) { theObject.interact(this); } }

69

Den første metode undersøger, om vikingen har mødt et andet objekt, som det kan interagere med. Mere specifikt returneres det objekt af typen Interactable, der befinder sig på samme position som vikingen har lige nu. Hvis der ikke findes et sådant objekt, returneres null. Den anden metode kalder ganske enkelt interact på det givne objekt, såfremt det ikke er null. Igen er det vigtigt at huske, at hele denne øvelse gør det muligt at tilføje nye klasser af typen Interactable, uden vi skal ændre i koden i Viking-klassen. Viking-klassen kender ikke længere noget til specifikke klasser under Interactable.

Endelig gør de nye metoder det muligt at skrive selve act-metoden for Viking-klassen lidt om, selv om der mest er tale om lidt kosmetisk forbedring:

public void act() { if (!isDead()) { move(); // Flyt Vikingen interactWithObject(getInteractableObject()); // Interagér med andre objekter if (isDead()) { doDeathBehavior(); } } }

70

7.12 Scenariet TVP_Kap7_Opg7_17_efter

Vi fortsætter nu fra det fælles udgangspunkt TVP_Kap7_Opg7_17_efter. I dette scenarie er MedKit-klassen inkluderet, ligesom base-klasserne Respawnable og Interactable er indført som beskrevet. Med udgangs-punkt i dette scenario skal vi nu føje noget noget mere funktionalitet til spillet. Mere specifikt vil vi tilføje:

Et ”infoboard”, hvor diverse informationer kan vises En øvre grænse for værdien af health-points Såkaldte ”experience points”, ofte kaldet XP

7.12.1 Indførelse af Infoboard

En ret stor mangel ved spillet lige nu er manglen på information til spilleren om vikingens tilstand. Lige nu kan vi kun afgøre om vikingen er død eller levende, med mindre vi pauser spillet, og bruger Inspect til at se hvor mange health-points vikingen har lige nu. Denne information bør løbende blive vist til spilleren.

I de fleste spil er et vist område af skærmen afsat til at vise den slags information. Vi vil gøre det samme her, ved at indføre et såkaldt infoboard. På dette infoboard skal det være muligt at vise forskellig relevant information til brugeren. I første omgang vil vi blot benytte det til at vise den aktuelle værdi af health-points – senere kan yderligere informationer blive tilføjet.

Indtil nu har verdenen i TVP bestået af en kvadratisk flade, delt op i 32x32 celler. Vi vil nu reservere en del af dette area til vores nye infoboard, mere specifikt de nederste 4 rækker. Præcist hvordan infoboardet skal se ud vender vi tilbage til, men blot det at reservere de nederste 4 rækker til infoboardet giver et par komplikationer, vi skal have løst. Lige nu benytter vi – naturligvis – alle 32x32 celler i spillet, hvilket mere specifikt vil sige:

Enhver af de 32x32 celler kan blive befolket af et Actor-objekt En flytbar Actor kan gå rundt på alle 32x32 celler

Hvis de nederste fire rækker af celler er optaget af vores infoboard, skal disse celler naturligvis være tomme, så vi må derfor sætte nogle begrænsninger ind i forhold til hvilke celler der kan befolkes og ”besøges”. Bemærk, at vi ikke bare kan gøre vores verden mindre; infoboardet selv skal også være en Actor, og skal derfor have en plads inde i selve verdenen.

Opgave 7.18: Indfør de ovennævnte begrænsninger i programmet, således at man ikke kan sætte en Actor i de nederste fire rækker, og heller ikke kan bevæge sig ind i dem. Tip: Se på metoden populateActorType i klassen TVPWorld, samt metoden canMoveRelative i klassen Viking. Disse metoder skal sikkert ændres, og måske endda gøres mere generelle.

71

En løsning på opgave 7.18 ligger i scenariet TVP_Kap7_Opg7_18_efter, som vi fortsætter fra. Bemærk, at dette scenario også rummer klassen InfoBoard, som vi beskriver i det følgende.

Vi har nu gjort plads til vores infoboard i TVP-verdenen. Næste skridt er at få lavet et passende infoboard. Inden vi tager hul på dette, er det det relevant at overveje, hvilke typer information der overhovedet skal kunne vises på sådan et infoboard. På et omfattende infoboard som f.eks. i World of Warcraft vises der meget forskellig information, der præsenteres på flere former, f.eks. som tal, tekst, ikoner, ”bars” med farver, og så videre. Vi vil gå efter et noget simplere infoboard, hvor vi kan vise information i form af tekst, tal og ”bars”. Den information vi i første omgang vi vise er information omkring health-points, hvilket jo er den eneste relevante information om en viking lige nu. Infoboardet skal dog være lidt forberedt for fremtiden, så vi vælger et design hvor der også kan vises experience points, en log over spillets forløb, og desuden et område reserveret til senere udvidelser:

Figur 7.11: Skitse over Infoboard

Med andre ord vil vi gerne vise f.eks. det aktuelle antal health-points (HP) dels som et tal, dels som en farvet ”bar”, der kan være nemmere at opfatte mens spillet pågår.

Hvis man ser i koden for scenariet TVP_Kap7_Opg7_18_efter, vil man se at det også rummer en ny klasse Infoboard. Denne klasse rummer netop kode for et infoboard som det beskrevne, der – til at starte med – kan holde styr på health-points. Selve koden for at lave et infoboard er en smule kompliceret, og involverer en del brug af metoderne i klassen GreenfootImage, som er en del af Greenfoot. Koden gennemgås ikke i detaljer her, se i stedet den kommenterede kode i selve klassen.

Vi ridser dog lige hovedprincipperne i koden op. Selve infoboardet består af et sort rektangel, med målene 600 pixels bredt og 60 pixels højt. På dette sorte rektangel kan der så tegnes tekst og farvede ”bars” på specifikke positioner i rektanglet, hvilket gøres med to metoder drawText og drawBar, henholdsvis. Tegningen af tekst og bar til at vise health-points er samlet i en metode drawHP, der således kalder de to foregående metoder. En metode til at vise f.eks. experience-points vil skulle kodes på samme vis.

Udefra set er det kun metoden updateInfoItem, man kan kalde. Ideen er, at man i kaldet kan angive hvilken del af infoboardet der skal opdates (ved at angive en InfoItemType, se koden), samt hvad den nye værdi for denne specifikke del er. Hvis vikingens health-points ændrer sig til f.eks. 30 points, vil man skulle foretage kaldet updateInfoItem(Inforboard.InfoItemType.hp,30) , hvor den første parameter således angiver, at det er health-points der er blevet opdateret.

Hvis vi har klassen InfoBoard til rådighed, er det ikke de store ændringer vi skal lave i resten af koden for at få vist health-points på vores infoboard:

72

Ændringer i TVPWorld:

public InfoBoard theInfoBoard; // Vi skal have en InfoBoard-variabel ... public void populateTVPWorld() { ... createInfoBoard(); // Gøres under initialiseringen ... }

// Lav et InfoBoard objekt, og sæt det centreret nederst i verdenen private void createInfoBoard() { theInfoBoard = new InfoBoard(); addObject(theInfoBoard, (limitX / 2), limitY + 2); }

Ændringer i Viking:

// Når HP bliver opdateret, skal vi også kalde updateInfoItem på variablen // theInfoBoard af typen InfoBoard, som ligger i TVPWorld public void setHP(int newHP) { healthPoints = newHP; ((TVPWorld)getWorld()).theInfoBoard.updateInfoItem( InfoBoard.InfoItemType.hp,healthPoints); }

Med andre ord; I TVPWorld skal vi sørge for at få sat vores infoboard rigtigt op, og så ellers kalde det fra Viking, når vikingens health-points ændres.

7.12.2 Øvre grænse for health-points

Hvis man studerer koden for at tegne den ”bar”, der skal vise den nuværende værdi af health-points, vil man se at bredden af baren er sat til 3 gange antallet af health-points. Dette er valgt lidt tilfældigt, og der er også nogle problemer i dette. Der er jo ingen øvre grænse for antallet af health-points, så hvad nu hvis vikingen efter lang tids spil har f.eks. 1000 health-points? Så skal baren være 3000 pixels bred… På den anden side nytter det heller ikke noget, at baren er alt for smal ved ”rimelige” værdier. I det hele er det også noget urealistisk, at der ikke er nogen øvre grænse for health-points. Hvis vi sætter den øvre grænse til 100 health-points, bliver spillet mere realistisk, og vi er tillige sikre på at baren ikke løber ud over kanten.

Opgave 7.19: Indfør en øvre grænse på 100 for værdien på health-points.

NB: Før du går videre, hent da scenariet TVP_Kap7_Opg7_19_efter. Dette scenarie indeholder en løsning på opgave 7.19.

73

Når vi nu har bedre styr på antallet af health-points, kan vi også nemmere indføre en mere indikativ farvning af baren for health-points. Lige nu er den altid grøn, lige meget hvad værdien er. Ofte benyttes farverne gul og rød for at indikere, at ens health-points er ret lave, henholdsvis meget lave. En bedre strategi for at farve baren kunne således være:

Health-points 10 % eller lavere af maximum: rød Health-points mellem 10 % og 50 % af maximum: gul Health-points 50 % eller mere af maximum: grøn

Opgave 7.20: Indfør ovenstående regler for at farve baren for health-points. Dette kan f.eks. gøres ved at definere en metode i InfoBoard-klassen, der kan returnere et passende Color-objekt, givet en værdi for health-points. Denne metode kan så benyttes i kaldet af drawBar, som udføres i kaldet af drawHP.

NB: Før du går videre, hent da scenariet TVP_Kap7_Opg7_20_efter. Dette scenarie indeholder en løsning på opgave 7.20.

7.12.3 Indførsel af experience-points

I øjeblikket er der ikke meget andet formål med spillet end at nedlægge drager i det uendelige, mens man blot sørger for at holde sig i live. I langt de fleste RPGs er der en eller anden form for belønning, når man har nedlagt et vist antal fjender, eller f.eks. klaret et vist antal opgaver i spillet. Denne belønning kan være i form af bedre evne til at nedlægge fjender, forøget antal maksimale health-points, og så videre. Til brug herfor skal man have defineret et eller andet mål for, hvor langt man er kommet i spillet. Dette mål kaldes ofte for experience-points (måske ”erfaringspoint” på dansk), og forkortes som regel XP.

I første omgang vil vil blot indføre XP på en simpel vis, hvor man får et antal XP for at nedlægge en fjende. Senere skal der være en form for reaktion fra spillet, når spilleren når et vist antal XP. Dette vender vi tilbage til senere.

Opgave 7.21: Indfør XP i spillet, på den måde at vikingen får 10 XP for at nedlægge en drage. Det er en relativt omfattende opgave at indføre XP (husk at XP også skal fremgå af vores infoboard), men prøv at følge strategien for hvordan der holdes styr på health-points i spillet. Det er muligt at indføre XP på en lignende måde.

74

7.13 Scenariet TVP_Kap7_Opg21_efter

Vi fortsætter nu fra det fælles udgangspunkt TVP_Kap7_Opg7_21_efter. I dette scenarie er ændringerne svarende til opgave 7.21 indført, det vil sige vi nu får XP for at nedlægge drager, og disse XP vises på vores info-board på tilsvarende vis som health-points.

I det følgende vil vi koncentrere os om at give vores eneste fjende p.t. (Dragon-klassen) en mere realistisk opførsel. Lige nu vandrer en drage tilfældigt rundt i verdenen, og det er udelukkende op til vikingen selv at søge en ”konfrontation” med en drage – dragen er med andre ord ikke opmærksom på vikingens position i verdenen. I mange RPG – og i computer-spil hvor man kæmper mod computer-styrede væsener i det hele taget – vil sådan et væsen have forskellige tilstande i forhold til hvordan det reagerer på spillerens tilstede-værelse i spillets verden:

Passivt – væsenet går rundt på må og få i verdenen, eller følger en i programmet forudbestemt bane, således at det ”patruljerer” et vist område i spillet.

Aggressivt – væsenet søger hen imod spilleren, og angriber spilleren når det har mulighed for det. Defensivt – væsenet søger væk fra spilleren. Dette sker oftest, når væsenet er ved at dø.

Indtil nu har vores Dragon-klasse udelukkende udvist passiv opførsel, hvor en drage går tilfældigt rundt i verdenen. Dette er ganske enkelt at programmere. Vi vil nu prøve at tilføje aggressiv opførsel til Dragon-klassen. Bemærk, at dette ikke betyder at en drage fra nu af udelukkende vil opføre sig aggressivt. Dette ville sandsynligvis gøre spillet for svært, hvis alle drager i spillet konstant går målrettet mod spilleren. Med andre ord skal vi definere regler for, hvornår en drage skifter mellem de to tilstande passiv og aggressiv.

En klassisk måde at define regler for et væsens aggressivitet på er i form af en ”aggressions-radius”. Et simpelt regelsæt baseret på dette er:

1. Som udgangspunkt er væsenet passivt, d.v.s. det går tilfældigt rundt, eller patruljerer et givent område i verdenen.

2. Hvis afstanden mellem spilleren og væsenet bliver mindre end aggressions-radius, skifter væsenet til aggressiv opførsel, og søger derfor hen mod spilleren.

3. Væsenet forbliver i aggressiv tilstand, til:a. Det dør.b. Afstanden mellem spiller og væsen igen bliver større end aggressions-radius.

Vi prøver nu at få formuleret disse regler som Java-kode. Først og fremmest kan vi bemærke, at der – i første omgang – udelukkende skal ændres i Dragon-klasssen, idet det kun er drager der skal ændre deres opførsel. En drage kan nu opføre sig på mere end én måde, altså er en drages tilstand mere kompliceret nu. Vi indfører derfor en ny type i Dragon-klassen, der beskriver en drages ”mentale tilstand”, og en ny instans-variabel, der gemmer den mentale tilstand for et givent Dragon-objekt. Denne nye instans-variabel skal selvfølgelig også sættes til en fornuftig værdi i constructoren for Dragon-klassen.

private enum MentalState {passive, aggressive}; private MentalState theMentalState;

75

public Dragon() { super("dragon_25.jpg"); theMentalState = MentalState.passive; }

Dermed er en drage som udgangspunkt i den passive tilstand. Man kan godt overveje, om vi ikke bare kunne have brugt en boolean her i stedet, når der nu kun er to mulige værdier for den mentale tilstand. Det kunne vi godt, men her tænker vi en lille smule fremad ved at bruge en enumereret type. På et senere tilds-punkt vil vi måske indføre en defensiv tilstand også, og da vil det være noget nemmere blot at skulle tilføje en ny værdi til den enumererede type, frem for at skifte fra en boolean til en enumereret type på det tidspunkt.

Næste skridt er en metode til at udregne dragens øjeblikkelige mentale tilstand. Da både dragen og vikingen kan flytte sig hver gang act bliver kaldt, bør vi tillige udregne den nye mentale tilstand i hvert kald af act. Heldigvis er den mentale tilstand jo en simpel funktion af afstanden mellem dragen og vikingen.

private MentalState calculateMentalState() { if (distanceToViking() > aggressionRadius) { return MentalState.passive; } else { return MentalState.aggressive; } }

Her har vi brugt ”ønskefe”-princippet to gange; for at kode metoden på denne måde, skal vi dels bruge en metode til at udregne den nuværende afstand mellem dragen og vikingen, dels en konstant der definerer en passende aggressions-radius. Det sidste er simpelt nok, og vi har derfor tilføjet en konstant til koden:

private static final double aggressionRadius = 10.0;

Om dette vitterligt bør være en konstant, eller måske en parameter, vender vi tilbage til.

Anden del er noget mere kompliceret. For at kunne udregne afstanden mellem dragen og vikingen skal vi bruge vikingens nuværende position. Hvor skal vi få den fra? Problemet minder lidt om da vi indførte et infoboard. Et infoboard hører hjemme i TVPWorld-klassen, og da vi fik brug for at opdatere infoboardet fra Viking-klassen, måtte vi skrive en lidt kryptisk kode (bemærk, at dette er én linje kode, selv om den i teksten er delt over to linjer):

((TVPWorld)getWorld()).theInfoBoard.updateInfoItem( InfoBoard.InfoItemType.hp,healthPoints);

76

Hvad er det egentlig der foregår her? Vores mål med denne linje kode er få opdateret det infoboard, der hører til TVPWorld. Dette infoboard eksisterer i form af en instans-variabel i TVPWorld-klassen, kaldet theInfoBoard. Denne variabel skal vi have fat i, fordi vi så kan kalde metoden updateInfoItem på denne variabel (som jo er et objekt af typen InfoBoard, og metoden updateInfoItem er en public metode i denne klasse). I Greenfoot findes metoden getWorld(), som enhvert Actor-objekt kan kalde for at få en reference til den verden, pågældende objekt lever i. Da dette er en helt generel metode i Greenfoot, kan den kun returnere en reference af typen World, ikke TVPWorld. TVPWorld er jo en klasse vi har defineret, så den kender Greenfoot ikke noget til. Det er et problem, fordi variablen updateInfoItem jo netop findes i klassen TVPWorld. Det vil derfor ikke give mening at skrive:

getWorld().theInfoBoard.updateInfoItem(...); // NB: Virker ikke!!

Her vil oversætteren brokke sig, fordi vi prøver at få fat i en variabel, der ikke findes i World-klassen. Vi kan imidlertid ”snigløbe” oversætteren lidt. Vi ved jo godt, at den eneste slags verden der findes i vores pro-gram netop er en verden af typen TVPWorld. Vi kan derfor sige til oversætteren: Vi ved godt at getWorld() returnerer noget af typen World, men vi ved at det rent faktisk altid har typen TVPWorld. Derfor laver vi lige typen om til TVPWorld i stedet for. Det er præcis det vi gør med den kryptiske syntaks:

((TVPWorld)getWorld()).theInfoBoard.updateInfoItem(...);

Dermed kan vi godt få fat på variablen theInfoBoard, og dermed få udført vores opgave.

Hvorfor al denne snak om, hvordan vi – med noget besvær – kan fiske en variabel ud fra TVPWorld fra en Actor-klasse? Fordi vi bliver nødt til at gøre noget lignende her. Det eneste Viking-objekt der lever i vores verden lever netop også i TVPWorld, så måske kan vi få vikingens position ved at følge en lignende strategi. Imidlertid er der ikke nogen variabel for vikingen i TVPWorld, så der skal lidt mere til.

Heldigvis er det sådan, at Greenfoot rummer metoder til at finde alle objekter af en given klasse, som p.t. findes i verdenen. Denne metode hedder getObjects(…), og returnerer en liste af alle objekter af en given klasse, svarende til det klasse-navn, man giver som parameter til metoden. For at få en liste af alle Viking-objekter i verdenen, skal vi – et eller andet sted i TVPWorld-klassen – foretage kaldet:

java.util.List theList = getObjects(Viking.class);

Nu vil theList rumme alle Viking-objekter. Vi ved jo godt, at der kun vil være et enkelt objekt i denne liste, idet vi kun indsætter én viking i verdenen. Vi kan derfor nu skrive en metode getViking(), der returnerer det ene Viking-objekt til kalderen af metoden:

public Viking getViking() { java.util.List theList = getObjects(Viking.class);

if (theList.size() > 0) { return (Viking)theList.get(0); } else { return null; } }

77

For at sikre os mod ubehagelige overraskelser kontrollerer vi, at listen ikke er tom. Hvis listen mod forventning er tom, returneren vi null. Dette burde ikke kunne ske, men som regel er det en god ide at programmere defensivt… Hvis listen rummer mindst et element, returnerer vi det første element (element nummer 0). Bemærk, at vi også her bliver nødt til at sige til oversætteren, at vi godt ved at dette objekt altid er et Viking-objekt.

Med denne metode kan vi nu fra Dragon-klassen godt få fat på Viking-objektet, på denne vis:

Viking theViking = ((TVPWorld)getWorld()).getViking()

Efterfølgende kan vi så kalde de relevante metoder på theViking variablen.

NB: Før du går videre, hent da scenariet TVP_Kap7_Opg7_22_klar. I dette scenarie er de ovenstående ændringer indkluderet.

Opgave 7.22: I scenariet TVP_Kap7_Opg7_22_klar er getViking() metoden med i TVPWorld-klassen, men metoden distanceToViking() i Dragon-klassen er ikke gjort færdig. Gør nu metoden færdig ved at kalde de relevante metoder på variablen theViking. Hint: Pythagoras…

7.13.1 Revision af move()-metoden

Idet vi nu kan beregne afstanden mellem dragen og vikingen, kan vi sætte den mentale tilstand af dragen korrekt. Nu skal vi til at bruge den til noget. Overordnet set er anvendelsen ret simpel; en given mental tilstand udmønter sig i et konkret bevægelsesmønster for dragen, så move()-metoden i Dragon-klassen må skulle revideres. Lige nu er move() kodet til altid at give et passivt bevægelsesmønster, hvor dragen går tilfældigt rundt i verdenen. Denne metode omdøber vi derfor til movePassive(). Selve move()-metoden gøres mere generel:

private void move() { switch (theMentalState) { case passive: movePassive(); break; case aggressive: moveAggressive(); break; default: break; } }

Med andre ord; hvis den mentale tilstand er passiv, så kald movePassive(), hvis den mentale tilstand er aggressiv, så kald moveAggressive(). Her kunne man godt overveje blot at bruge en if-sætning i stedet for den mere generelle switch-sætning, men igen er argumentet at denne kode vil være nemmere at ændre, hvis vi f.eks. vil udvide med en defensiv mental tilstand.

78

Selve act-metoden skal naturligvis også ændres, så vi rent faktisk bruger metoden calculateMentalState til noget. Den kommer til at se således ud:

public void act() { super.act();

if (isObjectActive()) { theMentalState = calculateMentalState(); // Ny!! move(); } ...

Bemærk, at vi i den nye version af move() igen har brugt ønskefe-princippet, idet vi jo ikke har skrevet en moveAggressive() metode endnu. Dette er en lille smule kompliceret. De helt basale regler for at bevæge sig aggressivt – d.v.s. hen imod vikingen – er i sig selv ret enkle, set fra dragens synspunkt:

1. Hvis vikingen er sydligere end dig selv, så gå mod syd, ellers2. Hvis vikingen er nordligere end dig selv, så gå mod nord, ellers3. Hvis vikingen er vestligere end dig selv, så gå mod vest, ellers4. Hvis vikingen er østligere end dig selv, så gå mod nord, ellers5. Står du oven på vikingen, og han vil angribe dig!

Der er dog den komplikation, at dragen stadig ikke må gå ind i vægge eller udenfor verdenens grænser, så alle de ovenstående regler er betinget heraf. Heldigvis er der jo taget hensyn til dette i den eksisterende metode moveRelative, der benyttes når dragen bevæger sig passivt rundt. Vi kan derfor kode en ret simpel version af moveAggressive på følgende vis:

private void moveAggressive() { Viking theViking = ((TVPWorld)getWorld()).getViking(); int xDiff = theViking.getX() - getX(); int yDiff = theViking.getY() - getY(); int xRel = 0; int yRel = 0; if (xDiff < 0) { xRel = -1;} if (xDiff > 0) { xRel = 1; } if (yDiff < 0) { yRel = -1;} if (yDiff > 0) { yRel = 1; } if (canMoveRelative(xRel, yRel)) { moveRelative(xRel, yRel); } }

Opgave 7.23: Prøv at læse koden for metoden moveAggressive grundigt (den er inkluderet i scenariet TVP_Kap7_Opg7_22_klar), med det mål at forstå, hvordan den realiserer de regler, som vi definerede

79

tidligere for et aggressivt bevægelsesmønster. Er der noget i koden, der ikke helt passer med definitionen af reglerne?

Opgave 7.24: Prøv at spille spillet et stykke tid. Er der nogen uhensigtsmæssigheder i den måde, dragerne nu bevæger sig på? Hvordan kan det i givet fald ændres (tænk ikke på kode, bare på regler)?

Som en hjælp til at se, hvornår en drage bliver aggressiv, bliver grafikken for en drage ændret, når den bliver aggressiv. Den bliver ændret tilbage til den oprindelig grafik, når dragen igen bliver passiv.

Som spillet er lige nu, er det temmelig svært at komme væk fra en drage, hvis først den er blevet aggressiv. Alle spillere går jo rundt i verdenen med samme hastighed, så hvis først man har 2-3 drager i hælene, er man næsten prisgivet. Vi kan gøre spillet lidt lettere ved at sænke dragernes hastighed. Dette kan gøres på flere måder, men de handler alle om at gøre kaldet af move() inde i act()-metoden afhængig af en eller anden form for betingelse, i stedet for – som det er nu – altid at kalde move(). Med andre ord vil vi kun rykke dragen i visse kald af act(), i stedet for i alle kald.

En måde at gøre dette på er ved at indføre en hastighed for drager. Vi kan f.eks. definere, at alle væsener i vores verden bevæger sig med en hastighed på mellem 0 og 100 (enheder er ikke så vigtige). Ideen er, at en hastighed på 100 indikerer, at væsenet flytter sig i alle kald af act(). Tilsvarende vil en hastighed på 60 indikere, at væsenet flytter sig i omkring 60 % af alle kald af act(), og så videre.

Ved at indføre en hastighed for væsener udvider vi jo tilstanden for sådan et væsen. I vores Dragon-klasse skal vi derfor tilføje endnu en instans-variabel, som vi kalder speed:

private static final int dragonSpeed = 60; // Ny!! ... private int speed; // Ny!! ...

public Dragon() { super("dragon_25.jpg"); theMentalState = MentalState.passive; speed = dragonSpeed; // Ny!! }

Vi går gennem de samme skridt som tidligere, dvs. vi indfører en instans-variabel, vi initialiserer den i constructoren, og vi har defineret en konstant for en drages hastighed (dette er måske ikke den smarteste måde at gøre det på, men det vender vi tilbage til).

Det mest komplicerede ved at indføre denne hastighed er, hvordan vi skal styre kaldene til move() afhæng-igt at hastigheden. Effekten skal jo være, at move() kun kaldes i et omfang, der svarer til hastigheden. En måde at gøre dette på er at lave et tilfældigt tal mellem 0 og 100 i hvert kald af act(). Hvis det tilfældige tal er mindre end hastigheden, kalder vi move(), ellers ikke. Så jo lavere værdi af hastigheden, jo lavere er sandsynligheden for at vi kalder move(). Man kan argumentere, at vi risikere at en drage kan få en ret ”ujævn” hastighed på denne måde, idet der kan komme lange sekvenser af f.eks. meget lave tal. Det er korrekt, men man kan også argumentere, at dette egentlig er mere realistisk, idet et virkeligt væsen ikke vil

80

løbe med en helt konstant hastighed. Hastighed skal altså i denne sammenhæng forstås som en gennem-snitshastighed. Den reelle hastighed kan variere noget over kortere distancer.

For at kode denne strategi skal vi ændre i koden for act()-metoden i Dragon-klassen:

public void act() { super.act(); if (isObjectActive()) { theMentalState = calculateMentalState(); Random generator = new Random(); int randomNumber = generator.nextInt(100); if (randomNumber < speed) { move(); } } }

Vi har altså fået ”indkapslet” kaldet af move() i en betingelse, som gør at metoden kun bliver kaldt hvis det tilfældige tal er lavere end den for dragen definerede hastighed.

NB: Før du går videre, hent da scenariet TVP_Kap7_Opg7_25_klar. I dette scenarie er de ovenstående ændringer indkluderet, og hastigheden for drager er sat ned til 60.

Opgave 7.25: Bedøm, hvordan det har ændret på spillets sværhedsgrad at ændre på dragernes hastighed. Eksperimentér eventuelt med andre hastigheder, ved at ændre på værdien af konstanten dragonSpeed.

7.13.2 At slippe en drage fri…

En anden uhensigtsmæssighed ved dragers opførsel lige nu, er deres tendens til at ”sidde fast”, hvis der ikke er fri passage hen imod vikingen, når dragen er aggressiv. Denne opførsel ses, når dragen jagter vikingen omkring bygningen i den nordlige del af verden; hvis der er en mur på den korteste vej mellem dragen og vikingen, vil dragen standse op og ikke flytte sig, så længe vikingen heller ikke flytter sig:

81

Figur 7.12: Dragen sidder fast på grund af muren

At løse dette problem er helt generelt ret vanskeligt. Vi skal på en eller anden måde få dragen til at forstå, at den bliver nødt til at gå væk fra vikingen (mere præcist: øge sin afstand målt i fugleflugt), hvis den skal have nogen chance for at komme helt hen til vikingen. Lidt som at prøve at lære en flue, at det ikke nytter noget at blive ved med at flyve ind i ruden…

En meget simpel strategi kunne være følgende: Hvis dragen ikke kan foretage sit ”optimale” træk på grund af en blokerende væg, så lad den foretage et tilfældigt træk i stedet, så den kan komme væk fra det sted den sidder fast. Det lyder i teorien meget smart, men i praksis vil man meget ofte se, at dragen foretager et enkelt træk væk fra vikingen, for derefter straks at gå hen til det sted, hvor den før sad fast… Der skal altså noget mere avanceret til.

En ny strategi kunne være at generalisere ovenstående strategi til at vare længere tid. Hvis en drage sidder fast, skal den vandre tilfældigt rundt et stykke tid – hvorved den forhåbentlig kommer helt fri af sin låste position – for derefter at genoptage sin aggressive opførsel. Dette giver i hvert fald større mulighed for f.eks. at kunne komme rundt om et hjørne. For at kunne kode denne opførsel, er der et par ting, vi skal have styr på:

1. Hvordan opdager en drage, at den sidder fast?2. Hvor længe skal dragen vandre tilfældigt rundt, før den genoptager sin aggressive adfærd?

Det første spørgsmål er ret simpelt at besvare. I slutningen af koden for metoden moveAggressive() tester vi for, om det optimale træk rent faktisk kan udføres:

if (canMoveRelative(xRel, yRel)) { moveRelative(xRel, yRel); }

Hvis betingelsen ikke er opfyldt, er det jo netop fordi vi ikke kan foretage det ønskede træk. Således skal vi udvide if-sætningen med en else-del, hvor vi skal indikere, at dragen nu skal bevæge sig tilfældigt rundt et vist stykke tid.

Hvordan holder vi rede på dette ”stykke tid”? Igen er det nødvendigt at udvide dragens tilstand, idet tiden der er tilbage til at skifte mental tilstand jo i sig selv er noget ekstra tilstand, vi indfører for at opnå denne opførsel. Vi indfører derfor instans-variablen lurkingTimer til dette formål. Variablen gives dette navn med en bagtanke, idet vi vil indføre en ny mental tilstand for dragen, der skal dække over denne tilstand, hvor dragen godt nok bevæger sig tilfældigt rundt, men venter på at gå tilbage til aggressiv tilstand. Vi kalder denne tilstand for ”lurende” (engelsk: lurking), og indfører derfor dette som en tredje mulig mental tilstand i Dragon-klassen:

private enum MentalState {passive, aggressive, lurking};

82

Den udvidede betingelse i metoden moveAggressive() kommer derfor til at se således ud:

if (canMoveRelative(xRel, yRel)) { moveRelative(xRel, yRel); } else // Ny!! { lurkingTimer = lurkingMaxTime; }lurkingMaxTime er en konstant der angiver, hvor lang tid (dvs. hvor mange kald af act()) dragen skal forblive i lurking-tilstanden. Vi bruger med andre ord værdien af lurkingTimer til at afgøre, om dragen skal være i den mentale tilstand lurking. Derfor skal koden for metoden calculateMentalState også ændres lidt:

private MentalState calculateMentalState() { if (lurkingTimer > 0) // Ny!! return MentalState.lurking; // Ny!! if (distanceToViking() > aggressionRadius) { return MentalState.passive; } else { return MentalState.aggressive; } }

Dermed skulle der være styr på den nye mentale tilstand, bortset fra to nok så vigtige detaljer:

1. Hvad skal vi gøre i move()-metoden, når vi er i lurking-tilstanden?2. Hvordan kommer vi ud af lurking-tilstanden?

For at holde den generelle ”stil” i move()-metoden indfører vi endnu en case i den switch-sætning, som metoden består af:

private void move() { switch (theMentalState) { case passive: movePassive(); break; case aggressive: moveAggressive(); break; case lurking: // Ny!! moveLurking(); // Ny!! break; // Ny!! default: break; } }

Det kan umiddelbart virke lidt overflødigt at indføre en helt ny metode moveLurking, når ideen var at dragen netop bare skulle gå tilfældigt rundt, når den er i denne tilstand. Vi burde således bare kunne kalde

83

movePassive i dette tilfælde også. Det er forsåvidt rigtigt, blot skal der lige ske en enkelt ting mere i dette tilfælde. Vi skal jo også ud af lurking-tilstanden på et tidspunkt, og da instans-variablen lurkingTimer netop holder styr på antallet af skridt før vi skal ud af tilstanden igen, skal den tælles én ned i dette skridt. Koden for moveLurking bliver således:

private void moveLurking() { lurkingTimer = lurkingTimer - 1; movePassive(); }

Det er ganske korrekt, at man sagtens kunne have skrevet disse to linier kode direkte i casen for lurking i move()-metoden; om man vælger dette, eller at lave en separat metode, er lidt smag og behag.

NB: Før du går videre, hent da scenariet TVP_Kap7_Opg7_26_klar. I dette scenarie er de ovenstående ændringer indkluderet, og den mentale tilstand lurking er indført.

Opgave 7.26: Prøv at spille spillet, og se hvordan dragens tilbøjelighed til at sætte sig fast har ændret sig. Prøv eventuelt at variere på værdien af konstanten lurkingMaxTime i Dragon-klassen. Dette ændrer på den tid, dragen befinder sig i lurking-tilstanden. Prøv gerne både højere og lavere værdier end den oprindelige værdi.

7.14 Scenariet TVP_Kap7_Opg7_26_klar

Vi fortsætter nu fra det fælles udgangspunkt TVP_Kap7_Opg7_26_klar. Vi ser på følgende problem-stillinger:

Generalisering af egenskaber fra Dragon-klassen til en mere generel Moving-klasse Mulighed for at vinde spillet Indførsel af sværhedsgrader

7.14.1 Udviking af en Moving-klasse

Vores Dragon-klasse rummer efterhånden flere egenskaber, som kan være nyttige for andre væsener af typen væsener-som-bevæger-sig, dvs. væsener som:

Kan have forskellige sindstilstande, i forhold til spillets omstændigheder Kan bevæge sig rundt i verdenen efter bevægelsesmønstre, der afhænger af deres sindstilstand

Hvis vi vil indføre en ny slags fjende i spillet, f.eks. en edderkop, vil den ganske naturligt også skulle have disse egenskaber. I stedet for at kode dem endnu en gang, vil vi flytte dem op i en base-klasse kaldet Moving, som både Dragon og Spider (hvis vi kalder klassen for en edderkop det) skal arve fra.

84

Klassen Moving skal rumme alle de egenskaber, som et væsen-som-bevæger-sig har. Altså skal de just indførte egenskaber som aggressions-radius, lurking-timeren med videre flyttes op i Moving-klassen. At flytte disse instans-variable til base-klassen gør, at vi ikke umiddelbart ved hvilke værdier vi skal sætte dem til. Dette er jo meget naturligt, idet Moving-klassen gerne skulle være base-klasse for alle mulige forskellige væsener-som-bevæger-sig, og dermed vil instans-variablene have forskellige værdier. De konkrete værdier for et specifikt væsen må derfor tilvejebringes via constructoren for Moving, mere specifikt som parametre til constructoren.

Constructoren for Moving-klassen kommer derfor til at se således ud (bemærk at listen af parametre er så lang, at den er delt over to linier):

public Moving(String theImageFile, double aggressionRadius, int lurkingMaxTime, int speed) { super(theImageFile); theMentalState = MentalState.passive; lurkingTimer = 0; this.aggressionRadius = aggressionRadius; this.lurkingMaxTime = lurkingMaxTime; this.speed = speed; }

Når alle instans-variablene er flyttet, kan vi tillige flytte stort set alle metoder fra Dragon-klassen op i Moving-klassen, såsom movePassive, moveAggressive, calculateMentalState og så videre. Dette efterlader os med en meget simpel Dragon-klasse:

public class Dragon extends Moving{ public Dragon() { super("dragon_25.jpg", 10.0, 10, 60); }

public void act() { super.act(); } public void specificInteract(Viking theViking) { theViking.adjustHP(-15); theViking.adjustXP(10); } }

Denne klasse har nu stort set samme struktur som f.eks. Food-klassen, dog rummer kaldet til base-klassens constructor en del flere parametre.

På samme måde som indførslen af base-klassen Respawnable gjorde det yderst nemt at indføre nye ”passive” elementer i spillet som f.eks. MedKit-klassen, gør Moving-klassen det meget nemt at indføre nye væsener-som-bevæger-sig i spillet. For at gøre det, skal vi tage beslutninger angående væsenets:

85

1. Grafik, altså vælge en passende billedfil for væsenet.2. Aggressionsradius3. Hastighed4. Lurking-tid5. Antal XP som vikingen vinder ved interaktion6. Antal HP som vikingen vinder/taber ved interaktion

De første fire egenskaber udmønter sig i parametre til base-klassen (Moving), mens de sidste to kodes direkte i metoden specificInteract. En klasse for et nyt væsen i form af en Spider-klasse kunne derfor se således ud:

public class Spider extends Moving{ public Spider() { super("spider_25.jpg", 5.0, 10, 30); }

public void act() { super.act(); } public void specificInteract(Viking theViking) { theViking.adjustHP(-5); theViking.adjustXP(3); } }

Altså er en edderkop et knapt så farligt væsen som en drage, idet en edderkop dels bevæger sig noget langsommere (hastigheden er 30), dels kun berøver vikingen for 5 health-points. Tilsvarende giver en edderkop kun 3 XP, mod 10 XP for en drage.

I den form Dragon- og Spider-klassen har nu, kunne man faktisk godt flytte specificInteract op i base-klassen, og dermed gøre værdierne for HP og XP til parametre, ligesom de andre egenskaber ved et væsen. Indtil videre har vi dog beholdt dem i de specifikke klasser. Man kunne måske forestille sig, at en inter-aktion med en edderkop kunne resultere i et giftigt bid, som kunne dræne vikingen for health-points over en vis tids-periode (denne form for negativ effekt på en spiller kaldes ofte for Damage over Time (DoT)), og ved at bevare specificInteract nede i klasserne for de specifikke væsener, bevarer man også muligheden for stadig at have interaktioner, der er helt specifikke for en given type væsen.

Til sidst skal det bemærkes, at selv om det nu er meget let indføre et nyt væsen, skal man stadig huske at foretage de relevante tilføjelser i TVPWorld-klassen, så nogle eksemplarer af det nye væsen rent faktisk viser sig i verdenen. Dvs. definere en konstant der fortæller hvor mange væsener af denne type der skal forekomme, definere en ny ActorType, og så videre.

86

7.14.2 Mulighed for at vinde spillet

Det er i sig selv ikke oplagt, at man overhovedet skal kunne vinde et spil som TVP. Mange RPG kan ikke som sådan vindes, men spilleren kan konstant forbedre sin figur i form af højere levels, bedre udstyr, og så videre. Spillet kan derfor i princippet fortsætte i det uendelige. I dette simple spil vil vi dog – indtil videre – indføre en måde at vinde spillet på. I en senere version kan vi måske vende tilbage til det typisk PRG-forløb med levels og hvad dertil hører.

I vores simple spil er XP det eneste som spilleren ”optjener” i løbet af spillets gang. Det er derfor naturligt at lade antallet af optjente XP være et kriterie for at vinde spillet. Vi sætter grænsen ved 300 XP, så når en spiller har optjent 300 XP, er spillet vundet.

Hidtil har spillet kun kunne stoppes ved at vikingen dør. Det checker vi løbende for i vikingens act-metode, ved at kalde metoden isDead(). Tilsvarende indfører vi nu en metode gameIsWon(), som cheker om kriteriet for at vinde spillet er opfyldt. Ydermere indfører vi endnu en instans-variabel gameOver af typen boolean, som holder styr på om spillet er slut eller ej.

Selve metoden gameIsWon() er meget simpel:

private boolean gameIsWon() { return (xp >= maxXP); }

Konstanten maxXP er sat til 300. Ligesom vi annoncerer vikingens død ved at afspille en lyd og ændre grafikken for vikingen, gør vi noget tilsvarende i tilfælde af at spillet vindes, ved hjælp af den nye metode doGameWinningBehavior():

private void doGameWinningBehavior() { setImage("trophy_25.jpg"); Greenfoot.playSound("win.wav"); gameOver = true; }

Den nye grafik og lyd vil være inkluderet i det kommende scenarie TVP_Kap7_Opg7_27_klar. Bemærk også den sidste linie; her sætter vi gameOver til true, så vi kan benytte denne variabel i den reviderede udgave af act-metoden. En sådan linie er også tilføjet til den eksisterende doDeathBehavior. Hermed kan vi ændre act-metoden i Viking-klassen til nedenstående:

87

public void act() { if (!gameOver) { move(); // Flyt Vikingen interactWithObject(getInteractableObject()); // Interager med andre objekter if (isDead()) { doDeathBehavior(); } if (gameIsWon()) { doGameWinningBehavior(); } } }

Indtil nu har spillet rummet en mindre uhensigtsmæssighed. Når vikingen dør, fortsætter de andre væsener med at vandre rundt i verdenen, og kan endda interagere med den døde viking… Dette skyldes, at de andre væsener ikke har nogen måde at opdage vikingens død på. Dette ændrer vi nu på, ved at indføre en ny – meget simpel – metode i Viking-klassen, kaldet isGameOver():

public boolean isGameOver() { return gameOver; }

Vi har jo tidligere set, at vi godt kan få fat i Viking-objektet i verdenen f.eks. fra Dragon-klassen, så ved hjælp af denne nye metode kan en drage nu få at vide, om spillet er slut eller ej. Dette kan udnyttes til at omskrive act-metoden i Moving-klassen:

public void act() { if (!getViking().isGameOver()) { // Udfør væsenets almindelige opførsel } }

Med andre ord gør metoden det den hele tiden har gjort, men nu kun hvis spillet stadig er i gang. I praksis vil dette betyde, at alle væsener-som-bevæger-sig vil standse op, når spillet slutter. Denne effekt gør det forhåbentligt mere tydeligt for spilleren, at spillet vitterligt er slut.

88

7.14.3 Indførsel af sværhedsgrad

I spillets nuværende form har spillet den sværhedsgrad det nu engang har, defineret ud fra antallet af væsener i verdenen, hvor mange XP hhv. HP man får/mister ved interaktion med andre objekter i spillet, og så videre. I mange spil er der mulighed for at vælge en sværhedsgrad for spillet, inden man starter på selve spillet. En typisk ”skala” for sværhedsgrad kan se således ud:

1. Easy2. Medium3. Hard

Hvordan disse valg så rent praktisk udmønter sig i spillet, er naturligvis op til spillets designer. Det kan være i form af flere/færre fjendtlige væsener i spillet, ændret hastighed eller aggressivitet, og så videre. I første omgang ser vi blot på, hvordan vi overhovedet giver spilleren mulighed for at vælge mellem de forskellige sværhedsgrader.

Første trin i denne proces er at indføre et skærmbillede i spillet, hvor man kan vælge sværhedsgraden. Dette skærmbillede skal således vises før spillet går i gang, og spilleren kan ikke fortsætte videre til spillet, før der er valgt en sværhedsgrad. Sådan et skærmbillede betegnes ofte som en splash screen. En splash screen er ofte med til at forme spillerens første indtryk af spillet, og må derfor gerne være lidt ”lækker” at se på. Vi har lavet følgende splash screen til TVP:

Figur 7.13: TVP splash screen

89

At lave selve billedet er en rent grafisk opgave, og det er også ganske nemt at vise billedet frem i Greenfoot. Metoden setBackground tager en billedfil som input, og viser den som baggrund på verdenen. Hvis blot vi sørger for at vise billedet før vi går i gang med at befolke verdenen, vil man kun se billedet. Bemærk, at man bør sørge for at billedet har samme størrelse som verdenen – i vores tilfælde 800x800 pixels, idet man ellers vil se enten et beskåret billede eller et billede der gentages flere gange (såkaldt tiling), hvilket ikke ser specielt godt ud.

Et mere vanskeligt problem er, hvordan vi får fat i brugerens valg af sværhedsgrad. Selve registreringen af valget er ikke så vanskelig; det er nemt at detektere, om spilleren har trykket på en given tast på tastaturet (det benytter vi allerede til at styre vikingen), så hvis spilleren skal vælge sværhedsgrad ved at trykke på ”1”, ”2” eller ”3”, er dette ret nemt at registrere ved hjælp af metoden isKeyDown. Komplikationen ligger mere i, at initialiseringen af spillet ikke længere er en enkelt, uafbrudt rutine. I sin nuværende form starter spillet så snart vi trykker på Run; verdenen bliver befolket med væsener, og umiddelbart derefter kan vi begynde at styre vikingen rundt i verdenen. Med indførslen af vores splash screen bliver forløbet af initialiseringen i stedet:

1. Vis splash screen2. Afvent, at spilleren vælger sværhedsgrad3. Initialiser verdenen, og lad spillet begynde

Trin 2 gør det umuligt at udføre hele initialiseringen af spillet i constructoren for TVPWorld – hvilket vi hidtil har gjort – idet man ikke kan stå og vente på bruger-input i constructoren for ens verden. Det er ikke helt klart hvorfor dette ikke er muligt, men forsøger man på det, fryser Greenfoot totalt, og kan kun lukkes ved at afslutte selve Grenfoot-processen!

Hvad gør man så? Heldigvis er det sådan, at der faktisk også findes en act-metode på World-klassen; vi har blot ikke haft brug for den før. Den kan vi bekvemt bruge nu. Vi gør nu ikke længere hele initialiseringen af spillet færdig i constructoren, men stopper når vi har sat vores splash screen til baggrund, og således venter på input fra spilleren. Det bliver så i stedet i act-metoden, at vi checker om spilleren har valgt sværheds-grad. Har han ikke det, gør vi ingenting, idet vi jo får checket det igen i det næste kald af act. Når engang spilleren har valgt sværhedsgrad, kan vi fortsætte med den egentlige initialisering af spillet, d.v.s. ved at kalde polulateTVPWorld().

Vi skal dog holde tungen lige i munden i act-metoden. Greenfoot holder jo ikke op med at kalde act-metoden, bare fordi vi er færdige med vores initialisering. Vi skal derfor sørge for, at initialiseringen kun sker én gang. Lidt mere overordnet kan vi sige, at vi kan være i tre forskellige tilstande, når act-metoden bliver kaldt:

1. Vi afventer spillerens valg af sværhedsgrad2. Spilleren har valgt sværhedsgrad, men vi har ikke initialiseret verdenen endnu3. Verdenen er blevet initialiseret

90

Vi bliver derfor nødt til at holde styr på to ting:

Har spilleren valgt sværhedsgrad? Er verdenen blevet initialiseret?

Dette gør vi ved hjælp af to nye instans-variable difficultyIsChosen og worldIsPopulated. De er begge af typen boolean, og er fra starten begge sat til false. Hermed indikerer vi, at vi fra starten hverken har valgt sværhedsgrad eller har initialiseret verdenen. Ved hjælp af disse to nye variable kan vi skrive en skitse til en passende act-metode:

public void act() { if (!difficultyIsChosen) { // Check om spilleren har trykket på ”1”, ”2” eller ”3” // Hvis ja, så sæt difficultyIsChosen til true. } else { if (!worldIsPopulated) { // Fyld TVPWorld ud med alle de objekter, // vi har besluttet skal være der fra starten setBackground("empty_25.jpg"); // Fjern splash screen populateTVPWorld(); worldIsPopulated = true; } } }

Første gang vi kommer ind i act, vil difficultyIsChosen være false, og vi vil udføre den del af koden, der er i if-delen af sætningen. Dette vil blive ved med at ske, indtil spilleren har lavet et korrekt valg, hvorefter difficultyIsChosen sættes til true. I følgende kald af act kommer vi således i stedet ind i else-delen af if-sætningen. Første gang det sker vil worldIsPopulated være false, og derfor vil selve initialiseringen blive udført, herunder at sætte worldIsPopulated til true. I efterfølgende kald vil der derfor slet ikke blive udført noget i act, idet både difficultyIsChosen og worldIsPopulated nu er true, og vil vedblive med at være det.

Selve koden for at registrere spillerens valg af sværhedsgrad er ret ligetil, og kan ses i scenariet TVP_Kap7_Opg7_27_klar. Resultatet af spillerens valg gemmes i den nye instans-variabel gameDifficulty af typen int, som således kan have værdien 1, 2 eller 3.

Det sidste – men nok så vigtige – spørgsmål er så, hvordan en konkret sværhedsgrad skal udmønte sig i spillet. Det kan som tidligere nævnt gøres på mange måder – vi har her valgt en rimeligt simpel strategi, hvor vi lader sværhedsgraden påvirke antallet af fjendtlige væsener i verdenen. Hidtil har vi defineret en konstant for hver type væsen, der definerer hvor mange der skal være i verdenen af netop denne type. Denne konstant benyttes så i kaldet af populateActorType:

91

populateActorType(dragonObjects, ActorType.dragon,...); populateActorType(spiderObjects, ActorType.spider,...); populateActorType(medkitObjects, ActorType.medkit,...);

Denne stump kode bliver nu modificeret på følgende vis:

populateActorType(dragonObjects*getGameDifficulty(), ActorType.dragon,...); populateActorType(spiderObjects*getGameDifficulty(), ActorType.spider,...); populateActorType(medkitObjects/getGameDifficulty(), ActorType.medkit,...);

Med andre ord: his man vælger sværhedsgraden Easy (1), vil der være det oprinderige antal Dragon-, Spider- og MedKit-objekter i verdenen, men vælger man Medium (2), vil der være dobbelt så mange Dragon- og Spider-objekter, og kun halvt så mange MedKit-objekter. Vælger man Hard (3) bliver det endnu værre! Om dette er nogle rimelige valg for effekten af sværhedsgrad må man naturligvis eksperimentere lidt med, og eventuelt bruge en anden ”skala”, hvor man ganger med nogle andre faktorer end lige 2 og 3.

NB: Før du går videre, hent da scenariet TVP_Kap7_Opg7_27_klar. I dette scenarie er alle funktionerne beskrevet i dette kapitel inkluderet.

Opgave 7.27: Prøv at spille spillet på hver af de tre sværhedsgrader, og se hvor lang tid det tager for dig at gennemføre spillet. Man starter spillet med 40 health-points, og vinder spillet ved at opnå 300 XP.

Opgave 7.28: Prøv at anvende den angivne sværhedsgrad på andre måder end den foreslåede. Måske kunne den anvendes til at ændre på væsenernes hastighed. Hint: Brug metoden getGameDifficulty()

Fortsættes engang, håber jeg

92