LÄR DIG JAVA

Skapa ditt eget spel - Uppbyggnad av spelet

Här finns hela implementationen av Fallande väggar. Varje publik klass eller gränssnitt ligger i en egen fil med samma namn. Koden nedan är samma kod som i nedladdningen.

Ladda ner hela spelet med källkod och bilder (ZIP). Packa upp filen och läs README.txt. Du behöver en JDK och en dator med grafiskt skrivbord; spelet körs lokalt, inte i webbläsaren.

Kompilera och starta

Öppna en terminal i den uppackade mappen. Bilderna Background.png, hero.png och wall.png måste ligga tillsammans med klassfilerna på klassökvägen. Filnamnens stora och små bokstäver spelar roll.

javac -encoding UTF-8 *.java
java Game

Så hänger delarna ihop

GameObject håller position och skapar kollisionsrektanglar. Entity anger metoderna tick och render. Player och Wall ärver positionen och implementerar gränssnittet. Ett enda Wall-objekt ritar åtta block; två rutor lämnas tomma. Counter visar poängen och Starter visar startskärmen.

Game är en JPanel som samordnar spelet. En Swing Timer begär uppdateringar med ungefär 16 millisekunders mellanrum. Uppdatering, tangentbordshändelser och målning hanteras på Swings händelsetråd, EDT. Det är ingen garanti för exakt 60 bilder per sekund. Undvik långvarigt arbete i dessa metoder.

Spelaren hålls inom spelytan. Tangenter lagras som tillstånd så att flera piltangenter fungerar samtidigt; tillståndet rensas när fönstret tappar fokus. Väggen börjar ovanför spelytan, får en ny öppning efter varje passage och återställs vid omstart.

Entity.java

import java.awt.*;


public interface Entity {

    void tick();
    void render(Graphics g);

}

GameObject.java

import java.awt.*;

public class GameObject {
    public int x, y;


    public GameObject(int x, int y) {
        this.x = x;
        this.y = y;
    }


    public int getX() {
        return x;
    }


    public int getY() {
        return y;
    }


    public void setX(int x) {
        this.x = x;
    }

    public void setY(int y) {
        this.y = y;
    }

    public Rectangle getBounds(int x, int y){
        return new Rectangle(x, y, 64, 64);
    }

}

BufferedImageLoader.java

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.net.URL;

public class BufferedImageLoader {
    public BufferedImage loadImage(String path) throws IOException {
        URL resource = getClass().getResource(path);
        if (resource == null) throw new IOException("Bildfilen saknas: " + path);
        BufferedImage image = ImageIO.read(resource);
        if (image == null) throw new IOException("Bildformatet kunde inte läsas: " + path);
        return image;
    }
}

Player.java

import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;


public class Player extends GameObject implements Entity {
    private BufferedImage hero;
    private double preciseX, preciseY;
    private double velX = 0;
    private double velY = 0;

    public Player(int x, int y) throws IOException {
        super(x, y);

        preciseX = x;
        preciseY = y;
        BufferedImageLoader loader = new BufferedImageLoader();
        hero = loader.loadImage("/hero.png");
    }

    @Override
    public void tick() {
        preciseX = Math.max(0, Math.min(Game.WIDTH - Game.SQUARE, preciseX + velX));
        preciseY = Math.max(0, Math.min(Game.HEIGHT - Game.SQUARE, preciseY + velY));
        x = (int) Math.round(preciseX);
        y = (int) Math.round(preciseY);
    }

    @Override public void setX(int x) { super.setX(x); preciseX = x; }
    @Override public void setY(int y) { super.setY(y); preciseY = y; }

    @Override
    public void render(Graphics g) {
        g.drawImage(hero, x, y, null);
    }

    //Velocity
    public void setVelX(double velX){
        this.velX = velX;
    }

    public void setVelY(double velY){
        this.velY = velY;
    }
}

Wall.java

import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Random;


public class Wall extends GameObject implements Entity {                //Ärver GameObject och har Entity som interface
    private BufferedImage wall;                                         //Skapar bildobjektet
    private double velY = 1;
    private double preciseY = 0;                                            //Sätter hastigheten, initialt till 1.
    Random random;                                                      //Skapar objektet random
    int rand;                                                           //Skapar instansvariabeln rand, som ska innehålla slumpvärdet

    public Wall(int x, int y) throws IOException {                                         //Konstruktorn
        super(x, y);                                                    //Instansvariablerna sätts av superklassen GameObject

        preciseY = y;
        BufferedImageLoader loader = new BufferedImageLoader();         //Skapa "bild-laddar"-objektet
        wall = loader.loadImage("/wall.png");
        random = new Random();                                          //Skapa ett nytt objekt som ska hantera random
        rand = random.nextInt(Game.FACTOR - 1);                                      //Slumpa ett tal mellan 0 och 8, (det är här första hålet i väggen är)
    }

    @Override
    public void tick() {                                                //Denna metod körs vid varje klockcykel
        preciseY += velY;
        y = (int) preciseY;                                                //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=-Game.SQUARE;
            preciseY=y;                                                        //Placera väggen precis ovanför spelytan
            rand = random.nextInt(Game.FACTOR - 1);                                  //Slumpa ett nytt tal (nytt hål i väggen)
            setVelY(Math.min(velY+0.5, 8));                                          //Ö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 + Game.SQUARE * i, y, null);                 //Rita ut objektet wall på rätt position
            }
        }
    }


    public void reset() {
        y = -Game.SQUARE;
        preciseY = y;
        velY = 1;
        rand = random.nextInt(Game.FACTOR - 1);
    }

    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(x + Game.SQUARE*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ågot av de åtta väggblocken korsar varandra, returnera true = Game Over
                return true;
            }
        }
        return false;                                                   //Om ingen av wall-objekten korsar player så returnera false
    }

}

Counter.java

import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;

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
        count = 0;
    }
}

Starter.java

import java.awt.*;

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

KeyInput.java

import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;

public class KeyInput extends KeyAdapter {

    Game game;

    public KeyInput(Game game){
        this.game = game;
    }

    public void keyPressed(KeyEvent e){
        game.keyPressed(e);
    }

    public void keyReleased(KeyEvent e){
        game.keyReleased(e);
    }

}

Game.java

import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.awt.event.KeyEvent;
import java.awt.image.BufferedImage;
import java.io.IOException;

/** Ett litet Swing-spel. Allt muterbart speltillstånd hanteras på EDT. */
public class Game extends JPanel {
    public static final int SQUARE = 64;
    public static final int FACTOR = 10;
    public static final int WIDTH = SQUARE * FACTOR;
    public static final int HEIGHT = SQUARE * FACTOR;
    public enum STATE { GAME, RESTART }
    public static STATE state = STATE.RESTART;

    private final BufferedImage background;
    final Player player;
    final Wall wall;
    private final Counter counter = new Counter();
    private final Starter starter = new Starter();
    private final Timer timer;
    private boolean left, right, up, down;

    public Game() throws IOException {
        background = new BufferedImageLoader().loadImage("/Background.png");
        player = new Player(WIDTH / 2 - SQUARE / 2, HEIGHT - SQUARE);
        wall = new Wall(0, -SQUARE);
        wall.reset();
        setPreferredSize(new Dimension(WIDTH, HEIGHT));
        setFocusable(true);
        addKeyListener(new KeyInput(this));
        addFocusListener(new FocusAdapter() {
            @Override public void focusLost(FocusEvent event) { clearKeys(); }
        });
        // Swing Timer anropar uppdateringen på samma tråd som tangent- och ritningshändelser.
        timer = new Timer(16, event -> tick());
    }

    private void clearKeys() {
        left = right = up = down = false;
        player.setVelX(0);
        player.setVelY(0);
    }

    void restart() {
        clearKeys();
        player.setX(WIDTH / 2 - SQUARE / 2);
        player.setY(HEIGHT - SQUARE);
        wall.reset();
        counter.reset();
        state = STATE.GAME;
    }

    void tick() {
        if (state == STATE.GAME) {
            double dx = (right ? 1 : 0) - (left ? 1 : 0);
            double dy = (down ? 1 : 0) - (up ? 1 : 0);
            double length = Math.hypot(dx, dy);
            double speed = length == 0 ? 0 : 7 / length;
            player.setVelX(dx * speed);
            player.setVelY(dy * speed);
            player.tick();
            wall.tick();
            if (wall.collision(player.getBounds(player.getX(), player.getY()))) {
                state = STATE.RESTART;
                clearKeys();
            }
        }
        repaint();
    }

    @Override protected void paintComponent(Graphics graphics) {
        super.paintComponent(graphics);
        graphics.drawImage(background, 0, 0, getWidth(), getHeight(), null);
        if (state == STATE.GAME) {
            player.render(graphics);
            wall.render(graphics);
            counter.render(graphics);
        } else {
            starter.render(graphics);
        }
    }

    public void keyPressed(KeyEvent event) {
        int key = event.getKeyCode();
        if (state == STATE.RESTART && key == KeyEvent.VK_ENTER) {
            restart();
            return;
        }
        if (state != STATE.GAME) return;
        if (key == KeyEvent.VK_LEFT) left = true;
        if (key == KeyEvent.VK_RIGHT) right = true;
        if (key == KeyEvent.VK_UP) up = true;
        if (key == KeyEvent.VK_DOWN) down = true;
    }

    public void keyReleased(KeyEvent event) {
        int key = event.getKeyCode();
        if (key == KeyEvent.VK_LEFT) left = false;
        if (key == KeyEvent.VK_RIGHT) right = false;
        if (key == KeyEvent.VK_UP) up = false;
        if (key == KeyEvent.VK_DOWN) down = false;
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> {
            try {
                Game game = new Game();
                JFrame frame = new JFrame("Codebean – Fallande väggar");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.setContentPane(game);
                frame.pack();
                frame.setResizable(false);
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
                game.requestFocusInWindow();
                game.timer.start();
            } catch (IOException error) {
                System.err.println("Spelet kunde inte laddas: " + error.getMessage());
            }
        });
    }
}

Vanliga frågor om Skapa ditt eget spel - Uppbyggnad av spelet

Vad handlar Skapa ditt eget spel - Uppbyggnad av spelet om?

Bygg Fallande väggar i Java med Swing. Komplett källkod, bilder och en fri nedladdning som motsvarar guidens exempel.

Hur övar jag på Skapa ditt eget spel - Uppbyggnad av spelet?

Kör exemplen själv, ändra ett värde i taget och jämför sedan resultatet med förklaringen på sidan.