Miniprojekt: Tetris
Detta är en kortare beskrivning som förutsätter att du är väl bekant med Java och OO.
Vi måste fortfarande se till att lösningen demonstrerar vissa specifika kunskaper för examinationen – därför kan vi inte korta ner uppgiften till "implementera ett Tetris-spel med dessa finesser"! Dit kommer ni istället i det större projektet.
Syfte
Nu övergår vi från att fokusera på enskilda delar av Java och OO till ett större projekt där du får skriva ett fullständigt program: Ett Tetris-spel. Här får du också testa enklare GUI-programmering i Java och får utforska fler OO-begrepp och modelleringsfrågor.
En fördel med Tetris som spel är att det inte är så krävande i fråga om t.ex. animering, kollisionsdetektering och andra knepigheter, så att vi kan fokusera mer på objektorienteringen och hur man kan tänka för att skriva ett objektorienterat program.
Denna variant av instruktionerna är till för dig som har mer erfarenhet, och som vill ha mer frihet på egen risk!
Vi minskar detaljinstruktionerna kraftigt och förutsätter att du är väl bekant med Java och OO, utom för designmönster som vi tror är nya för många. En del styrning på strukturnivå (klasser och modellering) finns också kvar, för att minska risken att du tar en väg som senare gör det svårt för dig att demonstrera de begrepp vi behöver examinera.
Friheten kommer som sagt på egen risk. Om du missar någon viktig poäng som vi tog upp i den detaljerade vägledningen kan det leda till komplettering: Du är här för att du anser dig kunna detta redan. Eftersom detta är första året vi ger möjlighet till mer frihet i programmeringen, har vi inte heller sett vilka potentiella fallgropar som kan finnas. Är du osäker på något: Gå åtminstone tillfälligt över till den långa beskrivningen.
Förberedelser
Vi förutsätter att du har tagit del av samtliga föreläsningar om objektorienteringens grunder.
Om du inte har spelat Tetris tidigare: Läs på lite om det och testa någon variant. Beskrivningen förutsätter att du vet hur Tetris fungerar.
Bra att tänka på
Så hur programmerar man ett lite större sammanhängande projekt såsom ett Tetris-spel? Ett tips är att bryta ner det i steg eller milstolpar som kan implementeras i tur och ordning – helst på ett sätt som gör att man kan testa varje steg för sig och känna att man faktiskt har åstadkommit något. Hela projektet är uppbyggt på detta sätt.
Mer att tänka på:
-
Arbeta på egen tid! Tetrisprojektet är betydligt större än labbarna och vi räknar med att många behöver betydligt mer tid än det som schemalagts.
-
Använd gärna referenskortet för IDEA när du programmerar. Många funktioner kan vara mycket användbara här och i det stora projektet.
-
Läs genom varje deluppgift (t.ex. hela uppgift 2.1) innan du utför den.
-
En viss mängd dokumentation av era lösningar krävs för detta projekt. Kommentera!
-
Du får använda andra labbmiljöer från och med nu – men koden måste lämnas in i IDEA-format och kontrolleras med IDEAs kodinspektioner!
5.0. Inledning
Bakgrund 5.0.1: Skapa projekt och paket
Om du vill sätter du nu upp ett nytt IDEA-projekt för Tetris. Det går också bra att fortsätta i det gamla projektet, så länge du separerar de gamla och nya klasserna från varandra genom att lägga dem i olika (under)paket.
Fortsätt i samma git-repo (filarkiv) som tidigare.
Att göra 5.0.1: Skapa projekt och paket
-
Skapa eventuellt ett nytt IDEA-projekt. Skapa isåfall först en egen underkatalog för det projektet i den existerande arbetskopian, så det blir enklare att hålla isär det nya från de tidigare labbarna. Följ sedan instruktionerna, och kom ihåg att ändra More Settings / Project format till ".ipr (file based)"!
Skapa ett paket för Tetris, med lämpligt namn enligt tidigare namngivningsregler.
5.1. Spelplan och "kvadrater"
Info: Tetris-begrepp!
Tetris har en spelplan där man steg för steg lägger till tetrisblock som "ramlar ner" uppifrån. Varje tetrisblock är tekniskt sett en tetromino, ett specialfall av en polyomino (tetra=4): Det består av fyra sammanhängande kvadrater i en viss färg och ett visst mönster, där totalt 7 olika mönster är möjliga. Nedan syns ett exempel från Wikipedia. Standardnamnen på dessa block är I, J, L, O, S, T och Z.

Info: Separera information från visning
Vi kan identifiera tre olika delar av Tetris-spelet: Spelmekaniken, visningen (grafisk eller på annat sätt), och styrningen (t.ex. via tangentbord). Detta är tre separata ansvarsområden som till stor del kan hanteras oberoende av varandra. Då är det också en fördel om vi kan separera dem kodmässigt, vilket har lett till ett designmönster som kallas Model-View-Controller (MVC). Förenklat kan detta beskrivas så här:
Det är bra att separera den grundläggande informationen om någonting, modellen, från hur den visas, vilket hanteras av vyn, och hur den kontrolleras via ett användargränssnitt, controllern.
I Tetris kan (om vi vill) se spelplanen som en sammanhållen enhet
där enskilda block inte har egna beteenden. Vi kan därför se till
att spelplanen, vår Board
-klass,
innehåller all information om vilka block och kvadrater
som ligger var inom spelplanens rutnät – inte om hur detta
ska visas på skärmen (färger, exakta GUI-koordinater, och så
vidare). Senare kan vi välja att "visualisera" modellen i
textformat, i 2D-format, med 3D-grafik, eller på många andra sätt.
Vi får alltså en relativt ren implementation av
Model-View-Controller i detta spel, vilket man
inte nödvändigtvis får i projektet.
Model-view-controller måste användas!
Bakgrund 5.1.1: Kvadrater
Du behöver välja ett sätt att representera spelplanens nuvarande utseende. En rekommendation är att inte lagra fullständiga Tetrisblock, utan att ha en 2D-array/matris som innehåller de kvadrater (delar av Tetrisblock) som fortfarande finns kvar, eller en speciell markör för "tom ruta" där ingen kvadrat ligger.
Man kan t.ex. representera en "typ av kvadrat" som en
enum SquareType
. Det finns exakt 8 värden: "här
finns ingen kvadrat", "här finns en kvadrat från ett
T-block", och så vidare.
SquareType
är en del av modellen och ska inte veta
något om hur den ritas ut!
Att göra 5.1.1: Kvadrater och spelplan
Skapa en lämplig enum-klass för att representera kvadrater.
Skapa klassen
Board
. Se till att den kan lagra spelplanen, till exempel i en 2D-array, och att detta görs privat. Skapa en lämplig konstruktor som tar en bredd och höjd på spelplanen som argument. Tänk på att fylla spelplanen med "ingen kvadrat här".-
Testa att konstruktorn inte kraschar, med en
main
-metod som helst enkelt skapar ett Board med valfri storlek. I framtiden påminner vi oftast inte om att testa.
5.2. Utskrift av spelplan
Syfte 5.2.1: Visualisera snabbt, examinera strängar
För att snabbt få till en visualisering, testa användningen av flera vyer, och examinera användningen av strängar i Java: Skapa en utskriftsfunktion för spelplanen.
Steg 5.2.1 saknas här. Vi behåller dock samma numrering som i den långa beskrivningen!
Att göra 5.2.2: Strängmanipulation
-
Skapa klassen
BoardToTextConverter
, med metodenconvertToText(Board)
som returnerar en strängrepresentation av ett modellobjekt – den skriver alltså inte ut på skärmen själv utan returnerar en sträng som anroparen kan skriva ut.Hitta på en lämplig representation. Tänk på att det är
BoardToTextConverter
som är vy och som ska ska känna till representationen.Se till att informationen i
Board
fortfarande är privat. Skapa getters för att plocka ut information som behövs. Du får inte returnera hela lagringsarrayen eftersom detta är en intern implementationsdetalj. Returnera istället info om vad som finns på en specifik position. -
Skapa en testklass som skriver ut ett Board med hjälp av
BoardToTextConverter
.
5.3. Slumpning av spelplan
Att göra 5.3.1: Slumptest
För vår testning vill vi också ha en metod för att slumpa fram en spelplan.
-
Skapa en metod i
Board
som kan användas för att ersätta det nuvarande innehållet med genererat slumpmässigt innehåll (en slumpmässigSquareType
i varje ruta).Använd
java.util.Random
. Skapa inte ett nytt Random-objekt varje gång du behöver ett slumptal! Då blir det (a) onödigt långsamt, och (b) dålig kvalitet på slumptalen.Hårdkoda inte kunskap om vilka SquareTypes som finns! Metoden
SquareType.values()
kan vara användbar. -
Testa: Slumpa ett antal spelplaner och skriv ut.
5.5. Tetrisblock / tetrominoes
Bakgrund 5.5.1: Polyominos
Efter spelplanen kommer själva Tetrisblocken. Det finns totalt 7 varianter i grundspelet, och vi kan tänkas vilja utöka detta i framtiden – genom nya former (utökning till pentominoes med fem kvadrater per block, penta=5) eller andra varianter (block som exploderar).
Tänk på att standardblocken egentligen inte beter sig
olika utan bara ser olika ut (olika data). Därför ska vi
inte ha en egen klass per blocktyp (L, T, ...), utan en
gemensam klass som vi kan kalla Poly
(kort för
Polyomino).
Varje Poly
-objekt måste veta sin konfiguration av
kvadrater. Detta har inte att göra med visualisering (pixlar på
skärmen) utan är en del av den fundamentala modellen för ett block
(till exempel avgör det hur långt ett block kan falla). Därför
ska det definitivt finnas med i
Poly
. Använd till exempel en 2D-array av kvadrater.
Poly
ska kunna användas för vilken typ av block som
helst, inklusive t.ex. block med 3 eller 5 kvadrater. Hårdkoda
inte detta i Poly
!
Att göra 5.5.1: Polyominos
-
Implementera klassen
Poly
enligt beskrivningen ovan.Klassens konstruktor ska alltså ta in en array som beskriver konfigurationen hos en godtycklig polyomino. Sådana arrayer kommer sedan att skickas in av
TetrominoMaker
i nästa uppgift – det är där de 7 specifika blocktyperna definieras.
Bakgrund 5.5.2: TetrominoMaker
Någon måste hålla reda på vilka Poly
som ska finnas i
spelet, hur de ser ut, och hur de skapas. Detta är ett tydligt
ansvarsområde som man kan skapa en egen klass för – med
en fabriksmetod. Vi föreslår
klassen TetrominoMaker
med dessa metoder.
public int getNumberOfTypes() { ... }
public Poly getPoly(int n) {
// Beroende på n, skapa och returnera en specifik Poly
// Om n är felaktigt:
// Signalera fel med IllegalArgumentException
}
För att underlätta för senare rotation av blocken är det en fördel att använda arrayer med storlek 2x2, 3x3 eller 4x4, beroende på vilken tetromino som den innehåller, enligt kolumnen till vänster i följande bild (där de övriga kolumnerna visar hur blocken ser ut när de roteras, vilket vi kommer till senare).
Bilden kommer från en beskrivning av det standardiserade rotationssystemet.
Att göra 5.5.2: TetrominoMaker
-
Implementera klassen
TetrominoMaker
enligt ovan.Bryt gärna ut privata hjälpmetoder från
getPoly()
så den inte blir för lång.
Info: Fabriker och designmönster
En TetrominoMaker
är en fabrik (factory): Ett
objekt som kan skapa andra objekt och returnera dem till anroparen.
Man kan också bredda detta begrepp och kalla getPoly()
för en fabriksmetod (factory method).
Detta ses normalt inte som ett objektorienterat designmönster, även om det finns objektorienterade designmönster med liknande namn, t.ex. factory method pattern och abstract factory pattern. Dessa mönster bygger istället på en specifik och mer komplicerad relation mellan olika objekt och klasser. Att ha ett objekt som kan returnera andra objekt räcker alltså inte för att man ska kunna säga att man använder ett objektorienterat designmönster. (Eller omvänt: Det behövs som tur är inget objektorienterat designmönster för att man ska kunna göra något så enkelt som att returnera ett nytt objekt från en metod!)
Bakgrund 5.5.3: Tetrisblock på spelplanen
Vår spelplan kan nu representera de block som redan har fallit ner. Nu behöver den också kunna representera ett block som är på väg att falla ner, så att all information om spelets nuvarande tillstånd sparas på ett och samma ställe.
Rekommendation: Hantera detta som en overlay. Med andra ord, håll reda på om något block faller, och i så fall vilket och var det finns just nu. Lägg inte in kvadrater i spelplanen förrän blocket verkligen har fallit "ända ner". Detta underlättar också om man t.ex. vill få block att "glida" pixel för pixel istället för att "hoppa".
Att göra 5.5.3: Tetrisblock på spelplanen
-
Om du följer rekommendationen: Lägg till ett fält
Poly falling
iBoard
, som pekar på "den poly som just nu håller på att ramla ner", eller annars är null. Lägg till info om positionen för fallande blocket. -
Ändra så att även den fallande tetrominon "ritas" ut (i textsträngen som
BoardToTextConverter
skapar). Detta kan kräva fler getters, eller kan hanteras genom ändring av existerande getters.
5.5. Ett enkelt GUI
Bakgrund 5.5.1: Textgrafik
Det är nu dags att gå över från rent textformat till ett grafiskt gränssnitt.
För att förenkla börjar vi med att skriva ett grafiskt gränssnitt
som använder en grafisk textkomponent, en JTextArea
,
för att visa den gamla textrepresentationen av en "spelplan". Det
är ett krav att du går via det steget i Tetris, men är inte
ett krav för det senare projektet!
Att göra 5.5.1: Textgrafik
Skapa den nya klassen
TetrisFrame
, subklass tillJFrame
.Låt
TetrisFrame
få tillgång till ettBoard
-objekt via en ny konstruktor. (Grafiska gränssnittet har inte ansvar för att skapa ett spel utan bara för att visa det!)Se till att konstruktorn ger en lämplig fönstertitel till superklassens konstruktor.
Låt
TetrisFrame
bygga upp ett lämpligt användargränssnitt med denJTextArea
som ska användas till att visa själva "spelplanen". Antalet kolumner och rader får du reda på genom att fråga dittBoard
, inte genom att hårdkoda!Redan när en
TetrisFrame
skapas måste textarean ges sitt första innehåll med strängen frånBoardToTextConverter
. Att uppdatera textarean när spelplanen ändras blir en senare uppgift.Tips: För att rutor i tetrisbrädet skall visas lika stora oavsett vilka tecken man valt att representera olika SquareTypes lägger vi till
textarea.setFont(new Font("Monospaced",Font.PLAIN,20));
.Testklassen behöver ändras så att den skapar ett
Board
och öppnar ettTetrisFrame
-fönster. När testet körs ska slutresultatet vara att du ser en lagom storTetrisFrame
med en framslumpad spelplan (inte 10 spelplaner, som tidigare).
5.6. Timer för spelloop
Bakgrund 5.6.1: Timer
Så småningom behöver vi ha något sätt att driva spelet framåt, att få block att falla ner i lagom takt och så vidare.
Ett inte alltför ovanligt misstag är att man lägger in en loop som "stegar fram" ett steg, gör en paus av konstant längd, "stegar fram" nästa steg, och så vidare. Problemet är att när pausen är av konstant längd kommer tiden från starten av ett steg till starten av nästa att variera, beroende på hur lång tid själva steget tar, vilket är olika beroende på dator och CPU-belastning på datorn. Vi får göra på något annat sätt.
Som tur är finns en klass som
heter javax.swing.Timer
som vi kan använda för att få ett "steg" i spelet att köras
regelbundet i Swings händelsehanteringstråd. Blanda inte ihop den
med andra klasser som heter Timer,
t.ex.
!
java.util.Timer
Att göra 5.6.1: Timers
-
Skapa någonstans en
Timer
som kör en handling med regelbundna mellanrum, en gång i sekunden. Handlingen ska slumpa om spelplanen (inte byta utBoard
-objektet utan ändra i det!) på motsvarande sätt som du gjort tidigare, och visa resultatet i textarean som du skapade i en tidigare uppgift. Slutresultatet ska helt enkelt bli att du ser en enkel "animering" på skärmen medan programmet körs.
5.7. Kodinspektion!
Bakgrund 5.7.1: Kodinspektion
Vi rekommenderar starkt att man granskar sin egen kod då och då, för att eventuella problem inte ska bli för långlivade.
Att göra 5.7.1: Kodinspektion
-
Om du normalt använder en annan miljö: Importera ditt projekt till IDEA. Det behöver du ändå kunna göra inför inlämningen.
-
Kör IDEAs kodinspektioner med profil TDDD78-2018-v1.
- Välj Analyze | Inspect Code i menyn.
-
Välj inspektionsprofil "TDDD78-2018-v1" och tryck OK.
Använd INTE defaultprofilen.
Det här är till stor del ett sätt att lära sig skriva kod som inte bara gör vad den ska utan även är välskriven och strukturerad.
Förstår du inte en varning? Utmärkt! Då har du en chans att lära dig något nytt, redan innan du lämnar in koden! Läs IDEAs inbyggda beskrivningar, läs våra utökade beskrivningar, och om det inte hjälper, fråga gärna assistenten eller examinatorn.
Inget automatiskt inspektionsverktyg är 100% korrekt. Varningen ska alltså oftast tolkas som "har du tänkt på det här?" snarare än "du har fel!".
5.8. Mer GUI-programmering: Menyer!
Att göra 5.8.1: Menyer
-
Lägg till menyer i spelet enligt vanliga lösningar. Se minst till att det finns en meny med valet "Avsluta".
Se till att "Avsluta" gör att en dialogruta visas med hjälp av
JOptionPane
, där man får bekräfta att man vill sluta. Vid bekräftelse ska spelet avslutas viaSystem.exit(0)
.
5.9. En grafisk spelplan
Bakgrund 5.9.1: Grafik del 1 – komponenten
Det är nu dags att gå över från en textbaserad visning till en helt grafisk.
Att göra 5.9.1: Grafik del 1 – komponenten
Spara undan nuvarande
TetrisFrame
så att vi kan titta på den vid examinationen. Högerklicka påTetrisFrame
och välj Refactor -> Copy och döp den till TetrisFrame_v1.Skapa en
TetrisComponent
-klass som är subklass tillJComponent
och som känner till ettBoard
som den ska visa.Implementera
getPreferredSize()
så att den returnerar den storlek du helst vill ha för den grafiska visaren.
Grafik
Snygg grafik är inget vi premierar här. Experimentera gärna, men spendera inte alltför mycket tid på det. Enfärgade kvadrater med 1 pixels mellanrum duger gott när syftet är att lära sig objektorientering!
Bakgrund 5.9.2: Grafik del 2 – utritningen
En JComponent
behöver kunna rita upp sig
själv när som helst, när Swings bakgrundstråd anropar
den. Vi skall därför implementera
metoden paintComponent()
så att den ritar upp
spelplanen som det ser ut just nu: Bakgrunden, de kvadrater som
har ramlat ner på spelplanen, och det block (om något) som håller
på att ramla ner.
Fundera på hur paintComponent()
ska veta vilken färg
(java.awt.Color
) den ska använda för
varje SquareType
.
Ett sätt är att använda en switch-sats med en gren för varje
SquareType
.-
Ett snyggare sätt är att någon ger den en
EnumMap<SquareType,java.awt.Color>
som lagrar mappningen – mer flexibelt, inte hårdkodat i komponenten.
Exempel: paintComponent()
Exempel:
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
final Graphics2D g2d = (Graphics2D) g;
g2d.setColor(Color.GREEN);
g2d.drawRect(a, b, c, d);
}
Här anropas super.paintComponent()
för att rensa
bakgrunden i komponenten.
Att göra 5.9.2: Grafik del 2 – utritningen
Implementera
paintComponent()
enligt beskrivningen ovan.Ändra i det tidigare GUIt så att
TetrisComponent
nu används istället förJTextArea
plusBoardToTextConverter
.Ändra i testklassen, eller skriv en ny testklass, så att den
Timer
som introducerades tidigare ber dinTetrisComponent
att rita om sig istället för att manipulera den textarea vi brukade använda.I en kommande uppgift ska vi använda ett mer principiellt sätt att få
TetrisComponent
att uppdatera sig vid ändringar.
Magiska konstanter
Undvik magiska
konstanter, i betydelsen "konstanter som används i koden utan
någon förklaring av var de kommer ifrån". Namnge dem, genom
t.ex. private final static int SQUARE_WIDTH = 30;
, så
blir det lättare att läsa och förstå koden.
5.10. Observer / Observable
Bakgrund 5.10.1: Lyssnare och notifiering
Här undersöker vi designmönstret Observer och examinerar dess användning. Detta kan vara nytt även för många vana programmerare, så här diskuterar vi den tänkta lösningen mer detaljerat.
Tidigare har du själv fått hitta på ett sätt att få spelplanen att ritas om när något ändras. Problemet med vår föreslagna lösning är att timerhandlingen måste känna till precis vilka som vill veta när spelplanen ändrar sig. Det geren alldeles för stark koppling mellan klasser som egentligen inte alls borde behöva känna till varandra.
Ett annat sätt är att använda designmönstret Observer: Se
spelplanen som något "observerbart" (Observable). Varje
gång planen förändras på något sätt (t.ex. genom att ett block
faller ner ett steg) kan den själv informera en eller flera
"observatörer" (Observer). Varje observatör kan
då själv registrera sig som intresserad av vad som händer
med ett visst Board
. Då behöver varken
timerhandlingen eller Board
känna till alla
intresserade på förhand!
Fördelen är bland annat att timerhandlingen kan fokusera helt på
att driva spelet framåt ett steg. När spelet drivs framåt ett steg
är det sedan spelplanen som talar om för alla registrerade att
något har hänt. Jämför med ActionListeners
i Swing!
Att göra 5.10.1: Lyssnare och notifiering
Skapa ett interface
BoardListener
med en metodpublic void boardChanged();
.Låt
Board
hålla reda på en lista av BoardListeners. Lägg också till en metodpublic void addBoardListener(BoardListener bl)
som adderar den givna lyssnaren till listan.Skapa i
Board
en privat metodprivate void notifyListeners()
som loopar över alla element i lyssnarlistan och anropar derasboardChanged()
-metoder.Se till att alla publika metoder i
Board
som ändrar på spelplanen anroparnotifyListeners()
på slutet. Detta inkluderar metoder som t.ex. ändrar koordinater på det block som håller på att ramla ner. Du måste också se till att göra på samma sätt i nya metoder du lägger till senare.Ändra
TetrisComponent
så att den implementerar gränssnittetBoardListener
. Implementera metodenboardChanged()
så att den anroparrepaint()
.Se till att timerhandlingen som driver animeringen framåt inte längre anropar
repaint()
själv, som i tidigare lösning. Ändra istället på era testklasser så attTetrisComponent
-objektet du skapar adderas som en lyssnare på spelplanen.
Resultat
Slutresultatet av denna ändring ska bli att allt på skärmen ser ut precis som tidigare – men att koden är mer modulär då vi har minskat den starka kopplingen mellan datalagring och utritning på skärmen. Detta är en mycket viktig ändring.
5.11. Fallande block
Bakgrund 5.11.1: Fallande block
Nu är det dags att börja implementera den riktiga spelmekaniken i Tetris. Vi gör detta i flera steg. Till att börja med ska vi inte längre utnyttja timerhandlingen till att slumpa fram en helt ny spelplan, utan för att driva spelet ett steg framåt.
Det är dock en dålig idé att lägga spelmekanik och spelregler i
själva timerhandlingen – då skulle vi återigen blanda ihop
två helt olika typer av funktionalitet. Spelmekanik och
spelregler ska hanteras antingen i Board
eller en
egen klass för spelmekanik. Instruktionerna skrivs enligt det
första alternativet men du kan också välja att använda en separat
klass.
Att göra 5.11.1: Fallande block
Ändra så att spelet utgår från en tom spelplan istället för en framslumpad spelplan.
-
Implementera en ny
tick()
på lämplig plats. Om spelplanen saknar fallande block ska en blocktyp (T, L, ...) slumpas och ett nytt block av den typen placeras överst på spelplanen, centrerat. Annars ska det fallande blocket flyttas ett steg nedåt. (Det är OK om det fortsätter falla i all oändlighet eller till och mer kraschar när det når botten. "Kanterna" på spelplanen ska hanteras senare.) Se till att
tick()
anropas från timerhandlingen enligt ovan.
5.12. Tangentbordsstyrning
Bakgrund 5.12.1: Styra tetrominos
Dags att lägga till tangentbordsstyrning av block så att de kan flyttas
åt höger och vänster. Tangentbordsstyrning kan göras med hjälp
av key
bindings kopplade till din TetrisComponent
. Se
även vanliga lösningar!
Tänk på att man normalt kan flytta ett block flera steg åt sidan under samma tick. Eftersom key bindings hanteras asynkront, "i bakgrunden" ska detta fungera automatiskt.
Att göra 5.12.1: Styra tetrominos
Implementera en eller flera metoder i
Board
som anropas vid "draget" sidledsförflyttning. Dessa metoder ska helt enkelt flytta det nedfallande blocket ett steg åt sidan. Kollisionshantering kommer senare.Metoderna behöver så klart anropa
notifyListeners()
eftersom de ändrar på spelplanen.-
Använd key bindings (se ovan) för att anropa metoderna för sidledsförflyttning när man trycker pil vänster respektive pil höger.
Info: Synkronisering
Den som är bekant med trådad programmering undrar kanske om det nu
kan hända att Board
anropas från flera trådar
samtidigt, dels via tangentbordsbindningen och dels via
timerhandlingen. Nej, faktiskt inte. Tangentbordsbindningar
behandlas i Swings händelsehanteringstråd, så det är den tråden
som flyttar block i sidled. Den timer vi valde att använda
tidigare är javax.swing.Timer
, och den anropar också
sin handling (som i sin tur anropar Board.tick()
)
från Swings händelsehanteringstråd. Därför bör vi inte få några
trådningsproblem här.
5.13. Kollisionshantering och "game over"
Bakgrund 5.13.1: Kollisioner
Vi behöver detektera om det fallande blocket blockeras av gamla kvadrater på spelplanen. Det kan till exempel göras med en väldigt enkel form av kollisionshantering: Varje gång vi flyttar det fallande blocket ändrar vi först dess koordinater provisoriskt, utan att rita om skärmen. Vi testar sedan om det flyttade blocket överlappar några existerande kvadrater i spelbrädet. I så fall var förflyttningen förbjuden, och vi flyttar tillbaka blocket till sin gamla position.
Vi måste också detektera om det fallande blocket är på väg att gå
genom spelplanens gränser (vänster, höger, nedåt). Detta kan göras
genom att testa koordinater, men när vi senare lägger till rotation
blir det jobbigt att hålla reda på var varje block verkligen börjar
och slutar. Ett alternativ är att skapa en osynlig "ram" runt
spelplanen, full av den nya
markören SquareType.OUTSIDE
:
Om du väljer att använda en ram är det viktigt att
förstå att ramen bara är ett sätt att implementera
kollisionshanteringen, inte en fundamental egenskap hos ett
spelbräde. Vi vill alltså inte att ramens existens på något som
helst sätt ska synas utanför Board
: Det är en
implementationsdetalj som ska döljas, så att ingen annan behöver
veta om den och så att vi vid behov kan göra om det hela senare
utan att påverka andra klasser!
Att göra 5.13.1: Kollisioner
-
Se till att kollisioner med existerande kvadrater detekteras och att blocket då stannar.
-
Se till att fallande block inte kan gå utanför spelplanens gränser.
Lägg till en kontroll i
tick
så att ett block som "landar" på ett annat block också "fastnar" där och läggs in permanent i spelbrädet. Tänk på att det kan vara bra att skapa nya metoder som anropas från tick()!Implementera "game over" genom att direkt göra en kollisionskontroll då ett nytt block slumpats fram. Om det nya blocket omedelbart kolliderade med något, var det omöjligt att lägga till ett nytt block på spelplanen.
5.15. Rotation av tetrisblock
Bakgrund 5.15.1: Rotation
Om man ska ha en chans att fylla rader så de försvinner, vilket ju är målet med spelet, behöver man kunna rotera blocken. Detta testar också koden vi har skrivit tidigare: Är den korrekt, eller beror den på underförstådda antaganden som egentligen inte är sanna?
Det enklaste sättet att rotera en godtycklig tetromino åt höger är genom att flytta alla dess kvadrater enligt följande bild (som så klart behöver utökas i fallet där vi har 4x4 rutor):

Notera: Om vi valt att representera tetrominos med precis så många rutor som behövs för
att en Poly
ska få plats, kunde vi fått oväntade resultat. Nedan skulle
t.ex. "balken" inte rotera runt sin mittpunkt utan runt sin övre
vänstra kvadrat:
Därför valde vi istället en kvadratisk array av storlek 4x4, som i rotationsstandarden som presenterades tidigare, roterar allt runt den arrayens centrumpunkt:
Att göra 5.15.1: Rotation
-
Ge
Board
funktionalitet för att rotera det block som just nu håller på att falla ner. Du måste kunna rotera både åt höger och åt vänster.Innan man "sparar" det roterade blocket måste man testa om detta skulle krocka med någon ruta som redan är upptagen – i så fall är rotationen förbjuden och måste avbrytas, och man måste gå tillbaka till den Poly man startade med.
-
Lägg till kod för tangentbordsstyrning så att rotationsfunktionen i
Board
anropas då man trycker "pil upp". Ni kan även anropa funktionen från andra tangenter om ni vill, men pil upp måste också fungera.
5.15. Borttagning av rader
Att göra 5.15.1: Försvinnande rader
-
Se till att en rad kvadrater "försvinner" om den blir helt full. Kom ihåg att inte flytta med eventuella
OUTSIDE
-värden! -
Testa. Tips: Tvinga tillfälligt spelet att bara skapa kvadrater ("O"). Då testar du även att spelet kan ta bort två rader samtidigt!
5.17. Poänghantering
5.17.1. Poänghantering
Nu är det dags att lägga till poänghantering i spelet:
- 100 poäng om 1 rad försvinner
- 300 poäng om 2 rader försvinner på samma gång
- 500 poäng om 3 rader försvinner på samma gång
- 800 poäng om 4 rader försvinner på samma gång
Detta är en förenkling av ett vanligt poängsystem som även tar hänsyn till olika spelnivåer (hastigheter) och andra finesser som vi inte har implementerat.
5.17.1. Poänghantering
Inför poänghantering i spelet enligt poängsättningen ovan.
Bestäm en lämplig plats där nuvarande poäng kan lagras.
Se till att nuvarande poäng hela tiden visas någonstans i spelet.
5.17.2. Highscorelista
Här undersöker vi designmönstret Singleton och examinerar dess användning. Detta kan vara nytt även för många vana programmerare, så här diskuterar vi den tänkta lösningen mer detaljerat.
Nu fungerar poängen, men man kan bara se dem medan man spelar. Det vore bra om man kunde lagra poängen så att man efter varje spel kan se en highscorelista.
Vi tänker oss nu att man vill kunna spela många parallella spel, och att man i alla de spelen vill kunna använda sig av samma gemensamma highscorelista. Då är frågan hur samtliga spel ska kunna komma åt denna enda lista. En möjlighet är att det finns en central plats där alla spel "skapas", och att denna alltid skickar med highscorelistan till spelen. Det skulle säkert fungera. En annan möjlighet är att vi använder oss av ett designmönster som heter Singleton. Där ser man till att det enbart kan skapas ett enda objekt av en viss klass, och att detta objekt blir tillgängligt på ett enkelt sätt för de som behöver det.
En Singleton med "ivrig initialisering" (skapa objektet direkt, inte vid första anropet) kan i Java implementeras på det här sättet:
public class HighscoreList {
// Skapa bara objekt av denna typ EN gång, när klassen laddas.
private static final HighscoreList INSTANCE = new HighscoreList();
// Privat konstruktor, så ingen annan kan skapa fler objekt.
private HighscoreList() {
...
}
// Låt andra komma åt det unika objektet
public static HighscoreList getInstance() {
return INSTANCE;
}
// Flera fält och metoder, precis som i vilken klass som helst
private ...
public ...
}
För att komma åt det unika HighscoreList
-objektet kan
man nu helt enkelt anropa HighscoreList.getInstance()
.
Därefter kan man använda detta objekt som vanligt, men man vet att
alla andra som anropade HighscoreList.getInstance()
också har exakt samma objekt.
Detta är en helt annan sak än att göra alla fält och metoder
i HighscoreList
statiska! Den "lösningen" gör att vi
helt undviker att använda objekt och går över till procedurell
programmering med globala procedurer och globala variabler. Med
Singleton är en HighscoreList verkligen ett objekt, som följer
objektorienteringens principer. Den statiska variabeln (INSTANCE) och
metoden (getInstance) används bara för att på ett enkelt sätt komma åt
detta objekt.
Vi visar Singleton för att ge er en större repertoar av tekniker, men som alla tekniker är Singleton inte alltid bäst. Ett alternativ till Singleton är att någon skapar ett objekt, och sedan "skickar med" det till alla som behöver det. Detta förenklar också om man någon gång skulle kunna vilja ha fler än ett objekt av den givna typen. Singleton är alltså främst användbart där man (1) vet att man absolut aldrig någonsin skulle kunna vilja ha mer än ett objekt av den typen, och (2) tycker att nackdelen med att göra det objektet globalt tillgängligt överväger nackdelen med att istället skicka runt det till de som behöver det.
Det finns även flera andra sätt att implementera Singletons i Java. Viktigt är att det finns flera sätt som verkar rimliga på ytan, men som inte alls fungerar. Slå alltid upp korrekt sätt så du är säker att det fungerar! Överkurs: Vissa sätt att implementera double-checked locking fungerar inte i multitrådade program.
5.17.2. Highscorelista
För att en highscorelista ska fungera utan att lagras i fil måste man kunna fortsätta spela en ny omgång när ett spel är över. Se till att detta fungerar – vi har inte särskilt stora krav på att det ska vara elegant.
Skapa en
Highscore
-klass som lagrar antal poäng plus namn på den som fick poängen.Skapa en
HighscoreList
-klass. Den ska vara en Singleton enligt ovan, och ska innehålla funktionalitet för att lägga till highscores samt få fram samtliga highscores.Så snart en spelomgång avslutas ska programmet fråga användaren efter ett namn och lägga till användaren med sin poäng sist i listan (just nu ska listan inte sorteras).
Därefter ska programmet visa åtminstone de 10 första personerna i highscorelistan, i GUI eller med
System.out.println()
. Vi fokuserar på Singleton, inte på hur det visas.
Viktigt: Poängen med Singleton är att man bara ska få ett objekt av en klass. Den ska alltså bara användas när det verkligen är det man vill, nu och i framtiden! Om vi hade tänkt implementera grupper av spelare (kanske olika "divisioner"), där varje grupp ska ha sin egen highscorelista, kan vi så klart inte använda en Singleton.
Fördel: Eftersom vem som helst kan anropa en statisk metod för att få tag på klassens unika objekt, behöver man inte skicka runt det objektet (potentiellt genom flera nivåer av metodanrop och klasser) för att den som behöver det ska få tag på det.
Nackdel: En Singleton är en sorts global variabel. Sådana har också nackdelar. De kan t.ex. leda till onödiga beroenden mellan olika delar av ett program, och svårigheter att reda ut vilka beroendena egentligen är (eftersom olika delar man har tillgång till samma objekt trots att objekten inte uttryckligen "skickas" dit).
5.17.3. Sorterade highscores
Här undersöker vi designmönstret Strategy och examinerar dess användning. Detta kan vara nytt även för många vana programmerare, så här diskuterar vi den tänkta lösningen mer detaljerat.
Nu kan vi se highscorelistor, men vi vill se den högsta poängen först!
För att åstadkomma detta behöver vi kunna sortera
listorna. Den större delen av en sorteringsalgoritm brukar vara
generell och fungera för godtyckliga sorters element, och den
delen finns så klart "inbyggd" i Java. Men sortering bygger
oftast på att man kan jämföra två godtyckliga element i en lista
och tala om vilket av dem som borde vara först, och just denna del
är helt och hållet specifik för varje elementtyp. I vårt fall
handlar det alltså om att tala om för sorteringsalgoritmen hur man
tar reda på vilket av två Highscore
-objekt som ska
vara först i den sorterade listan.
Det finns flera olika sätt att göra detta på. Ett sätt är genom
designmönstret Strategy. Detta låter oss "plugga in"
jämförelser genom att först skapa en jämförare, ett
objekt som vet hur man jämför Highscore
-objekt, och
därefter skicka med denna jämförare som parameter till
sorteringsmetoden. Jämförarobjektet är alltså ett sätt att
implementera en sorteringsstrategi som sorteraren kan
använda sig av.
Vilken typ ska "jämförar-parametern" till sorteringsmetoden ha? I
Javas standardsortering används
gränssnittet Comparator
. Vi förutsätter att du
känner till generics och typ-parametrar <T>
.
Läs annars föreläsningen om datatyper.
public interface Comparator<T> {
/**
Compares its two arguments for order.
Returns a negative integer, zero, or a positive integer
as the first argument is less than, equal to, or greater than
the second.
*/
public int compareTo(T o1, T o2);
g}
Detta gränssnitt finns redan i Java. Vi behöver nu implementera det
i en poängjämförare. Vad vi jämför (typen T
) är
alltså Highscore
:
public class ScoreComparator implements Comparator<Highscore> {
public int compare(Highscore o1, Highscore o2) {
...
}
}
Vi kan sedan sortera listan:
List<Highscore> scores = ...;
...
scores.sort(new ScoreComparator());
5.17.3. Sorterade highscores
Skapa en jämförarklass, en
ScoreComparator
, enligt ovan.Använd denna för att se till att highscores sorteras (antingen vid visning eller varje gång en highscore läggs till i listan).
Testa!
5.18. Powerups
Info: State-mönstret
Här undersöker vi designmönstret Strate och examinerar dess användning. Detta kan vara nytt även för många vana programmerare, så här diskuterar vi den tänkta lösningen mer detaljerat.
Det är nu dags att titta på en teknik för att "plugga in" olika beteenden i en klass.
Grundtanken är att vi har en klass som har ett stabilt grundbeteende, men där vissa delar av beteendet kan ändras då och då, kanske ganska radikalt. Till exempel kanske vi skriver ett spel där spelaren kan vara i olika lägen beroende på om man har hittat vissa "powerups" som ger extra krafter. Det kan man ju ordna via vanliga villkorssatser:
public enum PlayerMode {
NORMAL, // Standard mode
STRONGER, // Stronger and less vulnerable
FASTER, // Runs faster
GHOST, ... // Can walk, run or jump through solid matter
}
public class Player {
private PlayerMode mode;
public void jump() {
if (mode == PlayerMode.NORMAL) {
// Ordinary jump
} else if (mode == PlayerMode.STRONGER) {
// Jump a bit higher
} else if (mode == PlayerMode.GHOST) {
// Can jump through platforms from
// below, land on platforms from above
} ...
}
public void run() {
...
}
}
Men att använda villkorssatser har vissa nackdelar:
Informationen om vad det innebär att vara
STRONGER
sprids ut i många metoder (jump, run, ...).Varje gång man lägger till en ny typ av powerup måste man ändra Player-klassen!
Alltså går det inte heller att lägga till nya typer av powerups om man inte har källkoden till Player...
En idé kanske kunde vara att göra Player abstrakt och skapa nya konkreta subklasser: GhostPlayer, StrongPlayer, FastPlayer, och så vidare. Grundbeteendet skulle då ligga kvar i Player, medan varje subklass skulle ha sin egen implementation av jump() och run(). Men då måste vi byta ut spelarobjektet varje gång spelaren får en ny förmåga! Det går ju inte att byta klass på ett existerande objekt. Det vi vill ha är en spelare, vars beteende ändras.
Därför kan man ibland använda den alternativa lösningen att plugga in ett beteende.
public interface PowerupState {
public void jump(Player p);
public void run(Player p);
}
public class NormalState implements PowerupState {
public void jump(Player p) {
// Make p jump a short distance
}
...
}
public class GhostState implements PowerupState {
public void jump(Player p) {
// Significant difference in code:
// Jump through other objects
}
...
}
public class Player {
private PowerupState pstate;
public Player() {
this.pstate = new NormalState();
}
public void jump() {
...
// Ask the state object to do the actual jump – delegate!
pstate.jump(this);
...
}
...
}
Det vill säga:
Vi vill kunna byta ut delar av beteendet hos ett objekt. Man kan säga att objektet är i flera olika tillstånd (state), och att varje tillstånd ger ett eget beteende.
Skillnaderna i beteende är inte sådana som enkelt representeras med ett enda värde – om bara hopplängden skulle vara olika, skulle vi helt enkelt ha ett fält "int hopplängd;".
Det är inte heller så att vi bara vill ändra en eller ett par rader kod för att implementera skillnaden. Vi behöver en hel del separat kod för varje tillstånd.
Därför skapar vi ett gränssnitt som beskriver just det beteende vi vill byta ut: Hoppandet och springandet. Sedan skapar vi en klass för varje konkret variation av beteendet: Hoppa och spring på normalt sätt, hoppa och spring som ett spöke, och så vidare.
Player har ett objekt av den givna typen (PowerupState). Detta sätts från början till ett objekt som representerar någon form av standardbeteende (new NormalState()).
När man dynamiskt vill byta beteende:
this.pstate = new FasterState();
På det sättet använder man objektorientering med sen bindning (dynamisk dispatch) för att variera beteendet, istället för en villkorssats (if / switch): När man anropar pstate.jump() är det den för tillfället aktuella tillståndsklassens kod som körs.
Viktigt: Detta används alltså om man dynamiskt vill byta ut lite komplexare beteenden. Om koden verkar bli mer komplicerad av att använda detta mönster, är det antagligen bättre att låta bli!
5.18.1. Gränssnitt och defaultimplementation
I Tetrisprojektet vill vi att man i vissa lägen ska kunna få "extra krafter" som hjälper till i spelet. Detta kallas ofta powerups.
Varje powerup påverkar spelmekaniken på ett specifikt sätt. Till exempel kan det finnas olika powerups som påverkar kollisionshanteringen. I standardläget stannar ju en fallande Poly så snart den når ner till en kvadrat som redan finns på spelplanen. Vi kan också tänka oss att en Poly skulle falla rakt genom existerande kvadrater, så att den rensar en väg ner till ett hål som var svårt att fylla, eller att den skulle falla tungt så att de underliggande kvadraterna lossnar och ramlar så långt ner de kan.
Här vill vi alltså kunna plugga in olika beteenden när en Poly når ner
till en existerande kvadrat. Vi skulle kunna göra detta med en
enum-variabel som räknar upp alla tänkbara beteenden
(NORMAL
, FALLTHROUGH
, HEAVY
)
tillsammans med en switch-sats som gör att koden beter sig olika beroende
på nuvarande värdet på denna variabel. I så fall skulle
vi centralisera kunskapen om alla existerande beteenden. Nu
väljer vi istället att modularisera detta via State-mönstret som
diskuterades ovan, så att varje beteende blir en egen klass.
Vi kommer därför att behöva ett gränssnitt för kollisionshanterare. Vi ska ge vissa tips om hur detta ska implementeras, men låter er i övrigt basera implementationen på vad ni har lärt er av föreläsningen om designmönster.
Första steget blir att flytta ut koden för kollisionshantering till en egen klass.
5.18.1. Flytta kod för kollisionshantering
-
Skapa ett gränssnitt som heter
FallHandler
. Detta ska ta över en del av funktionaliteten som just nu ligger iBoard
och bör därför ligga nära den klassen. -
Lägg till metodsignaturer (tomma metodhuvuden) i
FallHandler
motsvarande fallhanteringen du nu har iBoard
, inklusive både kollisionshantering och detektering av när enPoly
ska "fastna" på botten. Koden som överför kvadraterna från denPoly
som har fastnat till spelbrädet kan ligga kvar iBoard
.Om fallhanteringen inte är tydligt separerad från annan funktionalitet i
Board
kan du behöva genomföra den separationen först. -
Se till att de nya metoderna får en ny parameter av typ
Board
. När fallhanteringen inte längre ligger iBoard
måste den ju få veta vilket spelbräde det gäller! -
Skapa en klass som heter
DefaultFallHandler
som implementerarFallHandler
. Flytta implementationen av fallhantering frånBoard
till den nya klassen. Lägg även här till den nya parametern.Den kod du just flyttade försöker titta på spelbrädesinformation i
this
, menthis
är ju numera en fallhanterare, inte ettBoard
. Skriv om vid behov!Om koden försöker använda sig av den interna representationen (t.ex. en 2D-array av kvadrater) kan du behöva introducera nya getters så att externa klasser som
DefaultFallHandler
inte behöver tillgång till interna hemligheter. Den exakta representationen av ett helt spelbräde (t.ex. en 2D-array) ska dock fortfarande vara hemlig! -
Ge
Board
en privatFallHandler
som sätts till enDefaultFallHandler
. Se till att Board anropar lämpliga metoder i detta objekt. -
Testa! Om allt är korrekt ska spelet fortfarande fungera exakt som tidigare.
5.18.2. En första powerup
Nu är det dags att skapa en första powerup – ett första
alternativ till DefaultFallHandler
.
5.18.2. En första powerup
Din första powerup ska låta en Poly falla rakt ner, genom existerande kvadrater, ända till den når botten. Där kan den "täppa till" hål som har uppstått nära botten.
Skapa klassen
Fallthrough
som implementerarFallHandler
.Kopiera
hasCollision()
-koden frånDefaultFallHandler
.Modifiera koden så att:
Om den fallande brickan överlappar OUTSIDE, detekteras en kollision.
Om den fallande brickan överlappar någon annan kvadrat, ignoreras detta.
När brickan har nått botten ska dess kvadrater ersätta det som fanns på den tidigare positionen. Detta är ungefär vad som har hänt tidigare när en bricka faller. Skillnaden är att det som ersätts inte bara är EMPTY utan även kan vara andra kvadrater.
-
Hitta på ett sätt att trigga denna Powerup. Till exempel kan man få en
Fallthrough
var tionde Poly, eller varannan gång man har tagit bort en rad, eller något annat villkor. (Ofta får man powerups när man "plockar upp" dem någonstans på skärmen, men detta kan vara onödigt komplicerat att implementera.)När
Board
triggar en Powerup ska den alltså helt enkelt sätta om kollisionshanteraren till ennew Fallthrough()
. När nästa Poly har fallit ner skaBoard
sätta tillbaka kollisionshanteraren till ennew DefaultFallHandler()
.Se till att skärmen hela tiden visar om man har en powerup, och i så fall vilken. Man kan t.ex. lägga till en metod
getDescription()
iFallHandler
, så att varjeFallHandler
(inklusive default) kan ge en egen beskrivning av sig själv. -
Testa!
-
Fick
DefaultFallHandler
ochFallthrough
onödigt mycket gemensam, repeterad kod? Öva gärna på att skapa en gemensam (abstrakt) superklass där det gemensamma kan representeras. Duplicerad kod kan ge komplettering.
5.18.3. En andra powerup
För att verkligen utforska det här sättet att programmera borde vi skapa en powerup till.
5.18.3. En andra powerup
Skapa en powerup där den Poly som faller är extremt tung, så att den knackar loss underliggande kvadrater och trycker dem nedåt. Detta kan vara en väldigt kraftfull powerup, eftersom ett tryck på rätt plats kan få många rader att bli fulla på samma gång.
-
Skapa en ny kollisionshanterare:
Heavy
. Se till att den "trycker ner" kvadrater enligt följande:Om den fallande brickan överlappar OUTSIDE, detekteras en kollision.
Om den fallande brickan överlappar någon annan sorts kvadrat, vill vi se om detta kan "fixas" genom att samtliga överlappande kvadrater "trycks ner".
Man behöver alltså först testa om det för samtliga överlappande kvadrater finns tomma hål längre ner i samma kolumn.
Om detta är falskt, detekterar vi en kollision så att den fallande brickan fastnar (översta bilden nedan).
Om det istället är sant, ska samtliga överlappande gamla kvadrater tryckas ner ett steg (mittersta bilden nedan) och ingen kollision rapporteras. Detta kan i sin tur trycka ner andra kvadrater ett steg, men bara fram till nästa hål i denna kolumn (mittersta bilden visar hur två kvadrater trycks ner ett steg på samma gång).
Sedan går spelet automatiskt vidare och försöker flytta brickan ytterligare ett steg nedåt, vilket kan trycka ner kvadrater ytterligare steg om det finns flera hål (tredje bilden nedan).
Denna hanterare kommer också att behöva kollapsa rader, om de kvadrater som "lossnar" ramlar ner och fyller rader. Detta kan göras genom att man inför en särskild metod i
Board
för att kollapsa en given rad. I exemplet nedan bildar de gröna brickorna en sådan rad.-
Se till att
Heavy
kan triggas på något sätt. -
Testa och iterera till du är nöjd!
5.19. Kodgenomgång
Info: Kodgenomgång
Nu är det dags att gå genom koden innan inlämning och att demonstrera resultatet för handledaren. Du kan göra detta i vilken ordning du vill.
5.19.1: Allmän kodgenomgång
Även om spelet nu fungerar som det ska, är det inte säkert att koden ser ut riktigt som du vill. Gör en ordentlig genomgång, nu när du har lärt dig mer! Det är viktigt att verkligen göra detta, och inte bara säga att "jag vet att jag borde göra annorlunda här". Som vi har sagt på andra ställen lär man sig ofta mer på att göra rätt än på att veta rätt.
5.19.2: Inspektioner
Missa inte!
Ett krav är att köra IDEAs kodinspektioner med profil TDDD78-2018-v1. Har du inte använt IDEA måste du importera projektet till IDEA för att köra inspektionerna.
Välj Analyze | Inspect Code i menyn.
-
Välj inspektionsprofil "TDDD78-2018-v1" och tryck OK.
Använd INTE defaultprofilen. Saknar du "TDDD78-2018-v1" jobbar du antagligen hemma och har glömt att importera kursens inställningsfil.
Det här är till stor del ett sätt att lära sig skriva kod som inte bara gör vad den ska utan även är välskriven och strukturerad. Därför är detta en egen uppgift i Tetrisprojektet, inte bara ett inlämningskrav.
Förstår du inte en varning? Utmärkt! Då har du en chans att lära dig något nytt, redan innan du lämnar in koden (och innan du börjar med projektet)! Läs IDEAs inbyggda beskrivningar, läs våra utökade beskrivningar, och fråga gärna handledaren eller (i sista hand) examinatorn.
Inget automatiskt inspektionsverktyg är 100% korrekt. Det kommer nästan alltid att finnas några varningar som är "felaktiga", där du i själva verket har gjort rätt. Alla sådana måste kommenteras på plats i koden, och som en del av examinationen måste du ge en god motivering till varför du vet att du har rätt och IDEA har fel. Du får som sagt gärna fråga handledaren, men svaret måste ändå skrivas in där, för att demonstrera vad du har lärt dig!
5.20. Dags att demonstrera!
Avslutning
Du har nu implementerat ett spel, läst om varför vi implementerar på ett visst sätt, och på det sättet lärt dig mer om objektorienterad modellering och programmering. Du har också fått en praktisk introduktion till flera designmönster: Model-View-Controller, Observer, Singleton, och Strategy.
Som synes resulterar Tetrisprojektet inte nödvändigtvis i ett 100% fullständigt spel. Poängen sparas inte i en fil, blocken faller inte snabbare när tiden går så att spelet blir svårare, och så vidare. Det finns två anledningar till det: Dels skulle det mest kräva mer av samma typ av programmering som du redan har provat på, dels måste du ha gott om tid över till projektet.
Demonstration
-
Det är nu dags att demonstrera slutresultatet för labbhandledaren. Följ sedan instruktionerna nedan för att lämna in koden.
5.21. Kodinlämning
LÄS NOGA!
Syfte
Vi har strikta instruktioner för inlämning. Om vi behöver lägga ner 10 minuter extra per student "i onödan" tar det över 20 timmar som vi kunde ha lagt på handledning och kursutveckling! Därför måste vi returnera labbar som inte följer instruktionerna för komplettering.
Inlämningsprocedur
-
Se till att du har demonstrerat. Handledaren kan ha kommentarer redan vid demo, så du ska demonstrera före inlämning (och före deadline).
-
Projektet måste lämnas in i IDEA-format med en .ipr-fil så att vi snabbt kan öppna den och navigera genom koden.
Har du inte använt IDEA: Skapa ett projekt enligt instruktionerna i labb 1. Kom ihåg att ändra More Settings / Project format till ".ipr (file based)"! Se till att projektfilerna checkas in.
-
Se till att du verkligen har kört IDEAs kodinspektioner!
Har du kvar varningar som varken har fixats eller motiverats kommer du att få komplettering. Har du använt fel inspektionsprofil kan det finnas varningar kvar.
-
Är detta en komplettering? Beskriv i så fall i filen "kompletteringar.txt", i rotkatalogen för projektet, hur varje enskild kommentar från handledaren har hanterats: Vad som ändrats och var i koden, hur du har löst problemet, och annan information som är relevant för att handledaren lätt ska se vad som gjorts.
Gör du flera kompletteringar ska gamla kommentarer vara kvar och tydligt skilda från nya kommentarer (markera med kompletteringsdatum). Detta underlättar för oss och är en del av examinationen där du visar att du förstår varför kompletteringen behövdes.
-
Checka in all kod och dokumentation, och pusha till GitLab. Kontrollera noga att allt verkligen är incheckat, inklusive .ipr-fil och eventuell kompletteringsfil.
-
Skapa en tagg (etikett) i Gitlab. Detta visar oss vilken version av koden som lämnades in vid inlämningsdatumet.
Logga in på gitlab.
Gå till ditt projekt och välj "tags".
Välj "New tag".
Sätt taggnamnet till "t1" om detta är första gången du lämnar in Tetris. Om du får komplettering får du nästa gång sätta en ny tagg "t2", och så vidare.
Låt "Create from" vara kvar på "master".
Meddelanden och "release notes" behövs inte.
"Create Tag"!
-
Lämna in via vårt centrala gitlab-repo!
2012–2018 Jonas Kvarnström, Mikael Nilsson
Sidansvarig: Jonas Kvarnström
Senast uppdaterad: 2019-01-09