Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.
Excerpt

Denne oppgaven er en videreføring av de tidligere spilloppgavene TicTacToe, Sokoban, Sudoku og Sudoku Battleship, hvor en skal gjøre rutenettet observerbart og implementere et par grensesnitt, så spillogikken kan kobles til et ferdig skrevet grafisk brukergrensesnitt. I tillegg skal spillet utvides med ekstra funksjonalitet.

I denne oppgaven skal spillogikken og (det innkapslede) rutenettet fra tidligere spill kobles til et ferdig skrevet grafisk brukergrensesnitt (GUI). Det samme GUI-et skal kunne fungere mot mange spill og realiseringsteknikker, og den beste måten å gjøre det på er å definere spesifikke grensesnitt som brukergrensesnittet kan forutsette er implementert. De ulike grensesnittene svarer til ulik type funksjonalitet som finnes i rutenettbaserte spill for å kunne kobles til et GUI.

...

Dette er en videreføring av de samme brukergrensesnittene som ble utdelt i tidligere utgaver av spilloppgavene. Nytt for brukergrensesnittet utdelt i denne oppgaven er at de nå også skal ha mulighet for å registrere seg som lyttere på spillklassen, for å legge til rette for oppdatering av skjermbildet når det skjer endringer i tilstanden.

Filene som er nødvendig for å knytte ditt spill opp mot vårt GUI er tilgjengelig i Del 2 av denne oppgaven.

ObservableGrid- og GridListener-grensesnittene

Det første grensesnittet er knyttet til rutenettdata og heter GridProvider ObservableGrid. Tanken er at GUI-et må kunne spørre rutenettet om dimensjoner og innhold og kunne registrere seg som lytter, slik at det holdes oppdatert når rutenettet endres. Dette er observert-delen av observatør-observert-teknikken. Observatør-delen av observatør-observert-teknikken utgjøres av GridListener-grensesnittet, og siden det er GUI-et som skal holdes oppdatert, så er det den ferdigimplementerte GUI-klassen som må implementere GridListener. GUI-klassen utdelt i Del 2 av denne oppgaven implementerer allerede GridListener, så det er kun nødvendig å la din spillklasse implementere ObservableGrid og samt skrive logikken for å varsle registrerte GridListener-objekter når det skjer endringer i rutenettet.

PlantUML Macro
interface GridProviderObservableGrid {
    public int getGridWidth()
    public int getGridHeight()
    public Object getGridElement(int x, int y)
    public +void addGridListener(GridListener gridListener)
    public +void removeGridListener(GridListener gridListener)
}

interface GridListener {
    +void gridChanged(GridProviderObservableGrid grid, int x, int y, int w, int h)
}

class DinRutenettklasse DinSpillklasse{
}

GridProviderObservableGrid <|--.. DinRutenettklasseDinSpillklasse
DinRutenettklasseDinSpillklasse --> "*" GridListener: gridListeners
Code Block
languagejava
titleGridProvider- og GridListener-grensesnittene
collapsetrue
public interface GridProvider {
    
    /**
     * Gets the width (number of columns) of the grid
     * @return the width of the grid
     */
    public int getGridWidth();
    /**
     * Gets the height (number of rows) of the grid
     * @return the height of the grid
     */
    public int getGridHeight();
    /**
     * Gets the element at the specified x (column) and y (row) position 
     * @param x the x-coordinate of the element
     * @param y the y-coordinate of the element
     * @return the object at the specified position
     */
    public Object getGridElement(int x, int y);
    /**
     * Adds the listener, so it will be notified when the grid changes
     * @param gridListener the listener to add
     */
    public void addGridListener(GridListener gridListener);
    /**
     * Removes the listener, so it no longer will be notified when the grid changes
     * @param gridListener the listener to remove
     */
    public void removeGridListener(GridListener gridListener);
}

public interface GridListener {
    /**
     * Notifies the listener that the grid has changed. The changed region is a rectangle at x,y with dimensions w,h.
     * @param grid the grid that has changed
     * @param x the x coordinate of the changed rectangle
     * @param y the y coordinate of the changed rectangle
     * @param w the width of the changed rectangle
     * @param h the height of the changed rectangle
     */
    public void gridChanged(GridProvider grid, int x, int y, int w, int h);
}

GridProvider har følgende metoder for rutenettets dimensjoner og innhold:

  • int getGridWidth() - returnerer bredden eller antall kolonner i rutenettet, f.eks. 3 for standard Tic Tac Toe
  • int getGridHeight() - returnerer høyden eller antall rader i rutenettet, f.eks. 3 for standard Tic Tac Toe
  • Object getGridElement(int x, int y) - returnerer objektet som er lagret i en bestemt rute angitt med x,y-koordinater (altså kolonne,rad). Dette kan være noe så enkelt som et tegn, f.eks. 'x', 'o' eller ' ' for Tic Tac Toe, eller noe mer komplisert for Sudoku. Object brukes som returtype, for å kunne håndtere alle type spill og implementasjoner.

For å gå gjennom alle elementene i rutenettet vil en typisk lage en dobbel løkke:

Code Block
GridProvider grid = ...
for (int row = 0; row < grid.getGridHeight(); row++) {
	for (int column = 0; column < grid.getGridWidth(); column++) {
		Object element = grid.getGridElement(column, row);
		// gjør noe med element her
	}
}

GridProvider ObservableGrid har følgende metoder for lytterhåndtering:

  • void addGridListener(GridListener) - registrerer en lytter skal skal ha beskjed hver gang rutenettet endres
  • void removeGridListener(GridListener) - avregistrere en lytter som tidligere er registrert med addGridListener

Lytterne må implementere GridListener og dermed følgende metode:

  • gridChanged(GridProvider ObservableGrid grid, int x, int y, int w, int h) - metoden kalles for å gi beskjed om at en eller flere ruter i rutenettet (grid) innenfor det angitte rektanglet er endret. Rektanglet er angitt med posisjon x, y og dimensjonene w, h (bredde, høyde).

Hvis f.eks. metoden kalles med x=1, y=2, w=2 og h=1, så angir det at rutene 1,2 og 2,2 er endret. Det er opp til rutenett-klassen å avgjøre om endringer av flere ruter rapporteres med ett eller flere kall til gridChanged-metoden. Så endringer i rutene 1,2 og 2,2 kan også rapporteres med to kall, ett hvor x=1, y=2 og ett hvor x=2, y=2, med w=1 og h=1 for begge. Så selv om en kaller denne metoden, så er det ikke dermed sagt at alle rutene i det angitte rektangelet er endret (strengt tatt trenger ingen å være det).

 

GridGame- og GridOutput-grensesnittene

GridProvider og GridListener er grensesnitt som fokuserer på tilstanden til rutenettet, men ikke på hvordan tilstanden endres eller den overordned gangen i spillet, f.eks. oppstart eller brukerens kommandoer. De gir heller ingen mulighet til å styre hvordan rutenettet vises til brukeren eller vise annen type status for spillet. Dette er fokuset for GridGame- og GridOutput-grensesnittene.

PlantUML Macro
interface GridGame {
    public void init(String)
    public void run(GameOutput)
    public Integer doCommand(String)
    public GridProvider getGridProvider()
	public String getTextFor(Object)
    public String getImageFor(Object)
    public Integer gridElementSelected(int x, int y)
}

interface GameOutput {
    public void info(String message)
    public void warning(String message)
    public void error(String message)
}

class DittRutenettspill {
}

class GridGameGUI {
}

GridGame <|-- DittRutenettspill
DittRutenettspill --> GameOutput: gridOutput
GameOutput <|-- GridGameGUI
GridGameGUI --> GridGame: gridGame
Code Block
languagejava
titleGridGame- og GameOutput-grensesnittene
collapsetrue
public interface GridGame {
    
    /**
     * Loads and initializes the game to a (new) specified level. The String can be anything, e.g. a file name or a String encoding a level.
     * Note that there should be no output at this point.
     * @param level the String decribing a level, e.g. path or URL
     */
    public void init(String level);
    
    /**
     * Start the game (that was just initialized with init) and use the argument for all game output
     * @param output the object used for providing text-based output to the user (messages, warnings and errors)
     */
    public void run(GameOutput output);
    
    /**
     * Perform a command (line). This method may update the underlying grid and provide output to the user using the GameOutput object given to.
     * @param command
     * @return
     */
    public Integer doCommand(String command);
    /**
     * Returns the object that encapsulates the underlying grid.
     * @return  the object that encapsulates the underlying grid
     */
    public GridProvider getGridProvider();
    
    /**
     * Returns the text (label) corresponding to the provided grid element, or null of there is none.
     * @param o the object
     * @return the text (label) corresponding to the provided grid element
     */
    public String getTextFor(Object o);
    
    /**
     * Returns the location (URL or file) of the image corresponding to the provided grid element, or null of there is none.
     * @param o the object
     * @return the location (URL or file) of the image corresponding to the provided grid element
     */
    public String getImageFor(Object o);

 	/**
     * Perform an action corresponding to selecting a grid element. This method may update the underlying grid and provide output to the user using the GameOutput object given to.
     * @param command
     * @return A value indicating status of game, null for unfinished, 0 for a draw and negative or positive value for failure or success.
	 */
    public Integer gridElementInput(int x, int y);

    
    /**
     * Perform an action corresponding to moving in a certain direction. This method may update the underlying grid and provide output to the user using the GameOutput object given to.
     * @param command
     * @return A value indicating status of game, null for unfinished, 0 for a draw and negative or positive value for failure or success.
	 */
    public Integer directionInput(int dx, int dy);
}

public interface GameOutput {
    
    /**
     * Outputs a message to user, e.g. a status update.
     * @param message the message
     */
    public void info(String message);
    /**
     * Outputs a message to user, with a style indicating it is a warning.
     * @param message the warning message
     */
    public void warning(String message);
    /**
     * Outputs a message to user, with a style indicating it is an error.
     * @param message the error message
     */
    public void error(String message);
}

Spillets livssyklus

Det er GUI-et som lager en instans av spillklassen og styrer oppstart og utvikling av spillet, dvs. laster inn et nytt nivå eller bra, setter spillet igang og driver det fremover ved å ta imot input fra brukeren og videreformidle det til spillet. Spillklassen din må innordne seg hvordan GUI-et gjør dette, ved å tilby et standard sett med metoder definert i GridGame-grensesnittet. De viktigste metodene er knyttet til den overordnede livssyklusen til spillet:

  • void init(String) - denne metoden kalles av GUI-et for å initialisere et nytt spill, men uten at det settes igang. Argumentet er ment å angi et nivå, brett eller det som gir mening for spillet ditt. Det kan f.eks. være en nettadresse hvor nivået ligger, nivået koden direkte i en String eller dimensjonene til et brett. Det er brukeren som skriver String-argumentet inn, og det er opp til implementasjonen av metoden å tolke og bruke det.
  • GridProvider getGridProvider() - denne metoder returnerer objektet som representerer selve rutenettet, som ble initialisert av init-metoden. Ved hjelp av dette objektet, kan GUI-et vise frem rutenettet og registrere seg som lytter for å få beskjed om endringer i rutenettet.
  • void run(GameOutput) - denne metoden setter igang spillet. Argumentet er et objekt som spillet må huske og som under spillets gang kan brukes for å gi meldinger og statusoppdateringer til brukeren. (I praksis er dette objektet GUI-et selv, og GUI-ets implementasjon av GameOutput-metodene angjør hvordan meldinger vises til brukeren.)
  • Integer doCommand(String) - det er denne metoden som driver spillet fremover ved at brukerinput formidles fra GUI-et til spillet. Argumentet er en slags kommandolinje, på et format som kan tolkes og brukes av spillet. Returverdien er spillets måte å fortelle GUI-et om spillet er ferdig og i tilfelle hva som ble resultatet. En null-verdi betyr at spillet ikke er ferdig og 0 betyr at det ikke ble noe resultat (eller uavgjort). En negativ eller positiv verdi angir hhv. tap eller seier, evt. hvilken av to spillere som vant.

Meldinger til brukeren

Den viktigste logikken i spillet ligger typisk i doCommand-metoden, siden den håndterer input fra brukeren. Effekten vil typisk være at spillet endrer rutenettet, og dette fanges opp av GUI-et, som lytter til rutenettendringer, slik at den grafisk visningen kan oppdateres. I tillegg kan spillet gi meldinger og statusoppdateringer til brukeren vha. GameOutput-objektet som ble gitt til run-metoden, da spillet startet. Det er derfor dette objektet må lagres unna, slik at det er tilgjengelig for doCommand-metoden.

GameOutput-objektet tilbyr tre metoder, for meldinger av ulik alvorlighetsgrad:

  • info(String) - viser String-argumentet som en enkelt statusoppdatering, f.eks. "x sin tur" eller "4 av 8 bokser er på plass".
  • warning(String) - viser String-argumentet som et (forsiktig) varsel. f.eks. "Ulovlig trekk".
  • error(String) - viser String-argumentet som et (kraftig) varsel. f.eks. "Inkonsistent brett".

Akkurat hvordan meldingen vises er opp til GUI-et, som implementerer metodene, men tanken er når alvorlighetsgraden øker, så blir meldinger mer tydelig og påtrengende. F.eks. kan info oppdatere en statuslinje, mens error åpner en meldingsdialog.

Visning av rutenett-element

GridGame har i tillegg til metodene angitt over to metoder for å angi hvordan elementene i rutenettet vises. Tanken er at hver rute i rutenettet kan vises med tekst og/eller bilde og at GUI-et for hver rute spør om disse med følgende metoder:

  • String getTextFor(Object) - returnerer teksten som skal vises for rutenett-elementet angitt av argumentet. Dersom en ikke ønsker en tekst så returneres null.
  • String getImageFor(Object) - returnerer en String som angir bildet som skal vises for rutenett-elementet angitt av argumentet. String-en kan være en nettadresse eller navnet/stien til en fil som er relativt til spillklassen eller prosjektet. Hvis en f.eks. har en fil med navn bilde.png liggende i samme mappe/pakke som java-fila for spillklassen din, så returneres bare "bilde.png". Dersom en ikke ønsker et bilde så returneres null.

Begge disse metodene tar et Object-argumentet, og disse er alle hentet med GridProvider sin getGridElement-metode. Siden argument-typen er Object, så kan det tenkes at du må bruke cast til den typen du vet ligger i rutenettet, for å kunne hente ut data som ligger inni dette objektet.

Håndtering av ikke-tekstlig input

doCommand-metoden beskrevet over, brukes for tekstlig input tilsvarende et kommandlinje-basert spill. For å gjøre spillet bedre egnet til å styres av et GUl så har GridGame i tillegg følgende metoder for å ta imot input:

  • Integer gridElementInput(int x, int y) - kalles når brukeren har valgt en rute i rutenettet, f.eks. ved å klikke i ruta.
  • Integer directionInput(int dx, int dy) - kalles når brukeren angir en retning, f.eks. ved å trykke en piltast.

For begge disse tolkes returverdien som for doCommand, og effekten kan, som for doCommand, være en kombinasjon av oppdatering av rutenettet eller tekstlig output til brukeren vha. GameOutput.

 

Eksempler på bruk av GUI-et

Det kan være litt vanskelig å skjønne hvordan instanser av din spillklasse brukes av GUI-et, så under så forklarer vi GUI-et og hvordan ulike type bruk trigger kallsekvenser hvor spillklassen inngår.

Image Removed

GUI-et har tre deler, fra øverst til nederst:

  • Øverst: En kan angi et nivå/bret e.l. og starte spiller ved å trykke "init and run"-knappen
  • I midten: Rutenettet vises, og hvis det har fokus, så kan en trykke piltastene eller klikke på en rute
  • Nederst: Tekstfelt for å gi inn kommandoer
PlantUML Macro
border1
actor Bruker
Bruker -> GUI: init og run
GUI -> DinSpillklasse: init(<innhold av tekstfelt>)
GUI -> DinSpillklasse: getGridProvider()
GUI -> GridProvider: getGridWidth(), getGridHeight()
GUI -> GridProvider: getGridElement(...)
GUI -> DinSpillklasse: getTextFor(...), getImageFor(...)
GUI -> Bruker: rutenettoppdatering
GUI -> GridProvider: addGridListener(this)
GUI -> DinSpillklasse: run(this)
DinSpillklasse -> GUI: info(...)
GUI -> Bruker: statusoppdatering

Når brukeren trykker "init and run"-knappen vil GUI-et ta innholdet i det øverste tekstfeltet og gi det som argument til spillklassen sin init-metode. Deretter vil den be om å få rutenettet vha. getGridProvider()-metoden. Dette objektet vil så spørres om antall kolonner (getGridWidth()) og rader (getGridHeight()) og selve innholdet (getGridElement(...)), slik at den kan tegne rutenettet. Akkurat hvilken tekst og hvilket bilde som vises pr. rute bestemmes av svaret fra getTextFor(...) og getImageFor(...). Til slutt vil GUI-et registrere seg som lytter, slik at den kan reagrere på fremtidige endringer i rutenett. Til slutt startes selve spillet med et kall til run(...) og denne metoden vil typisk gi oppstartsinfo til brukeren med info(...)-metoden.

PlantUML Macro
border1
actor Bruker
Bruker -> GUI: doCommand
GUI -> DinSpillklasse: doCommand(<innhold av tekstfelt>)
DinSpillklasse -> DinRutenettklasse: <endre rutenett>
DinRutenettklasse -> GUI: gridChanged(...)
GUI -> GridProvider: getGridElement(...)
GUI -> DinSpillklasse: getTextFor(...), getImageFor(...)
GUI -> Bruker: rutenettoppdatering
DinSpillklasse -> GUI: info(...)
GUI -> Bruker: statusoppdatering

Når brukeren trykker "doCommand"-knappen vil GUI-et ta innholdet i det nederste tekstfeltet og gi det som argument til spillklassen sin doCommand-metode. Dette vil typisk trigge en endring i rutenettet, som gir beskjed til lytterne, inkl. GUI-et, om endringen. GUI-et vil hente innholdet i ruten(e) som ble endret med getGridElement(...) og hvilken tekst og hvilket bilde som skal vises med getTextFor(...). Det kan også hende at statusen til spillet oppdateres med info(...)-metoden.

Kjøring av spillet

Som forklart over, så skal spillet kobles til en ferdigskrevet GUI-klasse. I tillegg trenger du to andre filer, her er hele lista:

For denne delen av oppgaven trenger du følgende filer: imagegrid/GridListener.java og imagegrid/ObservableGrid.java

Del 2: Utvidelse av spillet

I denne deloppgaven skal spillet utvides med ny funksjonalitet. Hva denne funksjonaliteten består i avhenger av hvilket spill du har valgt å implementere. Dette er beskrevet på egne sider for hvert spill:

Filene som er nødvendige for å kjøre det forhåndsskrevne GUI-et er tilgjengelig på disse ulike oppgavesidene. Siden det er GUI-et som styrer showet, så er det GridGameGUI som klassen <spill>FX.java som skal startes ved kjøring av spillet ditt. GridGameGUI har en main-metode som tar spillklassen sitt navn som argument. Det enkleste er å lage en main-metoden i din egen spillklasse, som følger (bytt ut DinSpillklasse med navnet på din egen spillklasse):

Code Block
    public static void main(String[] args) throws Exception {
        GridGameGUI.main(new String[]{DinSpillklasse.class.getName()});
    }