Med objektorienterad programmering kan vi enkelt skapa flera klasser med var sitt syfte som samverkar med varandra.
I vårt spel kommer vi ha ett antal klasser som samverkar med varandra. Vi kommer ha en playerklass som är själva spelaren man ska styra med piltangenterna. Vi kommer även att ha en klass Wall, som är en wall-bricka i spelet. I spelet faller åtta stycken wall-brickor neråt. Vi kommer alltså skapa 8 objekt av wall-klassen. Vi kommer även att skapa en klass för att ladda in bilder och en räknare som håller koll på hur många gånger vi lyckas passera väggen. Allting kommer till slut att byggas tillsammans i en Game-klass. Det rekommenderas att läsa igenom kapitlet om klasser och objekt innan du börjar, för att enklare förstå hur klasser och objekt samverkar med varandra.
I första klassen GameObject ska x- och y-koordinaten för varje objekt hanteras. Eftersom alla objekt i spelet behöver koordinater (så att man kan placera den på önskat ställe på spelrutan) kommer varje klass som kräver koordinater, kunna ärva från denna klass.
public class GameObject { public int x, y; public GameObject(int x, int y) { // Konstruktorn deklarerar klassvariablerna this.x = x; this.y = y; } public int getX() { // Returnera respektive koordinat return x; } public int getY() { return y; } public void setX(int x) { // Sätt ett nytt värde på x respektive y this.x = x; } public void setY(int y) { this.y = y; } public Rectangle getBounds(int x, int y){ //Returnerar det område som objektet täcker på spelplanen, return new Rectangle(x, y, 64, 64); //(så att man kan avgöra om två objekt kolliderar med varandra) }
Vi skapar sedan ett interface enligt:
public interface Entity { public void tick(); public void render(Graphics g); }
Varje klass som skapas med detta interface, ska ha två metoder:
Klassen BufferedImageLoader skapar ett bildobjekt från de bilder som vi vill ha i spelet:
public class BufferedImageLoader { private BufferedImage image; /* Skapar ett bildobjekt från en bildfil. Om det inte finns någon fil med namnet som skickas in i konstruktorn, kastas ett felmeddelande (IOException) och programmet kommer då sluta att exekvera. */ public BufferedImage loadImage(String path) throws IOException{ image = ImageIO.read(getClass().getResource(path)); return image; } }
Klassen Player är själva spelaren som vi ska styra med piltangenterna. Klassen ärver från GameObject och Entity. Observera alltså att metoderna tick(), render() och getBounds() är ärvda från Entity vilket visas med @Override. Klassen har även två egna metoder setVelX() och setVelY() som kommer sätta hastigheten på spelaren.
public class Player extends GameObject implements Entity { //Ärver klasserna GameObject och Entity private BufferedImage hero; //Bildobjektet på spelaren private double velX = 0; // Hastigheten på spelaren i x- och y-led private double velY = 0; public Player(int x, int y) { //Konstruktorn super(x, y); //Deklarerar klassvariablerna x och y i från superklassen GameObject BufferedImageLoader loader = new BufferedImageLoader(); //Skapar ett objekt från klassen BufferedImageLoader try{ hero = loader.loadImage("/hero.png"); //Tilldelar bildobjektet en bild från bildfilen med namnet "hero.png". Hämtad från pixabay.com (user: alvaro11basket) } catch (IOException e){ //Kastar felmeddelande om tilldelningen inte lyckas e.printStackTrace(); } } @Override public void tick() { x+=velX; //Sätter nya koordinater på x och y baserat på hastigheten y+=velY; if(x <=0){ // Kontrollerar så att koordinaterna inte är utanför spelplanen x=0; } if(x >=Game.WIDTH-64){ //Koordinaten på objektet utgår alltid övre, vänstra hörnet, vi drar alltså bort 64 pixlar eftersom det är objektets storlek x=640-64; } if(y <=0){ //Motsvarande gäller för y-led y=0; } if(y >=Game.HEIGHT-64){ y=Game.HEIGHT-64; } } @Override //Override visar att metoden är ärvd public void render(Graphics g) { g.drawImage(hero, x, y, null); //Ritar ut spelaren på spelplanen med koordinaterna x och y } @Override public Rectangle getBounds() { return new Rectangle((int)x, (int)y, 64, 64); //Skapar objektets yta. I detta fall en kvadrat på 64 x 64 pixlar } //Sätter hastigheten i x- och y-led public void setVelX(double velX){ this.velX = velX; } public void setVelY(double velY){ this.velY = velY; } }
Klassen Wall hanterar det fallande väggen som spelaren ska undvika. Liksom klassen Player, ärver denna klass från GameObject och Entity. I denna klass hanteras även om det sker en kollision mellan spelaren och väggen. Det kommer finnas ett wall-objekt (en ruta) som skrivs ut totalt åtta gånger på spelplanen (två hål i väggen).
public class Wall extends GameObject implements Entity { //Ärver GameObject och har Entity som interface private BufferedImage wall; //Skapar bildobjektet private double velY = 1; //Sätter hastigheten, initialt till 1. Random random; //Skapar objektet random int rand; //Skapar klassvariablen rand, som ska innehålla slumpvärdet public Wall(int x, int y) { //Konstruktorn super(x, y); //Klassvariablerna sätts av superklassen GameObjekt BufferedImageLoader loader = new BufferedImageLoader(); //Skapa "bild-laddar"-objektet try{ wall = loader.loadImage("/wall.png"); //Ladda bilden som heter "wall.png" } catch (IOException e){ //Om filen inte hittas, kasta ett fel (varna) e.printStackTrace(); //Skriv ut felet } random = new Random(); //Skapa ett nytt objekt som ska hantera random rand = random.nextInt(10); //Slumpa ett tal mellan 0-9, (det är här första hålet i väggen är) } @Override public void tick() { //Denna metod körs vid varje klockcykel y = y+(int)velY; //Sätter nya positionen på objektet (wall) if(y>=Game.HEIGHT){ //Om objektet har kommit längst ner i bild, starta om från början y=0; //Sätt koordinaten på objektet till 0 (överst i bild) rand = random.nextInt(10); //Slumpa ett nytt tal (nytt hål i väggen) setVelY(velY+0.5); //Öka hastigheten till nästa varv (så att spelet blir svårare och svårare) Counter.count++; //Öka räknare med ett (håller räkningen på hur många varv vi har klarat) } } @Override public void render(Graphics g) { //Denna metod ritar ut objektet på rätt position for(int i = 0; i < Game.FACTOR; i++ ) { //Rita ut objektet Game.Factor gånger (10 ggr) if(i != rand && i != rand+1) { //Rita bara ut objektet om vi inte är på den positionen där det ska vara ett hål g.drawImage(wall, x + 64 * i, y, null); //Rita ut objektet wall på rätt position } } } public void setVelY(double velY){ //Sätt ny hastighet this.velY = velY; } /* Vi skapar här en arraylist som vi fyller med rektanglar. Varje rektangel är en "del av väggen" (totalt 10 stycken - 2 hål) Genom att skapa reklanglar kan vi enkelt se om en vägg kolliderar med spelaren = Game Over, med hjälp av den inbyggda funktionen "intersect" som undersöker om två rektanglar korsar varandra. */ private ArrayList<Rectangle> getRectangleWall(){ ArrayList<Rectangle> rectangles = new ArrayList<Rectangle>(); //Skapar en Arraylist av typen Rectangle for(int i = 0; i < Game.FACTOR; i++ ) { //Loopar 10 ggr if(i != rand && i != rand+1) { //Lägger en ny rektangle i arraylisten om vi inte är "på hålet" rectangles.add(getBounds(64*i, y)); //GetBounds returnerar en ny rektangel från superklassen GameObject } } return rectangles; //Returnerar listan } public boolean collision(Rectangle rectanglePlayer){ //Undersöker om objektet player och wallen kolliderar med varandra = Game Over for(Rectangle rec : getRectangleWall()) { //Loopar igenom hela listan med rektanglar från getRectangleWall() if (rectanglePlayer.intersects(rec)) { //Om player och någon av de 8 wall-objekten korsar varandra, returnera true = Game Over return true; } } return false; //Om ingen av wall-objekten korsar player så returnera false } }
Klassen Counter håller räkningen på antal varv vi har klarat av:
public class Counter { //Denna klass håller räkningen på hur många varv vi klarar av Font fnt0 = new Font("Comics", Font.BOLD, 20); //Skapar en ny font public static int count = 0; //En statisk räknare som alla klasser kommer åt public void render(Graphics g) { //Ritar ut värdet på count g.setFont(fnt0); //Anger fonten g.setColor(Color.BLACK); //Anger färgen g.drawString(Integer.toString(count), 10, 30); //Ritar ut count på (x=10, y=30) } public void reset(){ //Om metoden reset anropas, nollställs räknaren this.count = 0; } }
Klassen Starter skriver ut “press enter to start” för att starta spelet
public class Starter { //Klassen Starter skriver enbart ut texten "Press enter to start" Font fnt0 = new Font("Comics", Font.BOLD, 25); //Skapar en ny font, (comics, bold, strl=25) public void render(Graphics g) { //Ritar ut texten på spelplanen g.setFont(fnt0); //Anger fonten g.setColor(Color.BLACK); //Sätter färgen på texten till svart g.drawString("Press Enter to start", 205, 220); //Skriver ut texten på x-pos: 205, y-pos:220 (mitten av spelplanen) } }
Avslutningsvis skapar vi klassen Game som ska vara grunden i själva spelet. Här skapar vi spelfönstret och kopplar samman de övriga klasser som ska finnas. Här hanterar vi även kommandot från skrivbordet som användaren manövrerar.
import javax.swing.*; import java.awt.*; import java.awt.event.KeyEvent; import java.awt.image.BufferStrategy; import java.awt.image.BufferedImage; import java.io.IOException; /* Huvudklassen Game ärver från den inbyggda klassen Canvas som innehåller användbara funktioner för att skapa spelet, som exempelvis en keylistener som kopplar tangenttryckningarna till spelet och funktioner för att skapa spelfönstret. Game implementerar även Runnable som gör det möjligt att starta och stoppa threads. */ public class Game extends Canvas implements Runnable { public static final int SQUARE = 64; //Delar in spelplanen i rutor, varje ruta är 64 pixlar public static final int FACTOR = 10; //Spelplanen är 10 rutor bred public static final int WIDTH = SQUARE*FACTOR; //Längden på spelplanen är antal rutor*längden på varje ruta public static final int HEIGHT = SQUARE*FACTOR; //Samma höjd som bredd public static final int SCALE = 1; //Scale om man vill förstora eller minska spelet proportionellt public final String TITLE = "ProgrammeraJava"; //Namnet på spelet private boolean running = false; //En boolean om tråden ska köras eller ej private Thread thread; //Skapar en ny tråd private BufferedImage background; //Skapa alla objekt som ska samverka med varandra private Player player; //Skapar ett nytt objekt från klassen Player private Wall wall; //Skapar ett nytt objekt från klassen Wall private Starter start; //Skapar ett nytt objekt från klassen Starter private Counter counter; //Skapar ett nytt objekt från klassen Counter public static enum STATE{ //Skapar två enum. GAME, //Game är aktivt när spelet körs RESTART, //Game är aktivt när man väntar på att starta spelet ("press enter to start") }; public static STATE state = STATE.RESTART; //Börja spelet med i läget "press enter to start" public void init(){ //Skapar alla initialtillstånd requestFocus(); //Gör så att spelet är aktivt när kompilatorn skapar spelplanen (ärvd från Canvas) BufferedImageLoader loader = new BufferedImageLoader(); //Skapar en ny bildladdar-objekt try{ background = loader.loadImage("/background.png"); //Försöker ladda in bakgrunden } catch (IOException e){ //Om fel inträffar, släng ett felmeddelande e.printStackTrace(); //Skriver ut felmeddelande } this.addKeyListener(new KeyInput(this)); //Kopplar tangenttryckningarna till spelet player = new Player(getWidth()/2-32,getHeight()); //Skapar ett nytt playerobjekt och ger koordinaterna till objektet wall = new Wall(0,0); //Skapar ett nytt wallobjekt och ger koordinaterna till objektet counter = new Counter(); //Skapar ett nytt objekt av typen Counter start = new Starter(); //Skapar ett nytt objekt av typen Starter } private synchronized void start(){ //Metod för att starta tråden (starta spelet) if(running){ //Om spelet redan är startat, return (för att inte starta tråden flera gånger) return; } running = true; //Sätter boolean running till true, spelet är startat thread = new Thread(this); //Skapar en tråd och kopplar spelet till tråden thread.start(); //Starta tråden } private synchronized void stop(){ //Motsvarande som start fast för att stänga tråden if(!running) { //Kör endast metoden om spelet är igång return; } running = false; try { thread.join(); //Slutför tråden } catch (InterruptedException e){ //Om något får fel, kasta felmeddelande e.printStackTrace(); } System.exit(1); //Stänger ner spelet } public void run(){ //Metoden som körs när spelet är igång. Hanterar tick och render init(); //Skapar alla objekten long lastTime = System.nanoTime(); //Nuvarande tiden i nanosekunder sen final double amountOfTicks = 60.0; //Hastighet på uppdateringarna double ns = 1000000000 / amountOfTicks; //Antal uppdateringar per sekund double delta = 0; //Skillnad på nuvarande tid och senaste uppdatering (tick) int updates = 0; //Antal uppdateringar per sekund int frames = 0; long timer = System.currentTimeMillis(); //Antal millisekunder sen 1 Januari, 1970 (klassiskt startdatum i programmering) while(running){ //Kör spelet till spelet stängs av long now = System.nanoTime(); //Nuvarande tid delta += (now - lastTime) / ns; //Beräknar skillnaden lastTime = now; //Nytt tidsteg if(delta >=1){ //Om antal steg är större än 1, uppdatera tick(); //Kör metoden tick updates++; //Uppdatera delta--; //Nollställ Delta } render(); //Rita ut alla objekten frames++; //Räkna hur många frames som ritas ut //If-satsen behövs inte för spelet men ger mer förståelse över hur många frames som skrivs ut if(System.currentTimeMillis() - timer > //Skriv ut hur många "frame per seconds" som har ritas ut timer += 1000; //Uppdatera med en sekund //System.out.println(updates + " Ticks, Fps " + frames); //Skriv ut antal frames per sekund updates = 0; //Nollställ frames = 0; } } stop(); //Om spelet stängs av, stäng av tråden } private void tick(){ //Körs varje uppdatering if(state==STATE.GAME) { //Uppdatera bara om vi är i "spelläge" player.tick(); //Uppdatera player wall.tick(); //Uppdatera wall if(wall.collision(player.getBounds(player.getX(), player.getY()))){ //Om player och wall kolliderar med varandra, ändra spelläge till "start-läge" state = STATE.RESTART; //Ändra till startläge } } } private void render(){ //Ritar ut objekten BufferStrategy bs = this.getBufferStrategy(); //BufferedStrategy gör det möjligt att ladda flera frames samtidigt, ger ett snabbare spel if (bs == null){ createBufferStrategy(3); //Buffrar tre lager åt gången return; } Graphics g = bs.getDrawGraphics(); //Gör det möjligt att rita ut objekten på spelplanen g.drawImage(background, 0, 0, null); //Ritar ut bakgrunden if(state == STATE.GAME) { //Uppdatera bara om vi är i "spelläge" player.render(g); //Rita ut objektet player wall.render(g); //Rita ut objektet wall counter.render(g); //Rita ut objektet counter } if(state == STATE.RESTART){ //Om spelet är i "start-läge" start.render(g); //rita ut objektet starter } ////// ends here g.dispose(); //Uppdaterar föregående ritning bs.show(); //Gör alla objekt synliga } //Följande metoder gör det möjligt att hantera tangenttryck public void keyPressed(KeyEvent e){ int key = e.getKeyCode(); //Nytt tangenttryck if(state == STATE.GAME) { //Piltangenterna fungerar bara när vi är i "spel-läge" if (key == KeyEvent.VK_RIGHT) { //Om vi trycker höger, öka hastigheten på player åt höger player.setVelX(9); } else if (key == KeyEvent.VK_LEFT) { //Om vi trycker vänster, öka hastigheten på player åt vänster player.setVelX(-9); } else if (key == KeyEvent.VK_DOWN) { //Om vi trycker neråt, öka hastigheten på player neråt player.setVelY(9); } else if (key == KeyEvent.VK_UP) { //Om vi trycker upp, öka hastigheten på player uppåt player.setVelY(-9); } } else if (state == STATE.RESTART){ //Om vi är i "start-läge" if(key == KeyEvent.VK_ENTER){ //Om vi trycker enter state = STATE.GAME; //Ändra till "spel-läge" player.setX(getWidth()/2-32); //Sätt ny x-koordinat på player player.setY(getHeight()); //Sätt ny y-koordinat på player wall.y = 0; //Sätt ny y-koordinat på wall wall.setVelY(3); //Sätt starthastighet på wall counter.reset(); //Nollställ räknaren } } } public void keyReleased(KeyEvent e){ //När en tangent släpps int key = e.getKeyCode(); //Ny tangent har släppts if(key == KeyEvent.VK_RIGHT){ //Om vi släpper högertangenten, sätt hastigheten åt höger till noll player.setVelX(0); } else if(key == KeyEvent.VK_LEFT){ //Om vi släpper vänstertangenten, sätt hastigheten åt vänster till noll player.setVelX(0); } else if(key == KeyEvent.VK_DOWN){ //Om vi släpper nertangenten, sätt hastigheten neråt till noll player.setVelY(0); } else if(key == KeyEvent.VK_UP){ //Om vi släpper upptangenten, sätt hastigheten uppåt till noll player.setVelY(0); } } //Mainmetoden - körs först i programmet public static void main (String args[]){ Game game = new Game(); //Skapar ett nytt game game.setPreferredSize(new Dimension(WIDTH * SCALE, HEIGHT * SCALE)); //Skapar dimensionerna för spelet enligt vad som är angivet i klassvariablerna game.setMaximumSize(new Dimension(WIDTH * SCALE, HEIGHT * SCALE)); game.setMinimumSize(new Dimension(WIDTH * SCALE, HEIGHT * SCALE)); JFrame frame = new JFrame(game.TITLE); // Skapar objektet frame och ger titeln på spelet frame.add(game); // Kopplar objektet game till GUI:t frame.pack(); //Gör GUI:t i rätt storlek (samma storlek som spelet) frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); //Om man stänger fönstret, stäng av programmet frame.setResizable(false); //Gör så att man inte kan ändra storlek på fönstret frame.setLocationRelativeTo(null); //Placerar GUI:t mitt på skärmen frame.setVisible(true); //Gör fönstret synligt för användaren game.start(); //Starta spelet } }
Hämta hem hela spelet och prova själv!
Vad tyckte du om sidan?
Lämna gärna feedback och hjälp oss göra sidan bättre
Feedback gör oss bättre!
Lämna gärna feedback om vad du tyckte om avsnittet!