Skapa ett eget spel i Java

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.

Spel i Java
Spel i Java

Klassen GameObject

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)
 
}

Skapa ett interface

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:

  • tick() – Innehåller allt som ska hända vid varje uppdatering
  • render() – Innehåller allt som gör att objekten ritas ut på spelplanen på rätt position

Ladda in bilder

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;
    }
}

Skapa klassen Player

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;
    }
}

Skapa klassen Wall

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
    }
}

Skapa en räknare

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;
    }
}

Skapa en start-meny när för spelet

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)
    }
}

Slå samman alla klasser

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!

Ladda ner spelet här

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!