Labb 4: Ett fungerande Tetris
Nu återgår vi till Tetris – det är dags att vi får ett fungerande spel!
Efter föreläsning 5, pekare, interface, typhierarkier
Tetris 4.1: Timer
Att förstå 4.1.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. Just nu kan vi komma en bit på vägen genom att i alla fall slumpa om spelplanen med jämna mellanrum, motsvarande hur ofta ett block skulle flyttas ett steg nedåt.
Hur får man något att hända med jämna mellanrum? 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, CPU-belastning, med mera. Exempel:
Jobba 50 ms, vänta 500 ms Jobba 120 ms, vänta 500 ms Jobba 80 ms, vänta 500 ms Jobba 60 ms, vänta 500 ms Jobba 200 ms, vänta 500 ms
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.
java.util.Timer
! Det kan se ut att fungera en
del av tiden, men händelser kommer att inträffa i fel tråd vilket kan leda
till mystiska fel som enbart uppstår ibland.
Här ser vi en anledning till att man lägger klasser i olika paket. Om du i
senare steg inte får rätt Timer
, Action
eller
ActionEvent
så se till att följande imports finns under package
i början på filen:
import javax.swing.*;
import java.awt.event.ActionEvent;
I den här uppgiften ska vi bara göra en enkel utforskning av
Timer
. Senare, när vi har gått vidare till en
fullständig grafisk komponent, kommer vi att titta närmare på
lämplig modellering av spelet som helhet och var spelloopen
egentligen ska placeras.
Det vi behöver veta kan vi då lära oss av följande exempel och beskrivningen under.
final Action doOneStep = new AbstractAction() {
public void actionPerformed(ActionEvent e) {
// Gå ett steg i spelet!
}
};
final Timer clockTimer = new Timer(500, doOneStep);
clockTimer.setCoalesce(true);
clockTimer.start();
...
clockTimer.stop();
En Timer
ser alltså till att Swings egna
händelsehanteringstråd anropar en viss handling ("action") med
jämna mellanrum. I detta fall är det 500 millisekunder mellan
anropsstarterna, och det som anropas är den handling som vi
kallade doOneStep
. Eller rättare
sagt, Timer
vet att den får någon
sorts Action
som parameter, och att
alla Action
-objekt har
metoden actionPerformed
som den kan anropa.
Genom att anropa
setCoalesce(true)
ser vi till att anropen inte "köas
upp" om ett visst anrop till doOneStep()
skulle ta för
lång tid. Vi kan starta timern när spelet börjar och stoppa den när
spelet är över.
I övrigt kan även clockTimer.setLogTimers() vara intressant för felsökning. Se även Javadoc-dokumentationen som vi länkar till ovan!
Notera att anonyma klasser, som vi använde ovan, bara bör
användas för små klasser. Om implementationen
av actionPerformed()
blir lång kan man vilja ge
klassen ett namn istället:
class StepMaker extends AbstractAction {
public void actionPerformed(ActionEvent e) {
// Gå ett steg i spelet!
}
};
final Action doOneStep = new StepMaker();
Att göra 4.1.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. Handlingen behöver alltså på något sätt få tillgång till både spelplan och textarea. Slutresultatet ska helt enkelt bli att du ser en enkel "animering" på skärmen medan programmet körs. Du behöver inte kunna stoppa timern. -
Testkör och se att spelplanen slumpas om.
Efter föreläsning 6, GUI
Tetris 4.2: En grafisk spelplan
Det är nu dags att gå över från en textbaserad visning till en helt grafisk, även i Tetris.
Om du inte är van vid grafikprogrammering med Graphics2D i Java kanske du vill vänta till du har varit på grafikföreläsningen. Se även vanliga lösningar.
Att göra 4.2.1: Grafik del 1 – komponenten
Vi kommer att förändra
TetrisViewer
så pass mycket att det kan vara av intresse att spara undan den. En version borde så klart vara incheckad i versionshanteringen, men just i det här fallet kan vi också spara den nuvarande versionen som en kopia (så handledaren kan se det gamla). Högerklicka påOldTetrisViewer
och välj Refactor -> Copy och döp den till TetrisViewerOld, så har ni lättare att använda den igen om ni skulle vilja.Skapa en
TetrisComponent
-klass som är subklass tillJComponent
.Låt
TetrisComponent
ha en pekare till detBoard
som den visar. Den behöver alltså ett fält som pekar på ettBoard
, och den behöver en konstruktor som tar ettBoard
som parameter.Implementera metoden
getPreferredSize()
så att den returnerar den storlek du helst vill ha för den grafiska visaren. Enheten är pixlar. Om du vill anpassa komponentstorleken till skärmstorleken har vi gått genom på föreläsningarna hur man får fram skärmupplösningen. Tänk bara på attTetrisComponent
-komponenten inte kan vara riktigt så stor, eftersom menyer, knappar, ramar och annat också kan ta plats.
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!
Att förstå 4.2.2: Grafik del 2 – utritningen
Kom ihåg att 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. Om metoden blir stor kanske
den behöver delas upp i delmetoder / hjälpmetoder för att
förbättra läsbarheten.
Tänk på att paintComponent()
behöver rita upp både 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 annat sätt är att den har en
EnumMap<SquareType,java.awt.Color>
som lagrar mappningen. En EnumMap är en mappning, en uppslagningstabell liknande det som kallas dictionaries i Python – i detta fall med enSquareType
som nyckel och enColor
som värde. I Java finns dock ingen specialsyntax för mappningar, utan det är en klass som alla andra, och objekten manipuleras med metoder somput()
ochget()
.Med denna lösning kan din komponent till och med ta in en
EnumMap
som konstruktorparameter, så kan den som skapar komponenten lätt konfigurera uppslagstabellen uppslagningstabellen (och därmed färgerna) utifrån – betydligt mer flexibelt än att hårdkoda en switchsats ipaintComponent()
.
Exempel: paintComponent()
För att rita ut grafik används override på
funktionen paintComponent()
som ärvs
från JComponent
. Det kan t.ex. se ut så här:
@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. I exemplet vill vi rita ut en grön
rektangel med hjälp av ett Graphics2D
-objekt, men får
ett Graphics
-objekt som inparameter. Därför castas
detta först till Graphics2D
innan vi ritar ut
rektangeln. Det finns många olika sätt att rita i en komponent och
detta är bara ett exempel. Mer information finner du på
föreläsningarna samt nätet.
Att göra 4.2.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. Slutmålet är att brädet ska slumpas om med regelbundna mellanrum och att varje ny konfiguration ska visas på skärmen iTetrisComponent
-komponenten som finns i enJFrame
. För tillfället kan du be dinTetrisComponent
att rita om sig indirekt genom att timerhandlingen direkt anroparrepaint()
iTetrisComponent
-objektet (som dinTetrisViewer
har en pekare till). I en kommande uppgift ska vi använda ett mer principiellt sätt att fåTetrisComponent
att uppdatera sig vid ändringar.
Att förstå: Magiska konstanter
Här är det läge att påpeka att du ska undvika magiska konstanter, i betydelsen "konstanter som används i koden utan någon förklaring av var de kommer ifrån". Om t.ex. en kvadrat ritas ut i en konstant storlek av 30x30 pixel är det bra att lägga in namngivna konstanter som man kan använda varje gång man refererar till bredden eller höjden. Då blir det mycket lättare att läsa uttrycken och att förstå hur koden fungerar. Till exempel kan man lägga in:
Fältet
private final static int SQUARE_SIZE = 30;
i klassen, om detta används i flera metoder."Variabeln"
final int squareSize = 30;
i en enskild metod, om värdet bara används där.
I båda fallen ska man använda final
för att markera
att detta är tänkt att vara en konstant. I det första fallet
använder vi också static
för att vi inte behöver en
kopia av konstanten för
varje TetrisComponent
-objekt som skapas.
Namngivna konstanter är viktigt för läsbar kod, vilket i sin tur är viktigt för betyget på projektet!
Speciellt svårt att läsa magiska konstanter är det om du gör
beräkningar som utgår från dem och sedan stoppar in de uträknade
värdena i koden. Till exempel är det oftare lättare att förstå
betydelsen hos "if (x > SQUARE_SIZE - MARGIN)
"
än "if (x > 27)
", även för den som vet
att SQUARE_WIDTH
är
30
och MARGIN
är 3
.
Ännu snyggare än att lägga in en namngiven konstant kan det bli om man
(alltså TetrisComponent
) dynamiskt tar reda på hur stor man
är vid uppritningen (i paintComponent
), och anpassar
storleken på kvadraterna till detta. Då kan allt följa med när man ändrar
storlek på fönstret under körning. Tips: this.getSize()
,
där this
är din TetrisComponent
.
Tetris 4.3: Observer-mönster / lyssnare
Att förstå 4.3.1: Lyssnare och notifiering
Tidigare har du själv fått hitta på ett sätt att få spelplanen att ritas
om när något ändras. Det föreslagna sättet har varit att en timerhandling
som utförs med regelbundna mellanrum både ser till att spelmodellen
uppdateras (ett block faller nedåt) och att TetrisComponent
därefter ritar om sig. Problemet med den lösningen är att timerhandlingen
måste känna till precis vilka som vill veta när spelplanen ändrar
sig. Vi får därmed en alldeles för stark koppling mellan två
klasser (timerhandling och TetrisComponent
) som egentligen
inte alls borde behöva känna till varandra.
Ett annat sätt är att använda designmönstret Observer. I korthet innebär detta att de objekt som kan vilja veta när något händer ses som observatörer. De objekt där något kan hända håller reda på vilka observatörer som har registrerat sitt intresse, och informerar dem (via ett metodanrop) när något faktiskt händer.
Vi har faktiskt redan sett denna sorts "callback", till exempel i
Javas GUI-bibliotek. Alla ActionListener
s är en sorts
observatörer som är intresserade av att veta när någon t.ex. trycker på en
knapp. Metoder som addActionListener()
lägger till en
lyssnare som är intresserad av en specifik typ av handling. Klasser
som JButton
känner inte till era specifika lyssnare i förväg,
men de har ett sätt för lyssnarna att registrera sitt intresse. Om man
vill kan man registrera ett godtyckligt antal lyssnare på samma knapp.
I Tetris innebär detta att vi ser spelplanen som något "observerbart".
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"
som kan implementera ett passande lyssnargränssnitt. Varje observatör kan
då själv registrera sig som intresserad av vad som händer med ett visst
Board
.
Fördelen med detta tankesätt ä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 intresserade att
något har hänt. En av dessa intresserade råkar
vara TetrisComponent
, som tidigare har registrerat sitt
intresse.
Att göra 4.3.1: Lyssnare och notifiering
Skapa ett interface
BoardListener
med en metodpublic void boardChanged();
Lägg till i klassen
Board
ett privat fält som innehåller en lista av BoardListeners. Lägg också till en metodpublic void addBoardListener(BoardListener bl)
som adderar den givna lyssnaren till listan. I detta spel kommer vi inte att behöva något sätt att ta bort lyssnare, även om man så klart kan lägga till även en sådan metod för att vara mer fullständig.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.
Det går också bra att låta boardChanged
ta parametrar som
talar om vad som har ändrats, och/eller att skapa flera olika metoder i
BoardListener som anropas vid olika händelser: Ett block har flyttats, ett
block har "landat", spelet är slut, ...
Resultat
Slutresultatet av denna ändring ska bli att allt på skärmen ser ut precis som tidigare – men att koden har en bättre struktur. Den är mer modulär i och med att vi har minskat den starka kopplingen mellan datalagring och utritning på skärmen. Detta är en mycket viktig ändring.
Tetris 4.4: Fallande block
Att förstå 4.4.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 full av kvadrater, utan istället använda den 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. Vi vill istället att
spelmekanik och spelregler ska hanteras i Board
. (Ett alternativ hade
varit att separera spelmekaniken ytterligare och placera den i en
TetrisGame
-klass, medan Board
bara
innehåller just lagringen av kvadrater, men det gör vi inte i just
det här spelet.)
Skapa därför en tick()
-metod i Board
och
anropa den metoden från timerhandlingen.
Metoden tick()
kan sedan veta hur spelet drivs fram
ett steg. Då hamnar kunskapen om spelets regler
i Board
, medan timerhandlingen används uteslutande
för att driva spelet framåt i rätt fart.
Att göra 4.4.1: Fallande block
Ändra spelet så att det utgår från en tom spelplan istället för en framslumpad spelplan.
-
Implementera
Board.tick()
, som behöver titta om spelplanen just nu innehåller ett block (Poly
) som håller på att ramla ner. I så fall flyttar den det blocket ett steg nedåt. Annars slumpar den fram en blocktyp (T
,L
, ...) och placerar ett nytt block av den typen överst på spelplanen, centrerat. (Mer spelmekanik tillkommer senare.)ÖverkursDet finns många sätt att slumpa fram Tetrisblock. Det enklaste är att ha en uniform fördelning: I varje steg har varje blocktyp exakt samma sannolikhet som alla andra blocktyper. Det är så ni förväntas göra i den här labben!
Det finns dock en hel del mer att läsa om svagheterna med denna enkla slumpmetod, för den som är intresserad. Till exempel kan man få långa sekvenser av samma bit, eller långa sekvenser där en viss önskad bit aldrig kommer, vilket kan göra spelet mindre roligt. Den som vill kan läsa mer på The history of Tetris randomizers. Spendera bara inte för mycket tid på detta, och var medveten om att mer avancerade slumpmetoder inte ger "extrapoäng".
Se till att
tick()
anropas från timerhandlingen enligt ovan.
Resultat
Slutresultatet i denna uppgift blir ett testprogram där ett block slumpas fram och sedan faller nedåt. Beroende på hur ni har implementerat spelet kan blocket "falla av skärmen" och sedan fortsätta falla i all oändlighet (även om detta inte syns), eller så kanske spelet kraschar när ni kommer "för långt ner". Båda varianterna är OK i det här läget. Senare i projektet ska vi få blocket att stoppa när det når botten.
Tetris 4.5: Tangentbordsstyrning
Att förstå 4.5.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 mellan förflyttningarna nedåt. Eftersom key bindings hanteras asynkront, "i bakgrunden", ska detta fungera automatiskt.
Att göra 4.5.1: Styra tetrominos
Som diskuterats ovan vill vi att
Board
ska innehålla "logiken" för alla de "drag" man kan göra. Implementera alltså metoder iBoard
som anropas vid "draget" sidledsförflyttning. Dessa metoder ska helt enkelt flytta det nedfallande blocket ett steg åt sidan. Än så länge finns inget som kan vara "i vägen", så vi bryr oss inte om kollisionshantering just nu.Precis som alla andra metoder som ändrar på speltillståndet behöver denna metod anropa
notifyListeners()
för att informera alla intresseradeBoardListener
s om att något har ändrats. Däribland finns den grafiska komponenten som därmed ritas om varje gång ett block har flyttats i sidled.-
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. Ni kan även anropa dem från andra tangenter om ni vill, men pilarna måste fungera.
-
Testa. Prova även att skicka blocket utanför spelplanens kanter. Om detta leder till en krasch är det än så länge helt OK.
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.
Att göra: Kodinspektion
Glöm inte att inspektera koden igen, om det var ett tag sedan du gjorde det senast. Använd Analyze | Inspect Code i menyn. Se till att inspektionsprofilen "TDDD78-2020-v1" är vald och tryck OK. Gå genom de varningar du ser. Fråga handledaren om du inte förstår en varning eller tycker att den är omotiverad.
Tetris 4.6: Kollisionshantering och "game over"
Att förstå 4.6.1: Kollisioner
Hittills har vi gjort ett Tetris där ett block faller tills det hamnar utanför spelplanen. Vi har också möjlighet att styra blocket i sidled, men även då kan blocket hamna utanför spelplanen. Vi behöver detektera dessa tillstånd för att spelet skall bete sig som vi önskar. Då kommer vi också att få flera block på planen, eftersom gamla block stannar kvar när de inte längre kan fortsätta falla. Fallande block ska då förhindras från att falla igenom eller flyttas in i befintliga block.
För att se till att block inte överlappar gamla "kvarlämnade" kvadrater på skärmen behöver vi en enkel form av kollisionshantering. Varje gång vi flyttar det fallande blocket ändrar vi först dess koordinater provisoriskt (tillfälligt), 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.
Hur ser vi då till att blocket inte kommer för långt till vänster eller höger? Det är ju en helt annan situation, eftersom det inte finns några gamla block där som stoppar oss. Måste vi börja testa blockets koordinater mot spelbrädets höjd och bredd? Det är en möjlighet, men när vi lägger till rotation blir det jobbigt att hålla reda på var varje block verkligen börjar och slutar...
Men om det är en helt annan situation kanske vi kan göra det till samma situation. Med andra ord, vi ser till att det finns något runt spelplanen som stoppar blocken. Alla instruktioner nedan bygger på att man följer den lösningen, men det är även tillåtet att implementera alternativa lösningar så länge man (1) förklarar dem, (2) accepterar att man får lösa de problem som skulle kunna uppstå.
Vi hittar på en ny SquareType
, OUTSIDE
,
och lägger sådana i en ram:
Att stöta mot ramen i sidled eller nedåt blir då lika lätt att detektera som att det fallande blocket stöter emot andra block i dessa riktningar. Detta är en vanlig teknik som vi vill att ni ska lära er.
Det är 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 4.6.1: Kollisioner
-
Lägg till
OUTSIDE
som ett nytt enum-värde iSquareType
. -
Se till att konstruktorn lägger till en ram runt spelplanen enligt bilden ovan. Det ska vara två "OUTSIDE" på varje sida, eftersom detta underlättar när vi senare implementerar rotation av block. Arrayens storlek ska alltså vara 4 större än den
width
ochheight
som anges. Tänk på att samma gamla värde ska fortfarande lagras iwidth
ochheight
, eftersom dessa fält representerar spelplanens "egentliga" storlek. -
Eftersom andra klasser inte ska vara medvetna om ramen måste gettern för
squares
ändras så att den indexerar "förbi" ramen. När någon ber om blocket på logisk position (0,0) ska gettern alltså returnera blocket på position (2,2) i arrayen. Testa att det fungerar precis som tidigare.Om du valt att behålla
BoardToTextConverter
i projektet behöver du eventuellt hantera en symbol förOUTSIDE
för att undvika varningar. -
Skriv en metod
hasCollision()
som returnerartrue
om det fallande blockets nuvarande position resulterar i att en icke-tom ruta i blocket överlappar en icke-tom ruta på spelplanen. BaraSquareType.EMPTY
räknas som tomt. Vi kan nu använda
hasCollision()
för att förhindra kollision i sidled. Metoden som förflyttar blocket när spelaren trycker pil vänster eller höger måste testa om detta resulterar i en kollision. I så fall måste metoden flytta tillbaka blocket eftersom förflyttningen var omöjlig. Implementera detta.Lägg till en kontroll i
tick
så att om blocket efter förflyttning nedåt kolliderar skall det flyttas tillbaka upp och stoppas in i brädet. Fältetfalling
skall i detta läge sättas tillnull
.Vi kan nu 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. Implementera detta test samt en flagga som håller koll på om det är "game over" och om så är fallet ser till att
tick
inte gör något.
Resultat
Resultatet i denna uppgift är ett spelbart Tetris som dock saknar rotation av blocket vilket blir nästa steg.
Att göra: Kodinspektion
Glöm inte att inspektera koden igen, om det var ett tag sedan du gjorde det senast. Använd Analyze | Inspect Code i menyn. Se till att inspektionsprofilen "TDDD78-2020-v1" är vald och tryck OK. Gå genom de varningar du ser. Fråga handledaren om du inte förstår en varning eller tycker att den är omotiverad.
Tetris 4.7: Rotation av tetrisblock
Att förstå 4.7.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 kan kännas knepigt, men är viktigt av två anledningar:
Det gör att spelet verkligen blir spelbart...
Det är ett bra test av koden vi har skrivit tidigare: Är den korrekt, eller beror den på underförstådda antaganden som egentligen inte är sanna?
Så hur roterar man ett block? Det enklaste sättet att rotera vår
tetromino åt höger är genom att flytta alla våra SquareType
enligt följande bild:

Vi gör då en funktion rotate(boolean right)
i Poly
som tar en boolean som inparameter. Här används inparametern för
att bestämma om vi roterar åt vänster eller höger. Rent praktiskt
behöver vi en ny tom array eller en ny Poly
som vi efter hand kan stoppa in de olika
rutornas innehåll i. När vi är klara byter vi till den nya
arrayen/polyn.
För ytterligare ledning kan du se följande metod. Den skapar först
en ny Poly
av samma storlek som den nuvarande Poly
n. Därefter kopierar den SquareType
s
till den nya Poly
n
enligt ovanstående mönster.
Metoden roterar enbart åt höger. Tänk på att two wrongs don't make a right – but three lefts do, och tvärtom. Med andra ord, man kan rotera åt vänster genom att rotera åt höger tre gånger. Alternativt kan man så klart skapa en motsvarande rotateLeft() som roterar åt vänster i ett enda steg.
public Poly rotateRight() {
Poly newPoly = new Poly(new SquareType[size][size]);
for (int r = 0; r < size; r++) {
for (int c = 0; c < size; c++){
newPoly.squares[c][size-1-r] = this.squares[r][c];
}
}
return newPoly;
}
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 4.7.1: Rotation
-
Lägg till en
rotate(boolean right)
-metod iBoard
och se till att den roterar det block som just nu håller på att falla ner, åt det håll som anges.Om det inte finns något nedfallande block ska metoden inte krascha utan helt enkelt inte göra något alls.
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. Det enklaste sättet att göra detta är troligen att man sparar undan det ursprungliga blocket, gör en roterad kopia, ser om kopian krockar, och därefter väljer vilket av de två blocken man ska behålla. -
Lägg till kod för tangentbordsstyrning så att blocket roteras åt höger när man trycker "pil upp" och åt vänster när man trycker "pil ned". Ni kan även anropa funktionen från andra tangenter om ni vill, men de angivna tangenterna måste också fungera för att handledaren ska kunna testa.
Ramtjocklek
Vi kan nu se varför ramen i en tidigare uppgift behövde vara två block stor. Om ett I-block placeras intill ramen och sedan roteras kan det leda till att den nya positionen hamnar två steg utanför spelplanen.
Tetris 4.8: Borttagning av rader
Att göra 4.8.1: Försvinnande rader
-
En av grundtankarna bakom Tetris är att en rad kvadrater "försvinner" om den blir helt full. Implementera detta, så att du kanske kan spela lite längre innan spelet tar slut!
När din nya
Board
-metod tar bort alla fulla rader och flyttar ner övriga rader: Kom ihåg att inte flytta medOUTSIDE
-värdena!Blir det svårt att fylla en rad? Testa att tillfälligt tvinga spelet att bara skapa kvadrater ("O"), så kan du testa borttagningen enklare. Då testar du även att spelet kan ta bort två rader samtidigt.
Avslutning
Här slutar fjärde laborationen. Det är dags att visa och demonstrera slutresultatet för din handledare – det krävs för att få godkänt!
Gå sedan vidare och läs instruktionerna i början av labb 5 – oavsett om du vill göra denna labb, för möjlighet till högre betyg på kursen, eller inte. Där får du bland annat en länk till inlämningsinstruktionerna för koden – det räcker inte att bara demonstrera och pusha koden till Gitlab!
Labb av Jonas Kvarnström, Mikael Nilsson 2014–2020.
Sidansvarig: Jonas Kvarnström
Senast uppdaterad: 2020-02-25